diff --git a/Analysis/include/Luau/AstUtils.h b/Analysis/include/Luau/AstUtils.h index 01ed15a3..0be03925 100644 --- a/Analysis/include/Luau/AstUtils.h +++ b/Analysis/include/Luau/AstUtils.h @@ -13,7 +13,15 @@ namespace Luau // uniquely held references. Append these types to 'uniqueTypes'. void findUniqueTypes(NotNull> uniqueTypes, AstExpr* expr, NotNull> astTypes); -void findUniqueTypes(NotNull> uniqueTypes, AstArray exprs, NotNull> astTypes); -void findUniqueTypes(NotNull> uniqueTypes, const std::vector& exprs, NotNull> astTypes); +void findUniqueTypes( + NotNull> uniqueTypes, + AstArray exprs, + NotNull> astTypes +); +void findUniqueTypes( + NotNull> uniqueTypes, + const std::vector& exprs, + NotNull> astTypes +); -} +} // namespace Luau diff --git a/Analysis/include/Luau/BuiltinDefinitions.h b/Analysis/include/Luau/BuiltinDefinitions.h index c582d42b..f19e8a2e 100644 --- a/Analysis/include/Luau/BuiltinDefinitions.h +++ b/Analysis/include/Luau/BuiltinDefinitions.h @@ -88,6 +88,7 @@ TypeId getGlobalBinding(GlobalTypes& globals, const std::string& name); bool matchSetMetatable(const AstExprCall& call); bool matchTableFreeze(const AstExprCall& call); bool matchAssert(const AstExprCall& call); +bool matchTypeOf(const AstExprCall& call); // Returns `true` if the function should introduce typestate for its first argument. bool shouldTypestateForFirstArgument(const AstExprCall& call); diff --git a/Analysis/include/Luau/BuiltinTypeFunctions.h b/Analysis/include/Luau/BuiltinTypeFunctions.h index e10b72f0..8c36c00c 100644 --- a/Analysis/include/Luau/BuiltinTypeFunctions.h +++ b/Analysis/include/Luau/BuiltinTypeFunctions.h @@ -9,6 +9,8 @@ namespace Luau struct BuiltinTypeFunctions { BuiltinTypeFunctions(); + BuiltinTypeFunctions(const BuiltinTypeFunctions&) = delete; + void operator=(const BuiltinTypeFunctions&) = delete; TypeFunction userFunc; @@ -51,6 +53,6 @@ struct BuiltinTypeFunctions void addToScope(NotNull arena, NotNull scope) const; }; -const BuiltinTypeFunctions& builtinTypeFunctions(); +const BuiltinTypeFunctions& builtinTypeFunctions_DEPRECATED(); } // namespace Luau diff --git a/Analysis/include/Luau/ConstraintSolver.h b/Analysis/include/Luau/ConstraintSolver.h index f5000830..2cdfe584 100644 --- a/Analysis/include/Luau/ConstraintSolver.h +++ b/Analysis/include/Luau/ConstraintSolver.h @@ -218,18 +218,6 @@ struct ConstraintSolver void generalizeOneType(TypeId ty); - /** - * Bind a type variable to another type. - * - * A constraint is required and will validate that blockedTy is owned by this - * constraint. This prevents one constraint from interfering with another's - * blocked types. - * - * Bind will also unblock the type variable for you. - */ - void bind(NotNull constraint, TypeId ty, TypeId boundTo); - void bind(NotNull constraint, TypePackId tp, TypePackId boundTo); - template void emplace(NotNull constraint, TypeId ty, Args&&... args); @@ -300,7 +288,7 @@ struct ConstraintSolver ValueContext context, bool inConditional, bool suppressSimplification, - DenseHashSet& seen + Set& seen ); /** @@ -404,6 +392,18 @@ struct ConstraintSolver */ void shiftReferences(TypeId source, TypeId target); + /** + * Bind a type variable to another type. + * + * A constraint is required and will validate that blockedTy is owned by this + * constraint. This prevents one constraint from interfering with another's + * blocked types. + * + * Bind will also unblock the type variable for you. + */ + void bind(NotNull constraint, TypeId ty, TypeId boundTo); + void bind(NotNull constraint, TypePackId tp, TypePackId boundTo); + /** * Generalizes the given free type if the reference counting allows it. * @param the scope to generalize in @@ -462,7 +462,12 @@ struct ConstraintSolver void reproduceConstraints(NotNull scope, const Location& location, const Substitution& subst); TypeId simplifyIntersection(NotNull scope, Location location, TypeId left, TypeId right); - TypeId simplifyIntersection(NotNull scope, Location location, std::set parts); + + // Clip with LuauSimplifyIntersectionNoTreeSet + TypeId simplifyIntersection_DEPRECATED(NotNull scope, Location location, std::set parts); + + TypeId simplifyIntersection(NotNull scope, Location location, TypeIds parts); + TypeId simplifyUnion(NotNull scope, Location location, TypeId left, TypeId right); TypePackId anyifyModuleReturnTypePackGenerics(TypePackId tp); diff --git a/Analysis/include/Luau/Frontend.h b/Analysis/include/Luau/Frontend.h index 0ec7f065..cbcb2274 100644 --- a/Analysis/include/Luau/Frontend.h +++ b/Analysis/include/Luau/Frontend.h @@ -225,14 +225,19 @@ struct Frontend ); // Batch module checking. Queue modules and check them together, retrieve results with 'getCheckResult' - // If provided, 'executeTask' function is allowed to call the 'task' function on any thread and return without waiting for 'task' to complete + // If provided, 'executeTasks' function is allowed to call any item in 'tasks' on any thread and return without waiting for them to complete void queueModuleCheck(const std::vector& names); void queueModuleCheck(const ModuleName& name); - std::vector checkQueuedModules( + std::vector checkQueuedModules_DEPRECATED( std::optional optionOverride = {}, std::function task)> executeTask = {}, std::function progress = {} ); + std::vector checkQueuedModules( + std::optional optionOverride = {}, + std::function> tasks)> executeTasks = {}, + std::function progress = {} + ); std::optional getCheckResult(const ModuleName& name, bool accumulateNested, bool forAutocomplete = false); std::vector getRequiredScripts(const ModuleName& name); @@ -270,7 +275,8 @@ struct Frontend void checkBuildQueueItems(std::vector& items); void recordItemResult(const BuildQueueItem& item); void performQueueItemTask(std::shared_ptr state, size_t itemPos); - void sendQueueItemTask(std::shared_ptr state, size_t itemPos); + void sendQueueItemTask_DEPRECATED(std::shared_ptr state, size_t itemPos); + void sendQueueItemTasks(std::shared_ptr state, const std::vector& items); void sendQueueCycleItemTask(std::shared_ptr state); static LintResult classifyLints(const std::vector& warnings, const Config& config); diff --git a/Analysis/include/Luau/NativeStackGuard.h b/Analysis/include/Luau/NativeStackGuard.h new file mode 100644 index 00000000..5ee83f0f --- /dev/null +++ b/Analysis/include/Luau/NativeStackGuard.h @@ -0,0 +1,28 @@ +// This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details +#pragma once + +#include + +namespace Luau +{ + +struct NativeStackGuard +{ + NativeStackGuard(); + + // Returns true if we are not dangerously close to overrunning the C stack. + bool isOk() const; + +private: + uintptr_t high; + uintptr_t low; +}; + +} + +namespace Luau +{ + +uintptr_t getStackAddressSpaceSize(); + +} diff --git a/Analysis/include/Luau/Normalize.h b/Analysis/include/Luau/Normalize.h index 435c86bb..0a45af33 100644 --- a/Analysis/include/Luau/Normalize.h +++ b/Analysis/include/Luau/Normalize.h @@ -202,6 +202,8 @@ enum class NormalizationResult // * G is a union of generic/free/blocked types, intersected with a normalized type struct NormalizedType { + NotNull builtinTypes; + // The top part of the type. // This type is either never, unknown, or any. // If this type is not never, all the other fields are null. @@ -315,6 +317,8 @@ class Normalizer DenseHashMap cachedIsInhabited{nullptr}; DenseHashMap, bool, TypeIdPairHash> cachedIsInhabitedIntersection{{nullptr, nullptr}}; + std::optional fuel{std::nullopt}; + bool withinResourceLimits(); bool useNewLuauSolver() const; @@ -340,13 +344,29 @@ class Normalizer // If this returns null, the typechecker should emit a "too complex" error std::shared_ptr normalize(TypeId ty); - void clearNormal(NormalizedType& norm); + void clearCaches(); + + NormalizationResult isIntersectionInhabited(TypeId left, TypeId right); + + // Check for inhabitance + NormalizationResult isInhabited(TypeId ty); + NormalizationResult isInhabited(const NormalizedType* norm); + + // -------- Convert back from a normalized type to a type + TypeId typeFromNormal(const NormalizedType& norm); + + std::optional intersectionOfTypePacks(TypePackId here, TypePackId there); + + // Move to private with LuauNormalizerStepwiseFuel + std::optional intersectionOfTypePacks_INTERNAL(TypePackId here, TypePackId there); + +private: // ------- Cached TypeIds TypeId unionType(TypeId here, TypeId there); TypeId intersectionType(TypeId here, TypeId there); const TypeIds* cacheTypeIds(TypeIds tys); - void clearCaches(); + void clearNormal(NormalizedType& norm); // ------- Normalizing unions void unionTysWithTy(TypeIds& here, TypeId there); @@ -387,7 +407,6 @@ class Normalizer void intersectExternTypes(NormalizedExternType& heres, const NormalizedExternType& theres); void intersectExternTypesWithExternType(NormalizedExternType& heres, TypeId there); void intersectStrings(NormalizedStringType& here, const NormalizedStringType& there); - std::optional intersectionOfTypePacks(TypePackId here, TypePackId there); std::optional intersectionOfTables(TypeId here, TypeId there, SeenTablePropPairs& seenTablePropPairs, Set& seenSet); void intersectTablesWithTable(TypeIds& heres, TypeId there, SeenTablePropPairs& seenTablePropPairs, Set& seenSetTypes); void intersectTables(TypeIds& heres, const TypeIds& theres); @@ -409,18 +428,20 @@ class Normalizer Set& seenSet ); - // Check for inhabitance - NormalizationResult isInhabited(TypeId ty); NormalizationResult isInhabited(TypeId ty, Set& seen); - NormalizationResult isInhabited(const NormalizedType* norm); NormalizationResult isInhabited(const NormalizedType* norm, Set& seen); // Check for intersections being inhabited - NormalizationResult isIntersectionInhabited(TypeId left, TypeId right); NormalizationResult isIntersectionInhabited(TypeId left, TypeId right, SeenTablePropPairs& seenTablePropPairs, Set& seenSet); - // -------- Convert back from a normalized type to a type - TypeId typeFromNormal(const NormalizedType& norm); + + // Fuel setup + + bool initializeFuel(); + void clearFuel(); + void consumeFuel(); + + friend struct FuelInitializer; }; } // namespace Luau diff --git a/Analysis/include/Luau/OverloadResolution.h b/Analysis/include/Luau/OverloadResolution.h index dbe4c06d..19954886 100644 --- a/Analysis/include/Luau/OverloadResolution.h +++ b/Analysis/include/Luau/OverloadResolution.h @@ -107,6 +107,18 @@ struct OverloadResolver std::optional failedSubTy, std::optional failedSuperTy ) const; + + // Checks if the candidate args are arity-compatible with the desired parameters. + // Used during overload selection to do arity-based filtering of overloads. + // We do not accept nil in place of a generic unless that generic is explicitly optional. + bool isArityCompatible(TypePackId candidate, TypePackId desired, NotNull builtinTypes) const; + + bool testFunctionTypeForOverloadSelection( + const FunctionType* ftv, + NotNull> uniqueTypes, + TypePackId argsPack, + bool useFreeTypeBounds + ); }; struct SolveResult diff --git a/Analysis/include/Luau/RecursionCounter.h b/Analysis/include/Luau/RecursionCounter.h index 962ecd02..fb07d7d7 100644 --- a/Analysis/include/Luau/RecursionCounter.h +++ b/Analysis/include/Luau/RecursionCounter.h @@ -3,6 +3,7 @@ #include "Luau/Common.h" #include "Luau/Error.h" +#include "Luau/NativeStackGuard.h" #include #include @@ -12,25 +13,18 @@ namespace Luau struct RecursionLimitException : public InternalCompilerError { - RecursionLimitException(const std::string system) - : InternalCompilerError("Internal recursion counter limit exceeded in " + system) - { - } + explicit RecursionLimitException(const std::string& system); }; struct RecursionCounter { - RecursionCounter(int* count) - : count(count) - { - ++(*count); - } - - ~RecursionCounter() - { - LUAU_ASSERT(*count > 0); - --(*count); - } + explicit RecursionCounter(int* count); + ~RecursionCounter(); + + RecursionCounter(const RecursionCounter&) = delete; + RecursionCounter& operator=(const RecursionCounter&) = delete; + RecursionCounter(RecursionCounter&&) = delete; + RecursionCounter& operator=(RecursionCounter&&) = delete; protected: int* count; @@ -38,14 +32,9 @@ struct RecursionCounter struct RecursionLimiter : RecursionCounter { - RecursionLimiter(const std::string system, int* count, int limit) - : RecursionCounter(count) - { - if (limit > 0 && *count > limit) - { - throw RecursionLimitException(system); - } - } + NativeStackGuard nativeStackGuard; + + RecursionLimiter(const std::string& system, int* count, int limit); }; } // namespace Luau diff --git a/Analysis/include/Luau/Simplify.h b/Analysis/include/Luau/Simplify.h index ebb14259..579cb835 100644 --- a/Analysis/include/Luau/Simplify.h +++ b/Analysis/include/Luau/Simplify.h @@ -5,6 +5,7 @@ #include "Luau/DenseHash.h" #include "Luau/NotNull.h" #include "Luau/TypeFwd.h" +#include "Luau/TypeIds.h" #include #include @@ -22,7 +23,11 @@ struct SimplifyResult }; SimplifyResult simplifyIntersection(NotNull builtinTypes, NotNull arena, TypeId left, TypeId right); -SimplifyResult simplifyIntersection(NotNull builtinTypes, NotNull arena, std::set parts); + +// Clip with LuauSimplifyIntersectionNoTreeSet +SimplifyResult simplifyIntersection_DEPRECATED(NotNull builtinTypes, NotNull arena, std::set parts); + +SimplifyResult simplifyIntersection(NotNull builtinTypes, NotNull arena, TypeIds parts); SimplifyResult simplifyUnion(NotNull builtinTypes, NotNull arena, TypeId left, TypeId right); diff --git a/Analysis/include/Luau/Subtyping.h b/Analysis/include/Luau/Subtyping.h index ca2e2334..3d348f12 100644 --- a/Analysis/include/Luau/Subtyping.h +++ b/Analysis/include/Luau/Subtyping.h @@ -59,8 +59,8 @@ inline const SubtypingReasoning kEmptyReasoning = SubtypingReasoning{TypePath::k * isn't strictly lexical (see test nested_generic_argument_type_packs), and that nested generic packs can shadow existing ones * such as with the type ((A...) -> (), (A...) -> ()) -> (), which should result in three independent bindings for A... . * To handle this, we maintain a stack of frames, each of which contains a mapping for the generic packs bound in that scope, as well as a pointers to - * its parent and child scopes. Inside each frame, we map the generic pack to an optional type pack, which is nullopt if we have not yet encountered a mapping - * for that generic pack in this scope. + * its parent and child scopes. Inside each frame, we map the generic pack to an optional type pack, which is nullopt if we have not yet encountered a + * mapping for that generic pack in this scope. */ struct MappedGenericEnvironment @@ -147,13 +147,6 @@ struct SubtypingEnvironment TypeIds upperBound; }; - // TODO: Clip with LuauSubtypingGenericsDoesntUseVariance - struct GenericBounds_DEPRECATED - { - DenseHashSet lowerBound{nullptr}; - DenseHashSet upperBound{nullptr}; - }; - /* For nested subtyping relationship tests of mapped generic bounds, we keep the outer environment immutable */ SubtypingEnvironment* parent = nullptr; @@ -165,21 +158,17 @@ struct SubtypingEnvironment TypeId ty, NotNull iceReporter ); - // TODO: Clip with LuauSubtypingGenericsDoesntUseVariance - std::optional applyMappedGenerics_DEPRECATED(NotNull builtinTypes, NotNull arena, TypeId ty); - const TypeId* tryFindSubstitution(TypeId ty) const; - // TODO: Clip with LuauSubtypingGenericsDoesntUseVariance + // TODO: Clip with LuauTryFindSubstitutionReturnOptional + const TypeId* tryFindSubstitution_DEPRECATED(TypeId ty) const; + std::optional tryFindSubstitution(TypeId ty) const; const SubtypingResult* tryFindSubtypingResult(std::pair subAndSuper) const; bool containsMappedType(TypeId ty) const; bool containsMappedPack(TypePackId tp) const; GenericBounds& getMappedTypeBounds(TypeId ty, NotNull iceReporter); - // TODO: Clip with LuauSubtypingGenericsDoesntUseVariance - GenericBounds_DEPRECATED& getMappedTypeBounds_DEPRECATED(TypeId ty); - // TODO: Clip with LuauSubtypingGenericPacksDoesntUseVariance - TypePackId* getMappedPackBounds_DEPRECATED(TypePackId tp); + MappedGenericEnvironment::LookupResult lookupGenericPack(TypePackId tp) const; /* * When we encounter a generic over the course of a subtyping test, we need @@ -188,12 +177,8 @@ struct SubtypingEnvironment * of each vector represents the current scope. */ DenseHashMap> mappedGenerics{nullptr}; - // TODO: Clip with LuauSubtypingGenericsDoesntUseVariance - DenseHashMap mappedGenerics_DEPRECATED{nullptr}; MappedGenericEnvironment mappedGenericPacks; - // TODO: Clip with LuauSubtypingGenericPacksDoesntUseVariance - DenseHashMap mappedGenericPacks_DEPRECATED{nullptr}; /* * See the test cyclic_tables_are_assumed_to_be_compatible_with_extern_types for @@ -203,8 +188,6 @@ struct SubtypingEnvironment */ DenseHashMap substitutions{nullptr}; - // TODO: Clip with LuauSubtypingGenericsDoesntUseVariance - DenseHashMap, SubtypingResult, TypePairHash> ephemeralCache{{}}; // We use this cache to track pairs of subtypes that we tried to subtype, and found them to be in the seen set at the time. // In those situations, we return True, but mark the result as not cacheable, because we don't want to cache broader results which @@ -234,7 +217,7 @@ struct Subtyping Contravariant }; - // TODO: Clip this along with LuauSubtypingGenericsDoesntUseVariance? + // TODO: Clip in CLI-170986 Variance variance = Variance::Covariant; using SeenSet = Set, TypePairHash>; @@ -269,7 +252,9 @@ struct Subtyping // TODO recursion limits SubtypingResult isSubtype(TypeId subTy, TypeId superTy, NotNull scope); - SubtypingResult isSubtype( + SubtypingResult isSubtype(TypePackId subTp, TypePackId superTp, NotNull scope, const std::vector& bindableGenerics); + // Clip with FFlagLuauPassBindableGenericsByReference + SubtypingResult isSubtype_DEPRECATED( TypePackId subTp, TypePackId superTp, NotNull scope, @@ -282,7 +267,6 @@ struct Subtyping SubtypingResult cache(SubtypingEnvironment& env, SubtypingResult res, TypeId subTy, TypeId superTy); SubtypingResult isCovariantWith(SubtypingEnvironment& env, TypeId subTy, TypeId superTy, NotNull scope); - SubtypingResult isCovariantWith(SubtypingEnvironment& env, TypePackId subTp, TypePackId superTp, NotNull scope); template SubtypingResult isContravariantWith(SubtypingEnvironment& env, SubTy&& subTy, SuperTy&& superTy, NotNull scope); @@ -321,7 +305,13 @@ struct Subtyping NotNull scope ); SubtypingResult isCovariantWith(SubtypingEnvironment& env, const TableType* subTable, const TableType* superTable, NotNull scope); - SubtypingResult isCovariantWith(SubtypingEnvironment& env, const TableType* subTable, const TableType* superTable, bool forceCovariantTest, NotNull scope); + SubtypingResult isCovariantWith( + SubtypingEnvironment& env, + const TableType* subTable, + const TableType* superTable, + bool forceCovariantTest, + NotNull scope + ); SubtypingResult isCovariantWith(SubtypingEnvironment& env, const MetatableType* subMt, const MetatableType* superMt, NotNull scope); SubtypingResult isCovariantWith(SubtypingEnvironment& env, const MetatableType* subMt, const TableType* superTable, NotNull scope); SubtypingResult isCovariantWith( @@ -420,9 +410,43 @@ struct Subtyping NotNull scope ); + // Pack subtyping + SubtypingResult isCovariantWith(SubtypingEnvironment& env, TypePackId subTp, TypePackId superTp, NotNull scope); + std::optional isSubTailCovariantWith( + SubtypingEnvironment& env, + std::vector& outputResults, + TypePackId subTp, + TypePackId subTail, + TypePackId superTp, + size_t superHeadStartIndex, + const std::vector& superHead, + std::optional superTail, + NotNull scope + ); + std::optional isCovariantWithSuperTail( + SubtypingEnvironment& env, + std::vector& outputResults, + TypePackId subTp, + size_t subHeadStartIndex, + const std::vector& subHead, + std::optional subTail, + TypePackId superTp, + TypePackId superTail, + NotNull scope + ); + + // Markers to help overload selection. + struct Anything{}; + struct Nothing{}; + + SubtypingResult isTailCovariantWithTail(SubtypingEnvironment& env, NotNull scope, TypePackId subTp, const VariadicTypePack* sub, TypePackId superTp, const VariadicTypePack* super); + SubtypingResult isTailCovariantWithTail(SubtypingEnvironment& env, NotNull scope, TypePackId subTp, const GenericTypePack* sub, TypePackId superTp, const GenericTypePack* super); + SubtypingResult isTailCovariantWithTail(SubtypingEnvironment& env, NotNull scope, TypePackId subTp, const VariadicTypePack* sub, TypePackId superTp, const GenericTypePack* super); + SubtypingResult isTailCovariantWithTail(SubtypingEnvironment& env, NotNull scope, TypePackId subTp, const GenericTypePack* sub, TypePackId superTp, const VariadicTypePack* super); + SubtypingResult isTailCovariantWithTail(SubtypingEnvironment& env, NotNull scope, TypePackId subTp, const GenericTypePack* sub, Nothing); + SubtypingResult isTailCovariantWithTail(SubtypingEnvironment& env, NotNull scope, Nothing, TypePackId superTp, const GenericTypePack* super); + bool bindGeneric(SubtypingEnvironment& env, TypeId subTp, TypeId superTp); - // Clip with LuauSubtypingGenericPacksDoesntUseVariance - bool bindGeneric_DEPRECATED(SubtypingEnvironment& env, TypePackId subTp, TypePackId superTp) const; template TypeId makeAggregateType(const Container& container, TypeId orElse); diff --git a/Analysis/include/Luau/TableLiteralInference.h b/Analysis/include/Luau/TableLiteralInference.h index 2da6301d..068c52fe 100644 --- a/Analysis/include/Luau/TableLiteralInference.h +++ b/Analysis/include/Luau/TableLiteralInference.h @@ -3,6 +3,7 @@ #pragma once #include "Luau/Ast.h" +#include "Luau/ConstraintSolver.h" #include "Luau/DenseHash.h" #include "Luau/NotNull.h" #include "Luau/TypeFwd.h" @@ -27,8 +28,8 @@ struct PushTypeResult PushTypeResult pushTypeInto( NotNull> astTypes, NotNull> astExpectedTypes, - NotNull builtinTypes, - NotNull arena, + NotNull solver, + NotNull constraint, NotNull unifier, NotNull subtyping, TypeId expectedType, diff --git a/Analysis/include/Luau/ToString.h b/Analysis/include/Luau/ToString.h index 53b62fbb..dceecca9 100644 --- a/Analysis/include/Luau/ToString.h +++ b/Analysis/include/Luau/ToString.h @@ -127,6 +127,10 @@ inline std::string toStringNamedFunction(const std::string& funcName, const Func return toStringNamedFunction(funcName, ftv, opts); } +// Converts the given number index into a human-readable string for that index to be used in errors. +// e.g. the index `0` becomes `1st`, `1` becomes `2nd`, `11` becomes `12th`, etc. +std::string toHumanReadableIndex(size_t number); + std::optional getFunctionNameAsString(const AstExpr& expr); // It could be useful to see the text representation of a type during a debugging session instead of exploring the content of the class diff --git a/Analysis/include/Luau/Type.h b/Analysis/include/Luau/Type.h index cc184c53..f0e7fafa 100644 --- a/Analysis/include/Luau/Type.h +++ b/Analysis/include/Luau/Type.h @@ -38,6 +38,7 @@ struct TypeFun; struct Constraint; struct Subtyping; struct TypeChecker2; +struct BuiltinTypeFunctions; enum struct SolverMode { @@ -1000,6 +1001,7 @@ struct BuiltinTypes bool debugFreezeArena = false; public: + std::unique_ptr typeFunctions; const TypeId nilType; const TypeId numberType; const TypeId stringType; diff --git a/Analysis/include/Luau/TypeChecker2.h b/Analysis/include/Luau/TypeChecker2.h index 663f262d..96cc18d5 100644 --- a/Analysis/include/Luau/TypeChecker2.h +++ b/Analysis/include/Luau/TypeChecker2.h @@ -200,6 +200,12 @@ struct TypeChecker2 bool testIsSubtype(TypeId subTy, TypeId superTy, Location location); bool testIsSubtype(TypePackId subTy, TypePackId superTy, Location location); + + void maybeReportSubtypingError(TypeId subTy, TypeId superTy, const Location& location); + // Tests whether subTy is a subtype of superTy in the context of a function iterator for a for-in statement. + // Includes some extra logic to help locate errors to the values and variables of the for-in statement. + void testIsSubtypeForInStat(TypeId iterFunc, TypeId prospectiveFunc, const AstStatForIn& forInStat); + void reportError(TypeError e); void reportErrors(ErrorVec errors); PropertyTypes lookupProp( diff --git a/Analysis/include/Luau/TypeFunction.h b/Analysis/include/Luau/TypeFunction.h index a96b800b..ffeb53dd 100644 --- a/Analysis/include/Luau/TypeFunction.h +++ b/Analysis/include/Luau/TypeFunction.h @@ -108,6 +108,10 @@ struct TypeFunctionReductionResult std::optional error; /// Messages printed out from user-defined type functions std::vector messages; + /// Some type function reduction rules may _create_ type functions (e.g. + /// the numeric type functions can "distribute" over an inner union). If + /// any type functions were created this way, we must add them here. + std::vector freshTypes; }; template diff --git a/Analysis/include/Luau/TypeIds.h b/Analysis/include/Luau/TypeIds.h index 87602d6f..9058f7b4 100644 --- a/Analysis/include/Luau/TypeIds.h +++ b/Analysis/include/Luau/TypeIds.h @@ -40,6 +40,8 @@ class TypeIds void retain(const TypeIds& tys); void clear(); + void clearWithoutRealloc(); + TypeId front() const; iterator begin(); iterator end(); diff --git a/Analysis/include/Luau/TypePack.h b/Analysis/include/Luau/TypePack.h index 5dcced9a..b3cc6f8d 100644 --- a/Analysis/include/Luau/TypePack.h +++ b/Analysis/include/Luau/TypePack.h @@ -231,11 +231,6 @@ bool isEmpty(TypePackId tp); /// Flattens out a type pack. Also returns a valid TypePackId tail if the type pack's full size is not known std::pair, std::optional> flatten(TypePackId tp); std::pair, std::optional> flatten(TypePackId tp, const TxnLog& log); -// TODO: Clip with LuauSubtypingGenericPacksDoesntUseVariance -std::pair, std::optional> flatten_DEPRECATED( - TypePackId tp, - const DenseHashMap& mappedGenericPacks -); /// Returs true if the type pack arose from a function that is declared to be variadic. /// Returns *false* for function argument packs that are inferred to be safe to oversaturate! @@ -267,7 +262,7 @@ LUAU_NOINLINE Unifiable::Bound* emplaceTypePack(TypeP TypePackId sliceTypePack( size_t sliceIndex, TypePackId toBeSliced, - std::vector& head, + const std::vector& head, std::optional tail, NotNull builtinTypes, NotNull arena diff --git a/Analysis/include/Luau/TypePath.h b/Analysis/include/Luau/TypePath.h index 1dc959d4..34a3cb82 100644 --- a/Analysis/include/Luau/TypePath.h +++ b/Analysis/include/Luau/TypePath.h @@ -241,64 +241,8 @@ std::string toString(const TypePath::Path& path, bool prefixDot = false); /// Converts a Path to a human readable string for error reporting. std::string toStringHuman(const TypePath::Path& path); -// To keep my head straight when clipping: -// LuauReturnMappedGenericPacksFromSubtyping2 expects mappedGenericPacks AND arena -// LuauSubtypingGenericPacksDoesntUseVariance expects just arena. this is the final state - -// TODO: clip below two along with `LuauReturnMappedGenericPacksFromSubtyping2` -std::optional traverse_DEPRECATED(TypeId root, const Path& path, NotNull builtinTypes); -std::optional traverse_DEPRECATED(TypePackId root, const Path& path, NotNull builtinTypes); -std::optional traverse( - TypePackId root, - const Path& path, - NotNull builtinTypes, - NotNull arena -); -// TODO: Clip with LuauSubtypingGenericPacksDoesntUseVariance -std::optional traverse_DEPRECATED( - TypePackId root, - const Path& path, - NotNull builtinTypes, - NotNull> mappedGenericPacks, - NotNull arena -); -std::optional traverse( - TypeId root, - const Path& path, - NotNull builtinTypes, - NotNull arena -); -// TODO: Clip with LuauSubtypingGenericPacksDoesntUseVariance -std::optional traverse_DEPRECATED( - TypeId root, - const Path& path, - NotNull builtinTypes, - NotNull> mappedGenericPacks, - NotNull arena -); - -/// Traverses a path from a type to its end point, which must be a type. This overload will fail if the path contains a PackSlice component or a -/// mapped generic pack. -/// @param root the entry point of the traversal -/// @param path the path to traverse -/// @param builtinTypes the built-in types in use (used to acquire the string metatable) -/// @returns the TypeId at the end of the path, or nullopt if the traversal failed. -std::optional traverseForType_DEPRECATED(TypeId root, const Path& path, NotNull builtinTypes); - -/// Traverses a path from a type to its end point, which must be a type. -/// @param root the entry point of the traversal -/// @param path the path to traverse -/// @param builtinTypes the built-in types in use (used to acquire the string metatable) -/// @param mappedGenericPacks the mapping for any encountered generic packs we want to reify -/// @param arena a TypeArena, required if path has a PackSlice component -/// @returns the TypeId at the end of the path, or nullopt if the traversal failed. -std::optional traverseForType_DEPRECATED( - TypeId root, - const Path& path, - NotNull builtinTypes, - NotNull> mappedGenericPacks, - NotNull arena -); +std::optional traverse(TypePackId root, const Path& path, NotNull builtinTypes, NotNull arena); +std::optional traverse(TypeId root, const Path& path, NotNull builtinTypes, NotNull arena); /// Traverses a path from a type to its end point, which must be a type. /// @param root the entry point of the traversal @@ -306,34 +250,7 @@ std::optional traverseForType_DEPRECATED( /// @param builtinTypes the built-in types in use (used to acquire the string metatable) /// @param arena a TypeArena, required if path has a PackSlice component /// @returns the TypeId at the end of the path, or nullopt if the traversal failed. -std::optional traverseForType( - TypeId root, - const Path& path, - NotNull builtinTypes, - NotNull arena -); - -/// Traverses a path from a type pack to its end point, which must be a type. -/// @param root the entry point of the traversal -/// @param path the path to traverse -/// @param builtinTypes the built-in types in use (used to acquire the string metatable) -/// @returns the TypeId at the end of the path, or nullopt if the traversal failed. -std::optional traverseForType_DEPRECATED(TypePackId root, const Path& path, NotNull builtinTypes); - -/// Traverses a path from a type pack to its end point, which must be a type. -/// @param root the entry point of the traversal -/// @param path the path to traverse -/// @param builtinTypes the built-in types in use (used to acquire the string metatable) -/// @param mappedGenericPacks the mapping for any encountered generic packs we want to reify -/// @param arena a TypeArena, required if path has a PackSlice component -/// @returns the TypeId at the end of the path, or nullopt if the traversal failed. -std::optional traverseForType_DEPRECATED( - TypePackId root, - const Path& path, - NotNull builtinTypes, - NotNull> mappedGenericPacks, - NotNull arena -); +std::optional traverseForType(TypeId root, const Path& path, NotNull builtinTypes, NotNull arena); /// Traverses a path from a type pack to its end point, which must be a type. /// @param root the entry point of the traversal @@ -341,35 +258,7 @@ std::optional traverseForType_DEPRECATED( /// @param builtinTypes the built-in types in use (used to acquire the string metatable) /// @param arena a TypeArena, required if path has a PackSlice component /// @returns the TypeId at the end of the path, or nullopt if the traversal failed. -std::optional traverseForType( - TypePackId root, - const Path& path, - NotNull builtinTypes, - NotNull arena -); - -/// Traverses a path from a type to its end point, which must be a type pack. This overload will fail if the path contains a PackSlice component or a -/// mapped generic pack. -/// @param root the entry point of the traversal -/// @param path the path to traverse -/// @param builtinTypes the built-in types in use (used to acquire the string metatable) -/// @returns the TypePackId at the end of the path, or nullopt if the traversal failed. -std::optional traverseForPack_DEPRECATED(TypeId root, const Path& path, NotNull builtinTypes); - -/// Traverses a path from a type to its end point, which must be a type pack. -/// @param root the entry point of the traversal -/// @param path the path to traverse -/// @param builtinTypes the built-in types in use (used to acquire the string metatable) -/// @param mappedGenericPacks the mapping for any encountered generic packs we want to reify -/// @param arena a TypeArena, required if path has a PackSlice component -/// @returns the TypePackId at the end of the path, or nullopt if the traversal failed. -std::optional traverseForPack_DEPRECATED( - TypeId root, - const Path& path, - NotNull builtinTypes, - NotNull> mappedGenericPacks, - NotNull arena -); +std::optional traverseForType(TypePackId root, const Path& path, NotNull builtinTypes, NotNull arena); /// Traverses a path from a type to its end point, which must be a type pack. /// @param root the entry point of the traversal @@ -377,34 +266,7 @@ std::optional traverseForPack_DEPRECATED( /// @param builtinTypes the built-in types in use (used to acquire the string metatable) /// @param arena a TypeArena, required if path has a PackSlice component /// @returns the TypePackId at the end of the path, or nullopt if the traversal failed. -std::optional traverseForPack( - TypeId root, - const Path& path, - NotNull builtinTypes, - NotNull arena -); - -/// Traverses a path from a type pack to its end point, which must be a type pack. -/// @param root the entry point of the traversal -/// @param path the path to traverse -/// @param builtinTypes the built-in types in use (used to acquire the string metatable) -/// @returns the TypePackId at the end of the path, or nullopt if the traversal failed. -std::optional traverseForPack_DEPRECATED(TypePackId root, const Path& path, NotNull builtinTypes); - -/// Traverses a path from a type pack to its end point, which must be a type pack. -/// @param root the entry point of the traversal -/// @param path the path to traverse -/// @param builtinTypes the built-in types in use (used to acquire the string metatable) -/// @param mappedGenericPacks the mapping for any encountered generic packs we want to reify -/// @param arena a TypeArena, required if path has a PackSlice component -/// @returns the TypePackId at the end of the path, or nullopt if the traversal failed. -std::optional traverseForPack_DEPRECATED( - TypePackId root, - const Path& path, - NotNull builtinTypes, - NotNull> mappedGenericPacks, - NotNull arena -); +std::optional traverseForPack(TypeId root, const Path& path, NotNull builtinTypes, NotNull arena); /// Traverses a path from a type pack to its end point, which must be a type pack. /// @param root the entry point of the traversal @@ -412,12 +274,7 @@ std::optional traverseForPack_DEPRECATED( /// @param builtinTypes the built-in types in use (used to acquire the string metatable) /// @param arena a TypeArena, required if path has a PackSlice component /// @returns the TypePackId at the end of the path, or nullopt if the traversal failed. -std::optional traverseForPack( - TypePackId root, - const Path& path, - NotNull builtinTypes, - NotNull arena -); +std::optional traverseForPack(TypePackId root, const Path& path, NotNull builtinTypes, NotNull arena); /// Traverses a path of Index and PackSlices to compute the index of the type the path points to /// Returns std::nullopt if the path isn't n PackSlice components followed by an Index component diff --git a/Analysis/include/Luau/TypeUtils.h b/Analysis/include/Luau/TypeUtils.h index 0e0a217b..82e35f03 100644 --- a/Analysis/include/Luau/TypeUtils.h +++ b/Analysis/include/Luau/TypeUtils.h @@ -6,6 +6,7 @@ #include "Luau/Type.h" #include "Luau/TypeIds.h" #include "Luau/TypePack.h" +#include "Luau/VisitType.h" #include #include @@ -336,6 +337,9 @@ bool isApproximatelyTruthyType(TypeId ty); // Unwraps any grouping expressions iteratively. AstExpr* unwrapGroup(AstExpr* expr); +// Returns true if ty is optional, ie if it is a supertype of nil +bool isOptionalType(TypeId ty, NotNull builtinTypes); + // These are magic types used in `TypeChecker2` and `NonStrictTypeChecker` // // `_luau_print` causes it's argument to be printed out, as in: @@ -385,5 +389,17 @@ struct IntersectionBuilder TypeId addIntersection(NotNull arena, NotNull builtinTypes, std::initializer_list list); TypeId addUnion(NotNull arena, NotNull builtinTypes, std::initializer_list list); +struct ContainsAnyGeneric final : public TypeOnceVisitor +{ + bool found = false; + + explicit ContainsAnyGeneric(); + + bool visit(TypeId ty) override; + bool visit(TypePackId ty) override; + + static bool hasAnyGeneric(TypeId ty); + static bool hasAnyGeneric(TypePackId tp); +}; } // namespace Luau diff --git a/Analysis/include/Luau/TypedAllocator.h b/Analysis/include/Luau/TypedAllocator.h index a5dd17b6..98f0cad7 100644 --- a/Analysis/include/Luau/TypedAllocator.h +++ b/Analysis/include/Luau/TypedAllocator.h @@ -49,7 +49,7 @@ class TypedAllocator T* block = stuff.back(); T* res = block + currentBlockSize; - new (res) T(std::forward(args...)); + new (res) T(std::forward(args)...); ++currentBlockSize; return res; } diff --git a/Analysis/include/Luau/VisitType.h b/Analysis/include/Luau/VisitType.h index c3534479..033847a0 100644 --- a/Analysis/include/Luau/VisitType.h +++ b/Analysis/include/Luau/VisitType.h @@ -336,7 +336,7 @@ struct GenericTypeVisitor { 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 diff --git a/Analysis/src/AstUtils.cpp b/Analysis/src/AstUtils.cpp index c72eab19..65488202 100644 --- a/Analysis/src/AstUtils.cpp +++ b/Analysis/src/AstUtils.cpp @@ -14,7 +14,8 @@ struct AstExprTableFinder : AstVisitor explicit AstExprTableFinder(NotNull> result, NotNull> astTypes) : result(result) , astTypes(astTypes) - {} + { + } bool visit(AstExpr* expr) override { @@ -38,8 +39,13 @@ void findUniqueTypes(NotNull> uniqueTypes, AstExpr* expr, N expr->visit(&finder); } -template -void findUniqueTypes(NotNull> uniqueTypes, Iter startIt, Iter endIt, NotNull> astTypes) +template +void findUniqueTypes( + NotNull> uniqueTypes, + Iter startIt, + Iter endIt, + NotNull> astTypes +) { while (startIt != endIt) { @@ -51,14 +57,22 @@ void findUniqueTypes(NotNull> uniqueTypes, Iter startIt, It } -void findUniqueTypes(NotNull> uniqueTypes, AstArray exprs, NotNull> astTypes) +void findUniqueTypes( + NotNull> uniqueTypes, + AstArray exprs, + NotNull> astTypes +) { findUniqueTypes(uniqueTypes, exprs.begin(), exprs.end(), astTypes); } -void findUniqueTypes(NotNull> uniqueTypes, const std::vector& exprs, NotNull> astTypes) +void findUniqueTypes( + NotNull> uniqueTypes, + const std::vector& exprs, + NotNull> astTypes +) { findUniqueTypes(uniqueTypes, exprs.begin(), exprs.end(), astTypes); } -} +} // namespace Luau diff --git a/Analysis/src/Autocomplete.cpp b/Analysis/src/Autocomplete.cpp index 544f19ff..13d9c644 100644 --- a/Analysis/src/Autocomplete.cpp +++ b/Analysis/src/Autocomplete.cpp @@ -10,7 +10,6 @@ #include "AutocompleteCore.h" LUAU_FASTFLAG(LuauSolverV2) -LUAU_FASTFLAG(LuauSuggestHotComments) namespace Luau { @@ -41,33 +40,17 @@ AutocompleteResult autocomplete(Frontend& frontend, const ModuleName& moduleName globalScope = frontend.globalsForAutocomplete.globalScope.get(); TypeArena typeArena; - if (FFlag::LuauSuggestHotComments) - { - bool isInHotComment = isWithinHotComment(*sourceModule, position); - if (isWithinComment(*sourceModule, position) && !isInHotComment) - return {}; - - std::vector ancestry = findAncestryAtPositionForAutocomplete(*sourceModule, position); - LUAU_ASSERT(!ancestry.empty()); - ScopePtr startScope = findScopeAtPosition(*module, position); - - return autocomplete_( - module, builtinTypes, &typeArena, ancestry, globalScope, startScope, position, frontend.fileResolver, std::move(callback), isInHotComment - ); - } - else - { - if (isWithinComment(*sourceModule, position)) - return {}; + bool isInHotComment = isWithinHotComment(*sourceModule, position); + if (isWithinComment(*sourceModule, position) && !isInHotComment) + return {}; - std::vector ancestry = findAncestryAtPositionForAutocomplete(*sourceModule, position); - LUAU_ASSERT(!ancestry.empty()); - ScopePtr startScope = findScopeAtPosition(*module, position); + std::vector ancestry = findAncestryAtPositionForAutocomplete(*sourceModule, position); + LUAU_ASSERT(!ancestry.empty()); + ScopePtr startScope = findScopeAtPosition(*module, position); - return autocomplete_( - module, builtinTypes, &typeArena, ancestry, globalScope, startScope, position, frontend.fileResolver, std::move(callback) - ); - } + return autocomplete_( + module, builtinTypes, &typeArena, ancestry, globalScope, startScope, position, frontend.fileResolver, std::move(callback), isInHotComment + ); } } // namespace Luau diff --git a/Analysis/src/AutocompleteCore.cpp b/Analysis/src/AutocompleteCore.cpp index 3498be24..d5661ac8 100644 --- a/Analysis/src/AutocompleteCore.cpp +++ b/Analysis/src/AutocompleteCore.cpp @@ -27,8 +27,6 @@ LUAU_FASTFLAG(LuauSolverV2) LUAU_FASTINT(LuauTypeInferIterationLimit) LUAU_FASTINT(LuauTypeInferRecursionLimit) LUAU_FASTFLAGVARIABLE(DebugLuauMagicVariableNames) -LUAU_FASTFLAGVARIABLE(LuauIncludeBreakContinueStatements) -LUAU_FASTFLAGVARIABLE(LuauSuggestHotComments) LUAU_FASTFLAG(LuauAutocompleteAttributes) static constexpr std::array kStatementStartingKeywords = @@ -1203,7 +1201,6 @@ static bool isBindingLegalAtCurrentPosition(const Symbol& symbol, const Binding& static bool isValidBreakContinueContext(const std::vector& ancestry, Position position) { - LUAU_ASSERT(FFlag::LuauIncludeBreakContinueStatements); for (auto it = ancestry.rbegin(); it != ancestry.rend(); ++it) { if ((*it)->is() || (*it)->is() || (*it)->is() || (*it)->is() || @@ -1267,18 +1264,10 @@ static AutocompleteEntryMap autocompleteStatement( scope = scope->parent; } - if (FFlag::LuauIncludeBreakContinueStatements) + bool shouldIncludeBreakAndContinue = isValidBreakContinueContext(ancestry, position); + for (const std::string_view kw : kStatementStartingKeywords) { - bool shouldIncludeBreakAndContinue = isValidBreakContinueContext(ancestry, position); - for (const std::string_view kw : kStatementStartingKeywords) - { - if ((kw != "break" && kw != "continue") || shouldIncludeBreakAndContinue) - result.emplace(kw, AutocompleteEntry{AutocompleteEntryKind::Keyword}); - } - } - else - { - for (const std::string_view kw : kStatementStartingKeywords) + if ((kw != "break" && kw != "continue") || shouldIncludeBreakAndContinue) result.emplace(kw, AutocompleteEntry{AutocompleteEntryKind::Keyword}); } @@ -1834,16 +1823,13 @@ AutocompleteResult autocomplete_( { LUAU_TIMETRACE_SCOPE("Luau::autocomplete_", "AutocompleteCore"); - if (FFlag::LuauSuggestHotComments) + if (isInHotComment) { - if (isInHotComment) - { - AutocompleteEntryMap result; + AutocompleteEntryMap result; - for (const std::string_view hc : kHotComments) - result.emplace(hc, AutocompleteEntry{AutocompleteEntryKind::HotComment}); - return {std::move(result), ancestry, AutocompleteContext::HotComment}; - } + for (const std::string_view hc : kHotComments) + result.emplace(hc, AutocompleteEntry{AutocompleteEntryKind::HotComment}); + return {std::move(result), ancestry, AutocompleteContext::HotComment}; } AstNode* node = ancestry.back(); diff --git a/Analysis/src/BuiltinDefinitions.cpp b/Analysis/src/BuiltinDefinitions.cpp index 008a5ead..7376e3cb 100644 --- a/Analysis/src/BuiltinDefinitions.cpp +++ b/Analysis/src/BuiltinDefinitions.cpp @@ -35,6 +35,7 @@ LUAU_FASTFLAG(LuauSolverV2) LUAU_FASTFLAGVARIABLE(LuauTableCloneClonesType3) LUAU_FASTFLAG(LuauUseWorkspacePropToChooseSolver) LUAU_FASTFLAG(LuauEmplaceNotPushBack) +LUAU_FASTFLAG(LuauBuiltinTypeFunctionsArentGlobal) namespace Luau { @@ -368,8 +369,17 @@ void registerBuiltinGlobals(Frontend& frontend, GlobalTypes& globals, bool typeC NotNull builtinTypes = globals.builtinTypes; NotNull globalScope{globals.globalScope.get()}; - if (frontend.getLuauSolverMode() == SolverMode::New) - builtinTypeFunctions().addToScope(NotNull{&arena}, NotNull{globals.globalScope.get()}); + if (FFlag::LuauBuiltinTypeFunctionsArentGlobal) + { + if (frontend.getLuauSolverMode() == SolverMode::New) + builtinTypes->typeFunctions->addToScope(NotNull{&arena}, NotNull{globals.globalScope.get()}); + } + else + { + if (frontend.getLuauSolverMode() == SolverMode::New) + builtinTypeFunctions_DEPRECATED().addToScope(NotNull{&arena}, NotNull{globals.globalScope.get()}); + } + LoadDefinitionFileResult loadResult = frontend.loadDefinitionFile( globals, globals.globalScope, getBuiltinDefinitionSource(), "@luau", /* captureComments */ false, typeCheckForAutocomplete @@ -438,7 +448,13 @@ void registerBuiltinGlobals(Frontend& frontend, GlobalTypes& globals, bool typeC if (frontend.getLuauSolverMode() == SolverMode::New) { // getmetatable : (T) -> getmetatable - TypeId getmtReturn = arena.addType(TypeFunctionInstanceType{builtinTypeFunctions().getmetatableFunc, {genericT}}); + TypeId getmtReturn = arena.addType( + TypeFunctionInstanceType{ + FFlag::LuauBuiltinTypeFunctionsArentGlobal ? builtinTypes->typeFunctions->getmetatableFunc + : builtinTypeFunctions_DEPRECATED().getmetatableFunc, + {genericT} + } + ); addGlobalBinding(globals, "getmetatable", makeFunction(arena, std::nullopt, {genericT}, {}, {genericT}, {getmtReturn}), "@luau"); } else @@ -450,7 +466,13 @@ void registerBuiltinGlobals(Frontend& frontend, GlobalTypes& globals, bool typeC if (frontend.getLuauSolverMode() == SolverMode::New) { // setmetatable(T, MT) -> setmetatable - TypeId setmtReturn = arena.addType(TypeFunctionInstanceType{builtinTypeFunctions().setmetatableFunc, {genericT, genericMT}}); + TypeId setmtReturn = arena.addType( + TypeFunctionInstanceType{ + FFlag::LuauBuiltinTypeFunctionsArentGlobal ? builtinTypes->typeFunctions->setmetatableFunc + : builtinTypeFunctions_DEPRECATED().setmetatableFunc, + {genericT, genericMT} + } + ); addGlobalBinding( globals, "setmetatable", makeFunction(arena, std::nullopt, {genericT, genericMT}, {}, {genericT, genericMT}, {setmtReturn}), "@luau" ); @@ -482,7 +504,12 @@ void registerBuiltinGlobals(Frontend& frontend, GlobalTypes& globals, bool typeC TypeId genericT = arena.addType(GenericType{globalScope, "T"}); TypeId refinedTy = arena.addType( TypeFunctionInstanceType{ - NotNull{&builtinTypeFunctions().intersectFunc}, {genericT, arena.addType(NegationType{builtinTypes->falsyType})}, {} + NotNull{ + FFlag::LuauBuiltinTypeFunctionsArentGlobal ? &builtinTypes->typeFunctions->intersectFunc + : &builtinTypeFunctions_DEPRECATED().intersectFunc + }, + {genericT, arena.addType(NegationType{builtinTypes->falsyType})}, + {} } ); @@ -1853,6 +1880,18 @@ bool matchAssert(const AstExprCall& call) return true; } +bool matchTypeOf(const AstExprCall& call) +{ + if (call.args.size != 1) + return false; + + const AstExprGlobal* funcAsGlobal = call.func->as(); + if (!funcAsGlobal || (funcAsGlobal->name != "typeof" && funcAsGlobal->name != "type")) + return false; + + return true; +} + bool shouldTypestateForFirstArgument(const AstExprCall& call) { // TODO: magic function for setmetatable and assert and then add them diff --git a/Analysis/src/BuiltinTypeFunctions.cpp b/Analysis/src/BuiltinTypeFunctions.cpp index 1295db89..33424a17 100644 --- a/Analysis/src/BuiltinTypeFunctions.cpp +++ b/Analysis/src/BuiltinTypeFunctions.cpp @@ -20,15 +20,16 @@ LUAU_FASTFLAG(DebugLuauEqSatSimplification) LUAU_DYNAMIC_FASTINT(LuauTypeFamilyApplicationCartesianProductLimit) LUAU_DYNAMIC_FASTINTVARIABLE(LuauStepRefineRecursionLimit, 64) -LUAU_FASTFLAGVARIABLE(LuauRefineOccursCheckDirectRecursion) LUAU_FASTFLAG(LuauReduceSetTypeStackPressure) LUAU_FASTFLAGVARIABLE(LuauRefineNoRefineAlways) LUAU_FASTFLAGVARIABLE(LuauRefineDistributesOverUnions) LUAU_FASTFLAG(LuauEGFixGenericsList) LUAU_FASTFLAG(LuauExplicitSkipBoundTypes) -LUAU_FASTFLAG(LuauRawGetHandlesNil) LUAU_FASTFLAG(LuauNoMoreComparisonTypeFunctions) +LUAU_FASTFLAGVARIABLE(LuauBuiltinTypeFunctionsArentGlobal) +LUAU_FASTFLAG(LuauPassBindableGenericsByReference) +LUAU_FASTFLAG(LuauEnqueueUnionsOfDistributedTypeFunctions) namespace Luau { @@ -110,7 +111,10 @@ std::optional> tryDistributeTypeFunctionApp( TypeId resultTy = ctx->arena->addType( TypeFunctionInstanceType{ - NotNull{&builtinTypeFunctions().unionFunc}, + NotNull{ + FFlag::LuauBuiltinTypeFunctionsArentGlobal ? &ctx->builtins->typeFunctions->unionFunc + : &builtinTypeFunctions_DEPRECATED().unionFunc + }, std::move(results), {}, } @@ -119,7 +123,10 @@ std::optional> tryDistributeTypeFunctionApp( if (ctx->solver) ctx->pushConstraint(ReduceConstraint{resultTy}); - return {{resultTy, Reduction::MaybeOk, {}, {}}}; + if (FFlag::LuauEnqueueUnionsOfDistributedTypeFunctions) + return {{resultTy, Reduction::MaybeOk, {}, {}, {}, {}, {resultTy}}}; + else + return {{resultTy, Reduction::MaybeOk, {}, {}}}; } return std::nullopt; @@ -238,7 +245,12 @@ TypeFunctionReductionResult lenTypeFunction( return {std::nullopt, Reduction::Erroneous, {}, {}}; // occurs check failed Subtyping subtyping{ctx->builtins, ctx->arena, ctx->simplifier, ctx->normalizer, ctx->typeFunctionRuntime, ctx->ice}; - if (!subtyping.isSubtype(inferredArgPack, instantiatedMmFtv->argTypes, ctx->scope).isSubtype) // TODO: is this the right variance? + if (FFlag::LuauPassBindableGenericsByReference) + { + if (!subtyping.isSubtype(inferredArgPack, instantiatedMmFtv->argTypes, ctx->scope, {}).isSubtype) + return {std::nullopt, Reduction::Erroneous, {}, {}}; + } + else if (!subtyping.isSubtype_DEPRECATED(inferredArgPack, instantiatedMmFtv->argTypes, ctx->scope).isSubtype) // TODO: is this the right variance? return {std::nullopt, Reduction::Erroneous, {}, {}}; // `len` must return a `number`. @@ -322,7 +334,13 @@ TypeFunctionReductionResult unmTypeFunction( if (!FFlag::LuauEGFixGenericsList) { Subtyping subtyping{ctx->builtins, ctx->arena, ctx->simplifier, ctx->normalizer, ctx->typeFunctionRuntime, ctx->ice}; - if (!subtyping.isSubtype(inferredArgPack, instantiatedMmFtv->argTypes, ctx->scope).isSubtype) // TODO: is this the right variance? + if (FFlag::LuauPassBindableGenericsByReference) + { + if (!subtyping.isSubtype(inferredArgPack, instantiatedMmFtv->argTypes, ctx->scope, {}).isSubtype) + return {std::nullopt, Reduction::Erroneous, {}, {}}; + } + else if (!subtyping.isSubtype_DEPRECATED(inferredArgPack, instantiatedMmFtv->argTypes, ctx->scope) + .isSubtype) // TODO: is this the right variance? return {std::nullopt, Reduction::Erroneous, {}, {}}; } @@ -679,7 +697,12 @@ TypeFunctionReductionResult concatTypeFunction( return {std::nullopt, Reduction::Erroneous, {}, {}}; // occurs check failed Subtyping subtyping{ctx->builtins, ctx->arena, ctx->simplifier, ctx->normalizer, ctx->typeFunctionRuntime, ctx->ice}; - if (!subtyping.isSubtype(inferredArgPack, instantiatedMmFtv->argTypes, ctx->scope).isSubtype) // TODO: is this the right variance? + if (FFlag::LuauPassBindableGenericsByReference) + { + if (!subtyping.isSubtype(inferredArgPack, instantiatedMmFtv->argTypes, ctx->scope, {}).isSubtype) + return {std::nullopt, Reduction::Erroneous, {}, {}}; + } + else if (!subtyping.isSubtype_DEPRECATED(inferredArgPack, instantiatedMmFtv->argTypes, ctx->scope).isSubtype) // TODO: is this the right variance? return {std::nullopt, Reduction::Erroneous, {}, {}}; return {ctx->builtins->stringType, Reduction::MaybeOk, {}, {}}; @@ -888,7 +911,12 @@ static TypeFunctionReductionResult comparisonTypeFunction( return {std::nullopt, Reduction::Erroneous, {}, {}}; // occurs check failed Subtyping subtyping{ctx->builtins, ctx->arena, ctx->simplifier, ctx->normalizer, ctx->typeFunctionRuntime, ctx->ice}; - if (!subtyping.isSubtype(inferredArgPack, instantiatedMmFtv->argTypes, ctx->scope).isSubtype) // TODO: is this the right variance? + if (FFlag::LuauPassBindableGenericsByReference) + { + if (!subtyping.isSubtype(inferredArgPack, instantiatedMmFtv->argTypes, ctx->scope, {}).isSubtype) + return {std::nullopt, Reduction::Erroneous, {}, {}}; + } + else if (!subtyping.isSubtype_DEPRECATED(inferredArgPack, instantiatedMmFtv->argTypes, ctx->scope).isSubtype) // TODO: is this the right variance? return {std::nullopt, Reduction::Erroneous, {}, {}}; return {ctx->builtins->booleanType, Reduction::MaybeOk, {}, {}}; @@ -1017,7 +1045,12 @@ TypeFunctionReductionResult eqTypeFunction( return {std::nullopt, Reduction::Erroneous, {}, {}}; // occurs check failed Subtyping subtyping{ctx->builtins, ctx->arena, ctx->simplifier, ctx->normalizer, ctx->typeFunctionRuntime, ctx->ice}; - if (!subtyping.isSubtype(inferredArgPack, instantiatedMmFtv->argTypes, ctx->scope).isSubtype) // TODO: is this the right variance? + if (FFlag::LuauPassBindableGenericsByReference) + { + if (!subtyping.isSubtype(inferredArgPack, instantiatedMmFtv->argTypes, ctx->scope, {}).isSubtype) + return {std::nullopt, Reduction::Erroneous, {}, {}}; + } + else if (!subtyping.isSubtype_DEPRECATED(inferredArgPack, instantiatedMmFtv->argTypes, ctx->scope).isSubtype) // TODO: is this the right variance? return {std::nullopt, Reduction::Erroneous, {}, {}}; return {ctx->builtins->booleanType, Reduction::MaybeOk, {}, {}}; @@ -1153,7 +1186,7 @@ struct RefineTypeScrubber : public Substitution return true; } } - return FFlag::LuauRefineOccursCheckDirectRecursion ? ty == needle : false; + return ty == needle; } bool ignoreChildren(TypeId ty) override @@ -1195,7 +1228,7 @@ struct RefineTypeScrubber : public Substitution else return ctx->arena->addType(IntersectionType{newParts.take()}); } - else if (FFlag::LuauRefineOccursCheckDirectRecursion && ty == needle) + else if (ty == needle) return ctx->builtins->unknownType; else return ty; @@ -1575,11 +1608,23 @@ struct CollectUnionTypeOptions : TypeOnceVisitor bool visit(TypeId ty, const TypeFunctionInstanceType& tfit) override { - if (tfit.function->name != builtinTypeFunctions().unionFunc.name) + if (FFlag::LuauBuiltinTypeFunctionsArentGlobal) { - options.insert(ty); - blockingTypes.insert(ty); - return false; + if (tfit.function->name != ctx->builtins->typeFunctions->unionFunc.name) + { + options.insert(ty); + blockingTypes.insert(ty); + return false; + } + } + else + { + if (tfit.function->name != builtinTypeFunctions_DEPRECATED().unionFunc.name) + { + options.insert(ty); + blockingTypes.insert(ty); + return false; + } } return true; } @@ -2031,8 +2076,16 @@ bool searchPropsAndIndexer( if (auto tfit = get(indexType)) { // if we have an index function here, it means we're in a cycle, so let's see if it's well-founded if we tie the knot - if (tfit->function.get() == &builtinTypeFunctions().indexFunc) - indexType = follow(tblIndexer->indexResultType); + if (FFlag::LuauBuiltinTypeFunctionsArentGlobal) + { + if (tfit->function.get() == &ctx->builtins->typeFunctions->indexFunc) + indexType = follow(tblIndexer->indexResultType); + } + else + { + if (tfit->function.get() == &builtinTypeFunctions_DEPRECATED().indexFunc) + indexType = follow(tblIndexer->indexResultType); + } } if (isSubtype(ty, indexType, ctx->scope, ctx->builtins, ctx->simplifier, *ctx->ice, SolverMode::New)) @@ -2276,7 +2329,7 @@ TypeFunctionReductionResult indexFunctionImpl( for (TypeId ty : *typesToFind) if (!tblIndexInto(ty, *tablesIter, properties, ctx, isRaw)) { - if (FFlag::LuauRawGetHandlesNil && isRaw) + if (isRaw) properties.insert(ctx->builtins->nilType); else return {std::nullopt, Reduction::Erroneous, {}, {}}; @@ -2419,7 +2472,7 @@ static TypeFunctionReductionResult getmetatableHelper(TypeId targetTy, c std::optional result = std::nullopt; bool erroneous = true; - if (auto table = get(targetTy)) + if (get(targetTy)) erroneous = false; if (auto mt = get(targetTy)) @@ -2677,7 +2730,8 @@ void BuiltinTypeFunctions::addToScope(NotNull arena, NotNull s scope->exportedTypeBindings[getmetatableFunc.name] = mkUnaryTypeFunction(&getmetatableFunc); } -const BuiltinTypeFunctions& builtinTypeFunctions() + +const BuiltinTypeFunctions& builtinTypeFunctions_DEPRECATED() { static std::unique_ptr result = std::make_unique(); diff --git a/Analysis/src/Constraint.cpp b/Analysis/src/Constraint.cpp index c95dcea2..4376d18b 100644 --- a/Analysis/src/Constraint.cpp +++ b/Analysis/src/Constraint.cpp @@ -5,6 +5,7 @@ #include "Luau/VisitType.h" LUAU_FASTFLAGVARIABLE(LuauExplicitSkipBoundTypes) +LUAU_FASTFLAG(LuauNoOrderingTypeFunctions) namespace Luau { @@ -87,7 +88,8 @@ TypeIds Constraint::getMaybeMutatedFreeTypes() const if (auto ec = get(*this)) { rci.traverse(ec->resultType); - // `EqualityConstraints` should not mutate `assignmentType`. + if (FFlag::LuauNoOrderingTypeFunctions) + rci.traverse(ec->assignmentType); } else if (auto sc = get(*this)) { diff --git a/Analysis/src/ConstraintGenerator.cpp b/Analysis/src/ConstraintGenerator.cpp index 51e09834..aff1851f 100644 --- a/Analysis/src/ConstraintGenerator.cpp +++ b/Analysis/src/ConstraintGenerator.cpp @@ -31,27 +31,31 @@ #include #include +LUAU_FASTFLAG(LuauIndividualRecursionLimits) +LUAU_DYNAMIC_FASTINTVARIABLE(LuauConstraintGeneratorRecursionLimit, 300) + LUAU_FASTINT(LuauCheckRecursionLimit) LUAU_FASTFLAG(DebugLuauLogSolverToJson) LUAU_FASTFLAG(DebugLuauMagicTypes) -LUAU_FASTFLAGVARIABLE(LuauEnableWriteOnlyProperties) LUAU_FASTINTVARIABLE(LuauPrimitiveInferenceInTableLimit, 500) -LUAU_FASTFLAGVARIABLE(LuauInferActualIfElseExprType2) -LUAU_FASTFLAGVARIABLE(LuauTypeFunNoScopeMapRef) -LUAU_FASTFLAG(LuauLimitDynamicConstraintSolving3) LUAU_FASTFLAG(LuauEmplaceNotPushBack) LUAU_FASTFLAG(LuauReduceSetTypeStackPressure) -LUAU_FASTFLAG(LuauParametrizedAttributeSyntax) LUAU_FASTFLAG(LuauExplicitSkipBoundTypes) LUAU_FASTFLAG(DebugLuauStringSingletonBasedOnQuotes) LUAU_FASTFLAGVARIABLE(LuauInstantiateResolvedTypeFunctions) -LUAU_FASTFLAGVARIABLE(LuauPushTypeConstraint) +LUAU_FASTFLAGVARIABLE(LuauPushTypeConstraint2) LUAU_FASTFLAGVARIABLE(LuauEGFixGenericsList) LUAU_FASTFLAGVARIABLE(LuauNumericUnaryOpsDontProduceNegationRefinements) LUAU_FASTFLAGVARIABLE(LuauInitializeDefaultGenericParamsAtProgramPoint) LUAU_FASTFLAGVARIABLE(LuauNoConstraintGenRecursionLimitIce) LUAU_FASTFLAGVARIABLE(LuauCacheDuplicateHasPropConstraints) LUAU_FASTFLAGVARIABLE(LuauNoMoreComparisonTypeFunctions) +LUAU_FASTFLAG(LuauNoOrderingTypeFunctions) +LUAU_FASTFLAGVARIABLE(LuauDontReferenceScopePtrFromHashTable) +LUAU_FASTFLAG(LuauBuiltinTypeFunctionsArentGlobal) +LUAU_FASTFLAGVARIABLE(LuauMetatableAvoidSingletonUnion) +LUAU_FASTFLAGVARIABLE(LuauAddRefinementToAssertions) +LUAU_FASTFLAG(LuauPushTypeConstraintLambdas) namespace Luau { @@ -567,7 +571,14 @@ void ConstraintGenerator::computeRefinement( } if (eq) - discriminantTy = createTypeFunctionInstance(builtinTypeFunctions().singletonFunc, {discriminantTy}, {}, scope, location); + discriminantTy = createTypeFunctionInstance( + FFlag::LuauBuiltinTypeFunctionsArentGlobal ? builtinTypes->typeFunctions->singletonFunc + : builtinTypeFunctions_DEPRECATED().singletonFunc, + {discriminantTy}, + {}, + scope, + location + ); for (const RefinementKey* key = proposition->key; key; key = key->parent) { @@ -681,12 +692,27 @@ void ConstraintGenerator::applyRefinements(const ScopePtr& scope, Location locat return ty; } std::vector args = {ty}; - const TypeFunction& func = kind == RefinementsOpKind::Intersect ? builtinTypeFunctions().intersectFunc : builtinTypeFunctions().refineFunc; - LUAU_ASSERT(!func.name.empty()); - args.insert(args.end(), discriminants.begin(), discriminants.end()); - TypeId resultType = createTypeFunctionInstance(func, std::move(args), {}, scope, location); - discriminants.clear(); - return resultType; + if (FFlag::LuauBuiltinTypeFunctionsArentGlobal) + { + const TypeFunction& func = + kind == RefinementsOpKind::Intersect ? builtinTypes->typeFunctions->intersectFunc : builtinTypes->typeFunctions->refineFunc; + LUAU_ASSERT(!func.name.empty()); + args.insert(args.end(), discriminants.begin(), discriminants.end()); + TypeId resultType = createTypeFunctionInstance(func, std::move(args), {}, scope, location); + discriminants.clear(); + return resultType; + } + else + { + const TypeFunction& func = + kind == RefinementsOpKind::Intersect ? builtinTypeFunctions_DEPRECATED().intersectFunc : builtinTypeFunctions_DEPRECATED().refineFunc; + LUAU_ASSERT(!func.name.empty()); + args.insert(args.end(), discriminants.begin(), discriminants.end()); + TypeId resultType = createTypeFunctionInstance(func, std::move(args), {}, scope, location); + discriminants.clear(); + return resultType; + } + }; for (auto& [def, partition] : refinements) @@ -741,7 +767,14 @@ void ConstraintGenerator::applyRefinements(const ScopePtr& scope, Location locat ty = flushConstraints(kind, ty, discriminants); if (partition.shouldAppendNilType) - ty = createTypeFunctionInstance(builtinTypeFunctions().weakoptionalFunc, {ty}, {}, scope, location); + ty = createTypeFunctionInstance( + FFlag::LuauBuiltinTypeFunctionsArentGlobal ? builtinTypes->typeFunctions->weakoptionalFunc + : builtinTypeFunctions_DEPRECATED().weakoptionalFunc, + {ty}, + {}, + scope, + location + ); updateRValueRefinements(scope, def, ty); } } @@ -853,7 +886,16 @@ void ConstraintGenerator::checkAliases(const ScopePtr& scope, AstStatBlock* bloc udtfData.definition = function; TypeId typeFunctionTy = arena->addType( - TypeFunctionInstanceType{NotNull{&builtinTypeFunctions().userFunc}, std::move(typeParams), {}, function->name, udtfData} + TypeFunctionInstanceType{ + NotNull{ + FFlag::LuauBuiltinTypeFunctionsArentGlobal ? &builtinTypes->typeFunctions->userFunc + : &builtinTypeFunctions_DEPRECATED().userFunc + }, + std::move(typeParams), + {}, + function->name, + udtfData + } ); TypeFun typeFunction{std::move(quantifiedTypeParams), typeFunctionTy}; @@ -985,10 +1027,21 @@ ControlFlow ConstraintGenerator::visitBlockWithoutChildScope(const ScopePtr& sco { RecursionCounter counter{&recursionCount}; - if (recursionCount >= FInt::LuauCheckRecursionLimit) + if (FFlag::LuauIndividualRecursionLimits) { - reportCodeTooComplex(block->location); - return ControlFlow::None; + if (recursionCount >= DFInt::LuauConstraintGeneratorRecursionLimit) + { + reportCodeTooComplex(block->location); + return ControlFlow::None; + } + } + else + { + if (recursionCount >= FInt::LuauCheckRecursionLimit) + { + reportCodeTooComplex(block->location); + return ControlFlow::None; + } } checkAliases(scope, block); @@ -1008,16 +1061,30 @@ ControlFlow ConstraintGenerator::visit(const ScopePtr& scope, AstStat* stat) { std::optional counter; std::optional limiter; + if (FFlag::LuauNoConstraintGenRecursionLimitIce) { counter.emplace(&recursionCount); - if (recursionCount >= FInt::LuauCheckRecursionLimit) + if (FFlag::LuauIndividualRecursionLimits) { - reportCodeTooComplex(stat->location); - return ControlFlow::None; + if (recursionCount >= DFInt::LuauConstraintGeneratorRecursionLimit) + { + reportCodeTooComplex(stat->location); + return ControlFlow::None; + } + } + else + { + if (recursionCount >= FInt::LuauCheckRecursionLimit) + { + reportCodeTooComplex(stat->location); + return ControlFlow::None; + } } } + else if (FFlag::LuauIndividualRecursionLimits) + limiter.emplace("ConstraintGenerator", &recursionCount, DFInt::LuauConstraintGeneratorRecursionLimit); else limiter.emplace("ConstraintGenerator", &recursionCount, FInt::LuauCheckRecursionLimit); @@ -1320,8 +1387,13 @@ ControlFlow ConstraintGenerator::visit(const ScopePtr& scope, AstStatForIn* forI const DefId keyDef = dfg->getDef(keyVar); const TypeId loopVar = loopScope->lvalueTypes[keyDef]; - const TypeId intersectionTy = - createTypeFunctionInstance(builtinTypeFunctions().intersectFunc, {loopVar, builtinTypes->notNilType}, {}, loopScope, keyVar->location); + const TypeId intersectionTy = createTypeFunctionInstance( + FFlag::LuauBuiltinTypeFunctionsArentGlobal ? builtinTypes->typeFunctions->intersectFunc : builtinTypeFunctions_DEPRECATED().intersectFunc, + {loopVar, builtinTypes->notNilType}, + {}, + loopScope, + keyVar->location + ); loopScope->bindings[keyVar] = Binding{intersectionTy, keyVar->location}; loopScope->lvalueTypes[keyDef] = intersectionTy; @@ -1387,18 +1459,11 @@ static void propagateDeprecatedAttributeToConstraint(ConstraintV& c, const AstEx { if (GeneralizationConstraint* genConstraint = c.get_if()) { - if (FFlag::LuauParametrizedAttributeSyntax) + AstAttr* deprecatedAttribute = func->getAttribute(AstAttr::Type::Deprecated); + genConstraint->hasDeprecatedAttribute = deprecatedAttribute != nullptr; + if (deprecatedAttribute) { - AstAttr* deprecatedAttribute = func->getAttribute(AstAttr::Type::Deprecated); - genConstraint->hasDeprecatedAttribute = deprecatedAttribute != nullptr; - if (deprecatedAttribute) - { - genConstraint->deprecatedInfo = deprecatedAttribute->deprecatedInfo(); - } - } - else - { - genConstraint->hasDeprecatedAttribute = func->hasAttribute(AstAttr::Type::Deprecated); + genConstraint->deprecatedInfo = deprecatedAttribute->deprecatedInfo(); } } } @@ -1491,7 +1556,7 @@ ControlFlow ConstraintGenerator::visit(const ScopePtr& scope, AstStatFunction* f sig.bodyScope->lvalueTypes[def] = sig.signature; updateRValueRefinements(sig.bodyScope, def, sig.signature); } - else if (AstExprIndexName* indexName = function->name->as()) + else if (function->name->is()) { updateRValueRefinements(sig.bodyScope, def, sig.signature); } @@ -1601,7 +1666,7 @@ ControlFlow ConstraintGenerator::visit(const ScopePtr& scope, AstStatFunction* f { visitLValue(scope, indexName, generalizedType); } - else if (AstExprError* err = function->name->as()) + else if (function->name->is()) { generalizedType = builtinTypes->errorType; } @@ -1796,7 +1861,7 @@ ControlFlow ConstraintGenerator::visit(const ScopePtr& scope, AstStatTypeAlias* scope->typeAliasLocations[alias->name.value] = alias->location; scope->typeAliasNameLocations[alias->name.value] = alias->nameLocation; - ScopePtr* defnScope = astTypeAliasDefiningScopes.find(alias); + ScopePtr* defnScopePtr = astTypeAliasDefiningScopes.find(alias); std::unordered_map* typeBindings; if (alias->exported) @@ -1807,13 +1872,20 @@ ControlFlow ConstraintGenerator::visit(const ScopePtr& scope, AstStatTypeAlias* // These will be undefined if the alias was a duplicate definition, in which // case we just skip over it. auto bindingIt = typeBindings->find(alias->name.value); - if (bindingIt == typeBindings->end() || defnScope == nullptr) + if (bindingIt == typeBindings->end() || defnScopePtr == nullptr) return ControlFlow::None; + ScopePtr defnScope = *defnScopePtr; + if (FFlag::LuauInitializeDefaultGenericParamsAtProgramPoint) - resolveGenericDefaultParameters(*defnScope, alias, bindingIt->second); + resolveGenericDefaultParameters(FFlag::LuauDontReferenceScopePtrFromHashTable ? defnScope : *defnScopePtr, alias, bindingIt->second); - TypeId ty = resolveType(*defnScope, alias->type, /* inTypeArguments */ false, /* replaceErrorWithFresh */ false); + TypeId ty = resolveType( + FFlag::LuauDontReferenceScopePtrFromHashTable ? defnScope : *defnScopePtr, + alias->type, + /* inTypeArguments */ false, + /* replaceErrorWithFresh */ false + ); TypeId aliasTy = bindingIt->second.type; LUAU_ASSERT(get(aliasTy)); @@ -1826,12 +1898,28 @@ ControlFlow ConstraintGenerator::visit(const ScopePtr& scope, AstStatTypeAlias* emplaceType(asMutable(aliasTy), ty); std::vector typeParams; - for (auto tyParam : createGenerics(*defnScope, alias->generics, /* useCache */ true, /* addTypes */ false)) - typeParams.push_back(tyParam.second.ty); + if (FFlag::LuauDontReferenceScopePtrFromHashTable) + { + for (const auto& tyParam : createGenerics(defnScope, alias->generics, /* useCache */ true, /* addTypes */ false)) + typeParams.push_back(tyParam.second.ty); + } + else + { + for (auto tyParam : createGenerics(*defnScopePtr, alias->generics, /* useCache */ true, /* addTypes */ false)) + typeParams.push_back(tyParam.second.ty); + } std::vector typePackParams; - for (auto tpParam : createGenericPacks(*defnScope, alias->genericPacks, /* useCache */ true, /* addTypes */ false)) - typePackParams.push_back(tpParam.second.tp); + if (FFlag::LuauDontReferenceScopePtrFromHashTable) + { + for (const auto& tpParam : createGenericPacks(defnScope, alias->genericPacks, /* useCache */ true, /* addTypes */ false)) + typePackParams.push_back(tpParam.second.tp); + } + else + { + for (auto tpParam : createGenericPacks(*defnScopePtr, alias->genericPacks, /* useCache */ true, /* addTypes */ false)) + typePackParams.push_back(tpParam.second.tp); + } addConstraint( scope, @@ -1855,150 +1943,76 @@ ControlFlow ConstraintGenerator::visit(const ScopePtr& scope, AstStatTypeFunctio reportError(function->location, ReservedIdentifier{"typeof"}); } - if (FFlag::LuauTypeFunNoScopeMapRef) - { - auto scopeIt = astTypeFunctionEnvironmentScopes.find(function); - LUAU_ASSERT(scopeIt); - - ScopePtr environmentScope = *scopeIt; - - Checkpoint startCheckpoint = checkpoint(this); - FunctionSignature sig = checkFunctionSignature(environmentScope, function->body, /* expectedType */ std::nullopt); - - // Place this function as a child of the non-type function scope - if (FFlag::LuauEmplaceNotPushBack) - scope->children.emplace_back(sig.signatureScope.get()); - else - scope->children.push_back(NotNull{sig.signatureScope.get()}); - - interiorFreeTypes.emplace_back(); - checkFunctionBody(sig.bodyScope, function->body); - Checkpoint endCheckpoint = checkpoint(this); - - TypeId generalizedTy = arena->addType(BlockedType{}); - NotNull gc = addConstraint( - sig.signatureScope, - function->location, - GeneralizationConstraint{ - generalizedTy, - sig.signature, - std::vector{}, - } - ); - - sig.signatureScope->interiorFreeTypes = std::move(interiorFreeTypes.back().types); - sig.signatureScope->interiorFreeTypePacks = std::move(interiorFreeTypes.back().typePacks); - - getMutable(generalizedTy)->setOwner(gc); - interiorFreeTypes.pop_back(); - - Constraint* previous = nullptr; - forEachConstraint( - startCheckpoint, - endCheckpoint, - this, - [gc, &previous](const ConstraintPtr& constraint) - { - gc->dependencies.emplace_back(constraint.get()); - - if (auto psc = get(*constraint); psc && psc->returns) - { - if (previous) - { - if (FFlag::LuauEmplaceNotPushBack) - constraint->dependencies.emplace_back(previous); - else - constraint->dependencies.push_back(NotNull{previous}); - } - - previous = constraint.get(); - } - } - ); - - std::optional existingFunctionTy = environmentScope->lookup(function->name); - - if (!existingFunctionTy) - ice->ice("checkAliases did not populate type function name", function->nameLocation); + auto scopeIt = astTypeFunctionEnvironmentScopes.find(function); + LUAU_ASSERT(scopeIt); - TypeId unpackedTy = follow(*existingFunctionTy); + ScopePtr environmentScope = *scopeIt; - if (auto bt = get(unpackedTy); bt && nullptr == bt->getOwner()) - emplaceType(asMutable(unpackedTy), generalizedTy); + Checkpoint startCheckpoint = checkpoint(this); + FunctionSignature sig = checkFunctionSignature(environmentScope, function->body, /* expectedType */ std::nullopt); - return ControlFlow::None; - } + // Place this function as a child of the non-type function scope + if (FFlag::LuauEmplaceNotPushBack) + scope->children.emplace_back(sig.signatureScope.get()); else - { - auto scopePtr = astTypeFunctionEnvironmentScopes.find(function); - LUAU_ASSERT(scopePtr); + scope->children.push_back(NotNull{sig.signatureScope.get()}); - Checkpoint startCheckpoint = checkpoint(this); - FunctionSignature sig = checkFunctionSignature(*scopePtr, function->body, /* expectedType */ std::nullopt); - - // Place this function as a child of the non-type function scope - if (FFlag::LuauEmplaceNotPushBack) - scope->children.emplace_back(sig.signatureScope.get()); - else - scope->children.push_back(NotNull{sig.signatureScope.get()}); + interiorFreeTypes.emplace_back(); + checkFunctionBody(sig.bodyScope, function->body); + Checkpoint endCheckpoint = checkpoint(this); - interiorFreeTypes.emplace_back(); - checkFunctionBody(sig.bodyScope, function->body); - Checkpoint endCheckpoint = checkpoint(this); + TypeId generalizedTy = arena->addType(BlockedType{}); + NotNull gc = addConstraint( + sig.signatureScope, + function->location, + GeneralizationConstraint{ + generalizedTy, + sig.signature, + std::vector{}, + } + ); - TypeId generalizedTy = arena->addType(BlockedType{}); - NotNull gc = addConstraint( - sig.signatureScope, - function->location, - GeneralizationConstraint{ - generalizedTy, - sig.signature, - std::vector{}, - } - ); + sig.signatureScope->interiorFreeTypes = std::move(interiorFreeTypes.back().types); + sig.signatureScope->interiorFreeTypePacks = std::move(interiorFreeTypes.back().typePacks); - sig.signatureScope->interiorFreeTypes = std::move(interiorFreeTypes.back().types); - sig.signatureScope->interiorFreeTypePacks = std::move(interiorFreeTypes.back().typePacks); + getMutable(generalizedTy)->setOwner(gc); + interiorFreeTypes.pop_back(); - getMutable(generalizedTy)->setOwner(gc); - interiorFreeTypes.pop_back(); + Constraint* previous = nullptr; + forEachConstraint( + startCheckpoint, + endCheckpoint, + this, + [gc, &previous](const ConstraintPtr& constraint) + { + gc->dependencies.emplace_back(constraint.get()); - Constraint* previous = nullptr; - forEachConstraint( - startCheckpoint, - endCheckpoint, - this, - [gc, &previous](const ConstraintPtr& constraint) + if (auto psc = get(*constraint); psc && psc->returns) { - gc->dependencies.emplace_back(constraint.get()); - - if (auto psc = get(*constraint); psc && psc->returns) + if (previous) { - if (previous) - { - if (FFlag::LuauEmplaceNotPushBack) - constraint->dependencies.emplace_back(previous); - else - constraint->dependencies.push_back(NotNull{previous}); - } - - previous = constraint.get(); + if (FFlag::LuauEmplaceNotPushBack) + constraint->dependencies.emplace_back(previous); + else + constraint->dependencies.push_back(NotNull{previous}); } + + previous = constraint.get(); } - ); + } + ); - std::optional existingFunctionTy = (*scopePtr)->lookup(function->name); + std::optional existingFunctionTy = environmentScope->lookup(function->name); - if (!existingFunctionTy) - ice->ice("checkAliases did not populate type function name", function->nameLocation); + if (!existingFunctionTy) + ice->ice("checkAliases did not populate type function name", function->nameLocation); - TypeId unpackedTy = follow(*existingFunctionTy); + TypeId unpackedTy = follow(*existingFunctionTy); - if (auto bt = get(unpackedTy); bt && nullptr == bt->getOwner()) - emplaceType(asMutable(unpackedTy), generalizedTy); + if (auto bt = get(unpackedTy); bt && nullptr == bt->getOwner()) + emplaceType(asMutable(unpackedTy), generalizedTy); - return ControlFlow::None; - } + return ControlFlow::None; } ControlFlow ConstraintGenerator::visit(const ScopePtr& scope, AstStatDeclareGlobal* global) @@ -2058,13 +2072,10 @@ ControlFlow ConstraintGenerator::visit(const ScopePtr& scope, AstStatDeclareExte } ); - if (FFlag::LuauLimitDynamicConstraintSolving3) - { - // If we don't emplace an error type here, then later we'll be - // exposing a blocked type in this file's type interface. This - // is _normally_ harmless. - emplaceType(asMutable(bindingIt->second.type), builtinTypes->errorType); - } + // If we don't emplace an error type here, then later we'll be + // exposing a blocked type in this file's type interface. This + // is _normally_ harmless. + emplaceType(asMutable(bindingIt->second.type), builtinTypes->errorType); return ControlFlow::None; } @@ -2089,7 +2100,11 @@ ControlFlow ConstraintGenerator::visit(const ScopePtr& scope, AstStatDeclareExte if (!FFlag::LuauNoConstraintGenRecursionLimitIce) counter.emplace(&recursionCount); - if (recursionCount >= FInt::LuauCheckRecursionLimit) + if (FFlag::LuauIndividualRecursionLimits && recursionCount >= DFInt::LuauConstraintGeneratorRecursionLimit) + { + reportCodeTooComplex(declaredExternType->indexer->location); + } + else if (recursionCount >= FInt::LuauCheckRecursionLimit) { reportCodeTooComplex(declaredExternType->indexer->location); } @@ -2238,18 +2253,11 @@ ControlFlow ConstraintGenerator::visit(const ScopePtr& scope, AstStatDeclareFunc FunctionType* ftv = getMutable(fnType); ftv->isCheckedFunction = global->isCheckedFunction(); - if (FFlag::LuauParametrizedAttributeSyntax) - { - AstAttr* deprecatedAttr = global->getAttribute(AstAttr::Type::Deprecated); - ftv->isDeprecatedFunction = deprecatedAttr != nullptr; - if (deprecatedAttr) - { - ftv->deprecatedInfo = std::make_shared(deprecatedAttr->deprecatedInfo()); - } - } - else + AstAttr* deprecatedAttr = global->getAttribute(AstAttr::Type::Deprecated); + ftv->isDeprecatedFunction = deprecatedAttr != nullptr; + if (deprecatedAttr) { - ftv->isDeprecatedFunction = global->hasAttribute(AstAttr::Type::Deprecated); + ftv->deprecatedInfo = std::make_shared(deprecatedAttr->deprecatedInfo()); } ftv->argNames.reserve(global->paramNames.size); @@ -2317,17 +2325,28 @@ InferencePack ConstraintGenerator::checkPack( { RecursionCounter counter{&recursionCount}; - if (recursionCount >= FInt::LuauCheckRecursionLimit) + if (FFlag::LuauIndividualRecursionLimits) + { + if (recursionCount >= DFInt::LuauConstraintGeneratorRecursionLimit) + { + reportCodeTooComplex(expr->location); + return InferencePack{builtinTypes->errorTypePack}; + } + } + else { - reportCodeTooComplex(expr->location); - return InferencePack{builtinTypes->errorTypePack}; + if (recursionCount >= FInt::LuauCheckRecursionLimit) + { + reportCodeTooComplex(expr->location); + return InferencePack{builtinTypes->errorTypePack}; + } } InferencePack result; if (AstExprCall* call = expr->as()) result = checkPack(scope, call); - else if (AstExprVarargs* varargs = expr->as()) + else if (expr->is()) { if (scope->varargPack) result = InferencePack{*scope->varargPack}; @@ -2440,9 +2459,19 @@ InferencePack ConstraintGenerator::checkPack(const ScopePtr& scope, AstExprCall* { expectedType = expectedTypesForCall[i]; } - auto [ty, refinement] = check(scope, arg, expectedType, /*forceSingleton*/ false, /*generalize*/ false); - args.push_back(ty); - argumentRefinements.push_back(refinement); + if (FFlag::LuauAddRefinementToAssertions && i == 0 && matchAssert(*call)) + { + InConditionalContext flipper{&typeContext}; + auto [ty, refinement] = check(scope, arg, expectedType, /*forceSingleton*/ false, /*generalize*/ false); + args.push_back(ty); + argumentRefinements.push_back(refinement); + } + else + { + auto [ty, refinement] = check(scope, arg, expectedType, /*forceSingleton*/ false, /*generalize*/ false); + args.push_back(ty); + argumentRefinements.push_back(refinement); + } } else { @@ -2504,13 +2533,26 @@ InferencePack ConstraintGenerator::checkPack(const ScopePtr& scope, AstExprCall* if (isTableUnion(target)) { - const UnionType* targetUnion = get(target); - std::vector newParts; + if (FFlag::LuauMetatableAvoidSingletonUnion) + { + const UnionType* targetUnion = get(target); + UnionBuilder ub{arena, builtinTypes}; - for (TypeId ty : targetUnion) - newParts.push_back(arena->addType(MetatableType{ty, mt})); + for (TypeId ty : targetUnion) + ub.add(arena->addType(MetatableType{ty, mt})); - resultTy = arena->addType(UnionType{std::move(newParts)}); + resultTy = ub.build(); + } + else + { + const UnionType* targetUnion = get(target); + std::vector newParts; + + for (TypeId ty : targetUnion) + newParts.push_back(arena->addType(MetatableType{ty, mt})); + + resultTy = arena->addType(UnionType{std::move(newParts)}); + } } else resultTy = arena->addType(MetatableType{target, mt}); @@ -2520,7 +2562,7 @@ InferencePack ConstraintGenerator::checkPack(const ScopePtr& scope, AstExprCall* scope->bindings[targetLocal->local].typeId = resultTy; DefId def = dfg->getDef(targetLocal); - scope->lvalueTypes[def] = resultTy; // TODO: typestates: track this as an assignment + scope->lvalueTypes[def] = resultTy; // TODO: typestates: track this as an assignment updateRValueRefinements(scope, def, resultTy); // TODO: typestates: track this as an assignment // HACK: If we have a targetLocal, it has already been added to the @@ -2613,10 +2655,21 @@ Inference ConstraintGenerator::check(const ScopePtr& scope, AstExpr* expr, std:: { RecursionCounter counter{&recursionCount}; - if (recursionCount >= FInt::LuauCheckRecursionLimit) + if (FFlag::LuauIndividualRecursionLimits) { - reportCodeTooComplex(expr->location); - return Inference{builtinTypes->errorType}; + if (recursionCount >= DFInt::LuauConstraintGeneratorRecursionLimit) + { + reportCodeTooComplex(expr->location); + return Inference{builtinTypes->errorType}; + } + } + else + { + if (recursionCount >= FInt::LuauCheckRecursionLimit) + { + reportCodeTooComplex(expr->location); + return Inference{builtinTypes->errorType}; + } } // We may recurse a given expression more than once when checking compound @@ -2839,7 +2892,7 @@ Inference ConstraintGenerator::checkIndexName( if (FFlag::LuauCacheDuplicateHasPropConstraints) { - if (auto cachedHasPropResult = propIndexPairsSeen.find({obj,index})) + if (auto cachedHasPropResult = propIndexPairsSeen.find({obj, index})) result = *cachedHasPropResult; } @@ -2978,12 +3031,24 @@ Inference ConstraintGenerator::check(const ScopePtr& scope, AstExprUnary* unary) { case AstExprUnary::Op::Not: { - TypeId resultType = createTypeFunctionInstance(builtinTypeFunctions().notFunc, {operandType}, {}, scope, unary->location); + TypeId resultType = createTypeFunctionInstance( + FFlag::LuauBuiltinTypeFunctionsArentGlobal ? builtinTypes->typeFunctions->notFunc : builtinTypeFunctions_DEPRECATED().notFunc, + {operandType}, + {}, + scope, + unary->location + ); return Inference{resultType, refinementArena.negation(refinement)}; } case AstExprUnary::Op::Len: { - TypeId resultType = createTypeFunctionInstance(builtinTypeFunctions().lenFunc, {operandType}, {}, scope, unary->location); + TypeId resultType = createTypeFunctionInstance( + FFlag::LuauBuiltinTypeFunctionsArentGlobal ? builtinTypes->typeFunctions->lenFunc : builtinTypeFunctions_DEPRECATED().lenFunc, + {operandType}, + {}, + scope, + unary->location + ); if (FFlag::LuauNumericUnaryOpsDontProduceNegationRefinements) return Inference{resultType, std::move(refinement)}; else @@ -2991,7 +3056,13 @@ Inference ConstraintGenerator::check(const ScopePtr& scope, AstExprUnary* unary) } case AstExprUnary::Op::Minus: { - TypeId resultType = createTypeFunctionInstance(builtinTypeFunctions().unmFunc, {operandType}, {}, scope, unary->location); + TypeId resultType = createTypeFunctionInstance( + FFlag::LuauBuiltinTypeFunctionsArentGlobal ? builtinTypes->typeFunctions->unmFunc : builtinTypeFunctions_DEPRECATED().unmFunc, + {operandType}, + {}, + scope, + unary->location + ); if (FFlag::LuauNumericUnaryOpsDontProduceNegationRefinements) return Inference{resultType, std::move(refinement)}; else @@ -3022,93 +3093,193 @@ Inference ConstraintGenerator::checkAstExprBinary( { case AstExprBinary::Op::Add: { - TypeId resultType = createTypeFunctionInstance(builtinTypeFunctions().addFunc, {leftType, rightType}, {}, scope, location); + TypeId resultType = createTypeFunctionInstance( + FFlag::LuauBuiltinTypeFunctionsArentGlobal ? builtinTypes->typeFunctions->addFunc : builtinTypeFunctions_DEPRECATED().addFunc, + {leftType, rightType}, + {}, + scope, + location + ); return Inference{resultType, std::move(refinement)}; } case AstExprBinary::Op::Sub: { - TypeId resultType = createTypeFunctionInstance(builtinTypeFunctions().subFunc, {leftType, rightType}, {}, scope, location); + TypeId resultType = createTypeFunctionInstance( + FFlag::LuauBuiltinTypeFunctionsArentGlobal ? builtinTypes->typeFunctions->subFunc : builtinTypeFunctions_DEPRECATED().subFunc, + {leftType, rightType}, + {}, + scope, + location + ); return Inference{resultType, std::move(refinement)}; } case AstExprBinary::Op::Mul: { - TypeId resultType = createTypeFunctionInstance(builtinTypeFunctions().mulFunc, {leftType, rightType}, {}, scope, location); + TypeId resultType = createTypeFunctionInstance( + FFlag::LuauBuiltinTypeFunctionsArentGlobal ? builtinTypes->typeFunctions->mulFunc : builtinTypeFunctions_DEPRECATED().mulFunc, + {leftType, rightType}, + {}, + scope, + location + ); return Inference{resultType, std::move(refinement)}; } case AstExprBinary::Op::Div: { - TypeId resultType = createTypeFunctionInstance(builtinTypeFunctions().divFunc, {leftType, rightType}, {}, scope, location); + TypeId resultType = createTypeFunctionInstance( + FFlag::LuauBuiltinTypeFunctionsArentGlobal ? builtinTypes->typeFunctions->divFunc : builtinTypeFunctions_DEPRECATED().divFunc, + {leftType, rightType}, + {}, + scope, + location + ); return Inference{resultType, std::move(refinement)}; } case AstExprBinary::Op::FloorDiv: { - TypeId resultType = createTypeFunctionInstance(builtinTypeFunctions().idivFunc, {leftType, rightType}, {}, scope, location); + TypeId resultType = createTypeFunctionInstance( + FFlag::LuauBuiltinTypeFunctionsArentGlobal ? builtinTypes->typeFunctions->idivFunc : builtinTypeFunctions_DEPRECATED().idivFunc, + {leftType, rightType}, + {}, + scope, + location + ); return Inference{resultType, std::move(refinement)}; } case AstExprBinary::Op::Pow: { - TypeId resultType = createTypeFunctionInstance(builtinTypeFunctions().powFunc, {leftType, rightType}, {}, scope, location); + TypeId resultType = createTypeFunctionInstance( + FFlag::LuauBuiltinTypeFunctionsArentGlobal ? builtinTypes->typeFunctions->powFunc : builtinTypeFunctions_DEPRECATED().powFunc, + {leftType, rightType}, + {}, + scope, + location + ); return Inference{resultType, std::move(refinement)}; } case AstExprBinary::Op::Mod: { - TypeId resultType = createTypeFunctionInstance(builtinTypeFunctions().modFunc, {leftType, rightType}, {}, scope, location); + TypeId resultType = createTypeFunctionInstance( + FFlag::LuauBuiltinTypeFunctionsArentGlobal ? builtinTypes->typeFunctions->modFunc : builtinTypeFunctions_DEPRECATED().modFunc, + {leftType, rightType}, + {}, + scope, + location + ); return Inference{resultType, std::move(refinement)}; } case AstExprBinary::Op::Concat: { - TypeId resultType = createTypeFunctionInstance(builtinTypeFunctions().concatFunc, {leftType, rightType}, {}, scope, location); + TypeId resultType = createTypeFunctionInstance( + FFlag::LuauBuiltinTypeFunctionsArentGlobal ? builtinTypes->typeFunctions->concatFunc : builtinTypeFunctions_DEPRECATED().concatFunc, + {leftType, rightType}, + {}, + scope, + location + ); return Inference{resultType, std::move(refinement)}; } case AstExprBinary::Op::And: { - TypeId resultType = createTypeFunctionInstance(builtinTypeFunctions().andFunc, {leftType, rightType}, {}, scope, location); + TypeId resultType = createTypeFunctionInstance( + FFlag::LuauBuiltinTypeFunctionsArentGlobal ? builtinTypes->typeFunctions->andFunc : builtinTypeFunctions_DEPRECATED().andFunc, + {leftType, rightType}, + {}, + scope, + location + ); return Inference{resultType, std::move(refinement)}; } case AstExprBinary::Op::Or: { - TypeId resultType = createTypeFunctionInstance(builtinTypeFunctions().orFunc, {leftType, rightType}, {}, scope, location); + TypeId resultType = createTypeFunctionInstance( + FFlag::LuauBuiltinTypeFunctionsArentGlobal ? builtinTypes->typeFunctions->orFunc : builtinTypeFunctions_DEPRECATED().orFunc, + {leftType, rightType}, + {}, + scope, + location + ); return Inference{resultType, std::move(refinement)}; } case AstExprBinary::Op::CompareLt: { addConstraint(scope, location, EqualityConstraint{leftType, rightType}); - TypeId resultType = createTypeFunctionInstance(builtinTypeFunctions().ltFunc, {leftType, rightType}, {}, scope, location); - return Inference{resultType, std::move(refinement)}; + if (FFlag::LuauNoOrderingTypeFunctions) + { + return Inference{builtinTypes->booleanType, std::move(refinement)}; + } + else + { + TypeId resultType = createTypeFunctionInstance( + FFlag::LuauBuiltinTypeFunctionsArentGlobal ? builtinTypes->typeFunctions->ltFunc : builtinTypeFunctions_DEPRECATED().ltFunc, + {leftType, rightType}, + {}, + scope, + location + ); + return Inference{resultType, std::move(refinement)}; + } } case AstExprBinary::Op::CompareGe: { addConstraint(scope, location, EqualityConstraint{leftType, rightType}); - TypeId resultType = createTypeFunctionInstance( - builtinTypeFunctions().ltFunc, - {rightType, leftType}, // lua decided that `__ge(a, b)` is instead just `__lt(b, a)` - {}, - scope, - location - ); - return Inference{resultType, std::move(refinement)}; + if (FFlag::LuauNoOrderingTypeFunctions) + { + return Inference{builtinTypes->booleanType, std::move(refinement)}; + } + else + { + TypeId resultType = createTypeFunctionInstance( + FFlag::LuauBuiltinTypeFunctionsArentGlobal ? builtinTypes->typeFunctions->ltFunc : builtinTypeFunctions_DEPRECATED().ltFunc, + {rightType, leftType}, // lua decided that `__ge(a, b)` is instead just `__lt(b, a)` + {}, + scope, + location + ); + return Inference{resultType, std::move(refinement)}; + } } case AstExprBinary::Op::CompareLe: { addConstraint(scope, location, EqualityConstraint{leftType, rightType}); - TypeId resultType = createTypeFunctionInstance(builtinTypeFunctions().leFunc, {leftType, rightType}, {}, scope, location); - return Inference{resultType, std::move(refinement)}; + if (FFlag::LuauNoOrderingTypeFunctions) + { + return Inference{builtinTypes->booleanType, std::move(refinement)}; + } + else + { + TypeId resultType = createTypeFunctionInstance( + FFlag::LuauBuiltinTypeFunctionsArentGlobal ? builtinTypes->typeFunctions->leFunc : builtinTypeFunctions_DEPRECATED().leFunc, + {leftType, rightType}, + {}, + scope, + location + ); + return Inference{resultType, std::move(refinement)}; + } } case AstExprBinary::Op::CompareGt: { addConstraint(scope, location, EqualityConstraint{leftType, rightType}); - TypeId resultType = createTypeFunctionInstance( - builtinTypeFunctions().leFunc, - {rightType, leftType}, // lua decided that `__gt(a, b)` is instead just `__le(b, a)` - {}, - scope, - location - ); - return Inference{resultType, std::move(refinement)}; + if (FFlag::LuauNoOrderingTypeFunctions) + { + return Inference{builtinTypes->booleanType, std::move(refinement)}; + } + else + { + TypeId resultType = createTypeFunctionInstance( + FFlag::LuauBuiltinTypeFunctionsArentGlobal ? builtinTypes->typeFunctions->leFunc : builtinTypeFunctions_DEPRECATED().leFunc, + {rightType, leftType}, // lua decided that `__gt(a, b)` is instead just `__le(b, a)` + {}, + scope, + location + ); + return Inference{resultType, std::move(refinement)}; + } } case AstExprBinary::Op::CompareEq: case AstExprBinary::Op::CompareNe: @@ -3131,7 +3302,13 @@ Inference ConstraintGenerator::checkAstExprBinary( else if (rightSubscripted) rightType = makeUnion(scope, location, rightType, builtinTypes->nilType); - TypeId resultType = createTypeFunctionInstance(builtinTypeFunctions().eqFunc, {leftType, rightType}, {}, scope, location); + TypeId resultType = createTypeFunctionInstance( + FFlag::LuauBuiltinTypeFunctionsArentGlobal ? builtinTypes->typeFunctions->eqFunc : builtinTypeFunctions_DEPRECATED().eqFunc, + {leftType, rightType}, + {}, + scope, + location + ); return Inference{resultType, std::move(refinement)}; } } @@ -3161,10 +3338,7 @@ Inference ConstraintGenerator::check(const ScopePtr& scope, AstExprIfElse* ifEls applyRefinements(elseScope, ifElse->falseExpr->location, refinementArena.negation(refinement)); TypeId elseType = check(elseScope, ifElse->falseExpr, expectedType).ty; - if (FFlag::LuauInferActualIfElseExprType2) - return Inference{makeUnion(scope, ifElse->location, thenType, elseType)}; - else - return Inference{expectedType ? *expectedType : makeUnion(scope, ifElse->location, thenType, elseType)}; + return Inference{makeUnion(scope, ifElse->location, thenType, elseType)}; } Inference ConstraintGenerator::check(const ScopePtr& scope, AstExprTypeAssertion* typeAssert) @@ -3461,12 +3635,31 @@ Inference ConstraintGenerator::check(const ScopePtr& scope, AstExprTable* expr, TypeIds valuesLowerBound; + std::optional start{std::nullopt}; + if (FFlag::LuauPushTypeConstraint2 && FFlag::LuauPushTypeConstraintLambdas) + start = checkpoint(this); + for (const AstExprTable::Item& item : expr->items) { // Expected types are threaded through table literals separately via the // function matchLiteralType. - TypeId itemTy = check(scope, item.value).ty; + // generalize is false here as we want to be able to push types into lambdas in a situation like: + // + // type Callback = (string) -> () + // + // local t: { Callback } = { + // function (s) + // -- s should have type `string` here + // end + // } + TypeId itemTy = check( + scope, + item.value, + /* expectedType */ std::nullopt, + /* forceSingleton */ false, + /* generalize */ !(FFlag::LuauPushTypeConstraint2 && FFlag::LuauPushTypeConstraintLambdas) + ).ty; if (item.key) { @@ -3494,6 +3687,10 @@ Inference ConstraintGenerator::check(const ScopePtr& scope, AstExprTable* expr, } } + std::optional end{std::nullopt}; + if (FFlag::LuauPushTypeConstraintLambdas) + end = checkpoint(this); + if (!indexKeyLowerBound.empty()) { LUAU_ASSERT(!indexValueLowerBound.empty()); @@ -3524,9 +3721,9 @@ Inference ConstraintGenerator::check(const ScopePtr& scope, AstExprTable* expr, ttv->indexer = TableIndexer{indexKey, indexValue}; } - if (FFlag::LuauPushTypeConstraint && expectedType) + if (FFlag::LuauPushTypeConstraint2 && expectedType) { - addConstraint( + auto ptc = addConstraint( scope, expr->location, PushTypeConstraint{ @@ -3537,6 +3734,19 @@ Inference ConstraintGenerator::check(const ScopePtr& scope, AstExprTable* expr, /* expr */ NotNull{expr}, } ); + if (FFlag::LuauPushTypeConstraintLambdas) + { + LUAU_ASSERT(start && end); + forEachConstraint( + *start, + *end, + this, + [ptc](const ConstraintPtr& c) + { + c->dependencies.emplace_back(ptc.get()); + } + ); + } } if (FInt::LuauPrimitiveInferenceInTableLimit > 0 && expr->items.size > size_t(FInt::LuauPrimitiveInferenceInTableLimit)) @@ -3815,7 +4025,7 @@ TypeId ConstraintGenerator::resolveReferenceType( else return resolveType_(scope, ref->parameters.data[0].type, inTypeArguments); } - else if (FFlag::LuauLimitDynamicConstraintSolving3 && ref->name == "_luau_blocked_type") + else if (ref->name == "_luau_blocked_type") { return arena->addType(BlockedType{}); } @@ -4005,18 +4215,11 @@ TypeId ConstraintGenerator::resolveFunctionType( // how to quantify/instantiate it. FunctionType ftv{TypeLevel{}, {}, {}, argTypes, returnTypes}; ftv.isCheckedFunction = fn->isCheckedFunction(); - if (FFlag::LuauParametrizedAttributeSyntax) + AstAttr* deprecatedAttr = fn->getAttribute(AstAttr::Type::Deprecated); + ftv.isDeprecatedFunction = deprecatedAttr != nullptr; + if (deprecatedAttr) { - AstAttr* deprecatedAttr = fn->getAttribute(AstAttr::Type::Deprecated); - ftv.isDeprecatedFunction = deprecatedAttr != nullptr; - if (deprecatedAttr) - { - ftv.deprecatedInfo = std::make_shared(deprecatedAttr->deprecatedInfo()); - } - } - else - { - ftv.isDeprecatedFunction = fn->hasAttribute(AstAttr::Type::Deprecated); + ftv.deprecatedInfo = std::make_shared(deprecatedAttr->deprecatedInfo()); } @@ -4365,7 +4568,13 @@ TypeId ConstraintGenerator::makeUnion(std::vector options) TypeId ConstraintGenerator::makeIntersect(const ScopePtr& scope, Location location, TypeId lhs, TypeId rhs) { - TypeId resultType = createTypeFunctionInstance(builtinTypeFunctions().intersectFunc, {lhs, rhs}, {}, scope, location); + TypeId resultType = createTypeFunctionInstance( + FFlag::LuauBuiltinTypeFunctionsArentGlobal ? builtinTypes->typeFunctions->intersectFunc : builtinTypeFunctions_DEPRECATED().intersectFunc, + {lhs, rhs}, + {}, + scope, + location + ); return resultType; } diff --git a/Analysis/src/ConstraintSolver.cpp b/Analysis/src/ConstraintSolver.cpp index e9de78d4..80a9c585 100644 --- a/Analysis/src/ConstraintSolver.cpp +++ b/Analysis/src/ConstraintSolver.cpp @@ -14,6 +14,7 @@ #include "Luau/ModuleResolver.h" #include "Luau/OverloadResolution.h" #include "Luau/RecursionCounter.h" +#include "Luau/ScopedSeenSet.h" #include "Luau/Simplify.h" #include "Luau/TableLiteralInference.h" #include "Luau/TimeTrace.h" @@ -40,16 +41,17 @@ LUAU_FASTFLAGVARIABLE(DebugLuauEqSatSimplification) LUAU_FASTFLAG(LuauTrackUniqueness) LUAU_FASTFLAG(LuauLimitUnification) LUAU_FASTFLAGVARIABLE(LuauCollapseShouldNotCrash) -LUAU_FASTFLAGVARIABLE(LuauContainsAnyGenericFollowBeforeChecking) -LUAU_FASTFLAGVARIABLE(LuauLimitDynamicConstraintSolving3) LUAU_FASTFLAGVARIABLE(LuauDontDynamicallyCreateRedundantSubtypeConstraints) LUAU_FASTFLAGVARIABLE(LuauExtendSealedTableUpperBounds) LUAU_FASTFLAG(LuauReduceSetTypeStackPressure) -LUAU_FASTFLAG(LuauParametrizedAttributeSyntax) -LUAU_FASTFLAGVARIABLE(LuauNameConstraintRestrictRecursiveTypes) LUAU_FASTFLAG(LuauExplicitSkipBoundTypes) LUAU_FASTFLAG(DebugLuauStringSingletonBasedOnQuotes) -LUAU_FASTFLAG(LuauPushTypeConstraint) +LUAU_FASTFLAG(LuauPushTypeConstraint2) +LUAU_FASTFLAGVARIABLE(LuauScopedSeenSetInLookupTableProp) +LUAU_FASTFLAGVARIABLE(LuauIterableBindNotUnify) +LUAU_FASTFLAGVARIABLE(LuauAvoidOverloadSelectionForFunctionType) +LUAU_FASTFLAG(LuauSimplifyIntersectionNoTreeSet) +LUAU_FASTFLAG(LuauPushTypeConstraintLambdas) namespace Luau { @@ -464,9 +466,7 @@ void ConstraintSolver::run() if (FFlag::DebugLuauLogSolver) { - printf( - "Starting solver for module %s (%s)\n", module->humanReadableName.c_str(), module->name.c_str() - ); + printf("Starting solver for module %s (%s)\n", module->humanReadableName.c_str(), module->name.c_str()); dump(this, opts); printf("Bindings:\n"); dumpBindings(rootScope, opts); @@ -507,7 +507,7 @@ void ConstraintSolver::run() // If we were _given_ a limit, and the current limit has hit zero, ] // then early exit from constraint solving. - if (FFlag::LuauLimitDynamicConstraintSolving3 && FInt::LuauSolverConstraintLimit > 0 && solverConstraintLimit == 0) + if (FInt::LuauSolverConstraintLimit > 0 && solverConstraintLimit == 0) break; std::string saveMe = FFlag::DebugLuauLogSolver ? toString(*c, opts) : std::string{}; @@ -938,10 +938,7 @@ bool ConstraintSolver::tryDispatch(const GeneralizationConstraint& c, NotNullisDeprecatedFunction = true; - if (FFlag::LuauParametrizedAttributeSyntax) - { - fty->deprecatedInfo = std::make_shared(c.deprecatedInfo); - } + fty->deprecatedInfo = std::make_shared(c.deprecatedInfo); } } } @@ -1067,8 +1064,16 @@ bool ConstraintSolver::tryDispatch(const IterableConstraint& c, NotNullerrorType, ty); + if (FFlag::LuauIterableBindNotUnify) + { + for (TypeId ty : c.variables) + bind(constraint, ty, builtinTypes->errorType); + } + else + { + for (TypeId ty : c.variables) + unify(constraint, builtinTypes->errorType, ty); + } return true; } @@ -1135,27 +1140,24 @@ bool ConstraintSolver::tryDispatch(const NameConstraint& c, NotNullpersistent || target->owningArena != arena) return true; - if (FFlag::LuauNameConstraintRestrictRecursiveTypes) + if (std::optional tf = constraint->scope->lookupType(c.name)) { - if (std::optional tf = constraint->scope->lookupType(c.name)) - { - // We check to see if this type alias violates the recursion restriction - InstantiationSignature signature{ - *tf, - c.typeParameters, - c.typePackParameters, - }; + // We check to see if this type alias violates the recursion restriction + InstantiationSignature signature{ + *tf, + c.typeParameters, + c.typePackParameters, + }; - InfiniteTypeFinder itf{this, signature, constraint->scope}; - itf.traverse(target); + 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; - } + if (itf.foundInfiniteType) + { + constraint->scope->invalidTypeAliasNames.insert(c.name); + shiftReferences(target, builtinTypes->errorType); + emplaceType(asMutable(target), builtinTypes->errorType); + return true; } } @@ -1217,7 +1219,7 @@ bool ConstraintSolver::tryDispatch(const TypeAliasExpansionConstraint& c, NotNul } // Adding ReduceConstraint on type function for the constraint solver - if (auto typeFn = get(follow(tf->type))) + if (get(follow(tf->type))) pushConstraint(NotNull(constraint->scope.get()), constraint->location, ReduceConstraint{tf->type}); // Due to how pending expansion types and TypeFun's are created @@ -1549,10 +1551,15 @@ bool ConstraintSolver::tryDispatch(const FunctionCallConstraint& c, NotNullargs, NotNull{&module->astTypes}); - auto [status, overload] = resolver.selectOverload(fn, argsPack, NotNull{&uniqueTypes}, /*useFreeTypeBounds*/ force); TypeId overloadToUse = fn; - if (status == OverloadResolver::Analysis::Ok) - overloadToUse = overload; + // NOTE: This probably ends up capturing union types as well, but + // that should be fairly uncommon. + if (!FFlag::LuauAvoidOverloadSelectionForFunctionType || !is(fn)) + { + auto [status, overload] = resolver.selectOverload(fn, argsPack, NotNull{&uniqueTypes}, /*useFreeTypeBounds*/ force); + if (status == OverloadResolver::Analysis::Ok) + overloadToUse = overload; + } TypeId inferredTy = arena->addType(FunctionType{TypeLevel{}, argsPack, c.result}); Unifier2 u2{NotNull{arena}, builtinTypes, constraint->scope, NotNull{&iceReporter}}; @@ -1621,48 +1628,6 @@ bool ConstraintSolver::tryDispatch(const FunctionCallConstraint& c, NotNull generics{nullptr}; - - bool found = false; - - ContainsGenerics_DEPRECATED() - : TypeOnceVisitor("ContainsGenerics_DEPRECATED", FFlag::LuauExplicitSkipBoundTypes) - { - } - - bool visit(TypeId ty) override - { - return !found; - } - - bool visit(TypeId ty, const GenericType&) override - { - found |= generics.contains(ty); - return true; - } - - bool visit(TypeId ty, const TypeFunctionInstanceType&) override - { - return !found; - } - - bool visit(TypePackId tp, const GenericTypePack&) override - { - found |= generics.contains(tp); - return !found; - } - - bool hasGeneric(TypeId ty) - { - traverse(ty); - auto ret = found; - found = false; - return ret; - } -}; - namespace { struct ContainsGenerics : public TypeOnceVisitor @@ -1781,53 +1746,19 @@ bool ConstraintSolver::tryDispatch(const FunctionCheckConstraint& c, NotNullself ? 1 : 0; - for (size_t i = 0; i < c.callSite->args.size && i + typeOffset < expectedArgs.size() && i + typeOffset < argPackHead.size(); ++i) + if (FFlag::LuauPushTypeConstraintLambdas && FFlag::LuauPushTypeConstraint2) { - TypeId expectedArgTy = follow(expectedArgs[i + typeOffset]); - const TypeId actualArgTy = follow(argPackHead[i + typeOffset]); - AstExpr* expr = unwrapGroup(c.callSite->args.data[i]); - - (*c.astExpectedTypes)[expr] = expectedArgTy; - - const auto lambdaTy = get(actualArgTy); - const auto expectedLambdaTy = get(expectedArgTy); - const auto lambdaExpr = expr->as(); + Subtyping subtyping{builtinTypes, arena, simplifier, normalizer, typeFunctionRuntime, NotNull{&iceReporter}}; - if (expectedLambdaTy && lambdaTy && lambdaExpr) + for (size_t i = 0; i < c.callSite->args.size && i + typeOffset < expectedArgs.size() && i + typeOffset < argPackHead.size(); ++i) { - if (ContainsGenerics::hasGeneric(expectedArgTy, NotNull{&genericTypesAndPacks})) - continue; - - const std::vector expectedLambdaArgTys = flatten(expectedLambdaTy->argTypes).first; - const std::vector lambdaArgTys = flatten(lambdaTy->argTypes).first; + TypeId expectedArgTy = follow(expectedArgs[i + typeOffset]); + AstExpr* expr = unwrapGroup(c.callSite->args.data[i]); - for (size_t j = 0; j < expectedLambdaArgTys.size() && j < lambdaArgTys.size() && j < lambdaExpr->args.size; ++j) - { - if (!lambdaExpr->args.data[j]->annotation && get(follow(lambdaArgTys[j]))) - { - shiftReferences(lambdaArgTys[j], expectedLambdaArgTys[j]); - bind(constraint, lambdaArgTys[j], expectedLambdaArgTys[j]); - } - } - } - else if (expr->is() || expr->is() || expr->is() || - expr->is() || expr->is()) - { - if (ContainsGenerics::hasGeneric(expectedArgTy, NotNull{&genericTypesAndPacks})) - { - replacer.resetState(TxnLog::empty(), arena); - if (auto res = replacer.substitute(expectedArgTy)) - { - InstantiationQueuer queuer{constraint->scope, constraint->location, this}; - queuer.traverse(*res); - expectedArgTy = *res; - } - } - if (FFlag::LuauPushTypeConstraint) + if (isLiteral(expr)) { - Subtyping subtyping{builtinTypes, arena, simplifier, normalizer, typeFunctionRuntime, NotNull{&iceReporter}}; PushTypeResult result = - pushTypeInto(c.astTypes, c.astExpectedTypes, builtinTypes, arena, NotNull{&u2}, NotNull{&subtyping}, expectedArgTy, expr); + pushTypeInto(c.astTypes, c.astExpectedTypes, NotNull{this}, constraint, NotNull{&u2}, NotNull{&subtyping}, expectedArgTy, expr); // Consider: // @@ -1860,13 +1791,97 @@ bool ConstraintSolver::tryDispatch(const FunctionCheckConstraint& c, NotNullargs.size && i + typeOffset < expectedArgs.size() && i + typeOffset < argPackHead.size(); ++i) + { + TypeId expectedArgTy = follow(expectedArgs[i + typeOffset]); + const TypeId actualArgTy = follow(argPackHead[i + typeOffset]); + AstExpr* expr = unwrapGroup(c.callSite->args.data[i]); + + (*c.astExpectedTypes)[expr] = expectedArgTy; + + const auto lambdaTy = get(actualArgTy); + const auto expectedLambdaTy = get(expectedArgTy); + const auto lambdaExpr = expr->as(); + + if (expectedLambdaTy && lambdaTy && lambdaExpr) { - u2.unify(actualArgTy, expectedArgTy); + if (ContainsGenerics::hasGeneric(expectedArgTy, NotNull{&genericTypesAndPacks})) + continue; + + const std::vector expectedLambdaArgTys = flatten(expectedLambdaTy->argTypes).first; + const std::vector lambdaArgTys = flatten(lambdaTy->argTypes).first; + + for (size_t j = 0; j < expectedLambdaArgTys.size() && j < lambdaArgTys.size() && j < lambdaExpr->args.size; ++j) + { + if (!lambdaExpr->args.data[j]->annotation && get(follow(lambdaArgTys[j]))) + { + shiftReferences(lambdaArgTys[j], expectedLambdaArgTys[j]); + bind(constraint, lambdaArgTys[j], expectedLambdaArgTys[j]); + } + } + } + else if (expr->is() || expr->is() || expr->is() || + expr->is() || expr->is()) + { + if (ContainsGenerics::hasGeneric(expectedArgTy, NotNull{&genericTypesAndPacks})) + { + replacer.resetState(TxnLog::empty(), arena); + if (auto res = replacer.substitute(expectedArgTy)) + { + InstantiationQueuer queuer{constraint->scope, constraint->location, this}; + queuer.traverse(*res); + expectedArgTy = *res; + } + } + if (FFlag::LuauPushTypeConstraint2) + { + Subtyping subtyping{builtinTypes, arena, simplifier, 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) + { + 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); + } } } } - // Consider: // // local Direction = { Left = 1, Right = 2 } @@ -2441,46 +2456,95 @@ bool ConstraintSolver::tryDispatch(const AssignIndexConstraint& c, NotNull(lhsType)) { - std::set parts; - for (TypeId t : lhsIntersection) + if (FFlag::LuauSimplifyIntersectionNoTreeSet) { - if (auto tbl = getMutable(follow(t))) + + TypeIds parts; + + for (TypeId t : lhsIntersection) { - if (tbl->indexer) + if (auto tbl = getMutable(follow(t))) { - unify(constraint, indexType, tbl->indexer->indexType); - parts.insert(tbl->indexer->indexResultType); - } + if (tbl->indexer) + { + unify(constraint, indexType, tbl->indexer->indexType); + parts.insert(tbl->indexer->indexResultType); + } - if (tbl->state == TableState::Unsealed || tbl->state == TableState::Free) + if (tbl->state == TableState::Unsealed || tbl->state == TableState::Free) + { + tbl->indexer = TableIndexer{indexType, rhsType}; + parts.insert(rhsType); + } + } + else if (auto cls = get(follow(t))) { - tbl->indexer = TableIndexer{indexType, rhsType}; - parts.insert(rhsType); + while (true) + { + if (cls->indexer) + { + unify(constraint, indexType, cls->indexer->indexType); + parts.insert(cls->indexer->indexResultType); + break; + } + + if (cls->parent) + cls = get(cls->parent); + else + break; + } } } - else if (auto cls = get(follow(t))) + + TypeId res = simplifyIntersection(constraint->scope, constraint->location, std::move(parts)); + + unify(constraint, rhsType, res); + } + else + { + + std::set parts; + + for (TypeId t : lhsIntersection) { - while (true) + if (auto tbl = getMutable(follow(t))) { - if (cls->indexer) + if (tbl->indexer) { - unify(constraint, indexType, cls->indexer->indexType); - parts.insert(cls->indexer->indexResultType); - break; + unify(constraint, indexType, tbl->indexer->indexType); + parts.insert(tbl->indexer->indexResultType); } - if (cls->parent) - cls = get(cls->parent); - else - break; + if (tbl->state == TableState::Unsealed || tbl->state == TableState::Free) + { + tbl->indexer = TableIndexer{indexType, rhsType}; + parts.insert(rhsType); + } + } + else if (auto cls = get(follow(t))) + { + while (true) + { + if (cls->indexer) + { + unify(constraint, indexType, cls->indexer->indexType); + parts.insert(cls->indexer->indexResultType); + break; + } + + if (cls->parent) + cls = get(cls->parent); + else + break; + } } } - } - TypeId res = simplifyIntersection(constraint->scope, constraint->location, std::move(parts)); + TypeId res = simplifyIntersection_DEPRECATED(constraint->scope, constraint->location, std::move(parts)); - unify(constraint, rhsType, res); + unify(constraint, rhsType, res); + } } // Other types do not support index assignment. @@ -2742,50 +2806,6 @@ bool ConstraintSolver::tryDispatch(const SimplifyConstraint& c, NotNull(ty); - return !found; - } - - bool visit(TypePackId ty) override - { - if (FFlag::LuauContainsAnyGenericFollowBeforeChecking) - found = found || is(follow(ty)); - else - found = found || is(ty); - return !found; - } - - static bool hasAnyGeneric(TypeId ty) - { - ContainsAnyGeneric cg; - cg.traverse(ty); - return cg.found; - } - - static bool hasAnyGeneric(TypePackId tp) - { - ContainsAnyGeneric cg; - cg.traverse(tp); - return cg.found; - } -}; - -} // namespace - bool ConstraintSolver::tryDispatch(const PushFunctionTypeConstraint& c, NotNull constraint) { // NOTE: This logic could probably be combined with that of @@ -2845,7 +2865,7 @@ bool ConstraintSolver::tryDispatch(const PushFunctionTypeConstraint& c, NotNull< bool ConstraintSolver::tryDispatch(const PushTypeConstraint& c, NotNull constraint, bool force) { - LUAU_ASSERT(FFlag::LuauPushTypeConstraint); + LUAU_ASSERT(FFlag::LuauPushTypeConstraint2); Unifier2 u2{arena, builtinTypes, constraint->scope, NotNull{&iceReporter}, &uninhabitedTypeFunctions}; Subtyping subtyping{builtinTypes, arena, simplifier, normalizer, typeFunctionRuntime, NotNull{&iceReporter}}; @@ -2854,10 +2874,12 @@ bool ConstraintSolver::tryDispatch(const PushTypeConstraint& c, NotNull seen{nullptr}; + Set seen{nullptr}; return lookupTableProp(constraint, subjectType, propName, context, inConditional, suppressSimplification, seen); } @@ -3101,12 +3123,18 @@ TablePropLookupResult ConstraintSolver::lookupTableProp( ValueContext context, bool inConditional, bool suppressSimplification, - DenseHashSet& seen + Set& seen ) { if (seen.contains(subjectType)) return {}; - seen.insert(subjectType); + + std::optional, TypeId>> ss; // This won't be needed once LuauScopedSeenSetInLookupTableProp is clipped. + + if (FFlag::LuauScopedSeenSetInLookupTableProp) + ss.emplace(seen, subjectType); + else + seen.insert(subjectType); subjectType = follow(subjectType); @@ -3634,7 +3662,7 @@ bool ConstraintSolver::isBlocked(TypePackId tp) const { tp = follow(tp); - if (auto tfitp = get(tp)) + if (get(tp)) return uninhabitedTypeFunctions.contains(tp) == false; return nullptr != get(tp); @@ -3672,15 +3700,12 @@ NotNull ConstraintSolver::pushConstraint(NotNull scope, const solverConstraints.push_back(std::move(c)); unsolvedConstraints.emplace_back(borrow); - if (FFlag::LuauLimitDynamicConstraintSolving3) + if (solverConstraintLimit > 0) { - if (solverConstraintLimit > 0) - { - --solverConstraintLimit; + --solverConstraintLimit; - if (solverConstraintLimit == 0) - reportError(CodeTooComplex{}, location); - } + if (solverConstraintLimit == 0) + reportError(CodeTooComplex{}, location); } return borrow; @@ -3832,11 +3857,11 @@ TypeId ConstraintSolver::simplifyIntersection(NotNull scope, Location loc return ::Luau::simplifyIntersection(builtinTypes, arena, left, right).result; } -TypeId ConstraintSolver::simplifyIntersection(NotNull scope, Location location, std::set parts) +TypeId ConstraintSolver::simplifyIntersection(NotNull scope, Location location, TypeIds parts) { if (FFlag::DebugLuauEqSatSimplification) { - TypeId ty = arena->addType(IntersectionType{std::vector(parts.begin(), parts.end())}); + TypeId ty = arena->addType(IntersectionType{parts.take()}); std::optional res = eqSatSimplify(simplifier, ty); if (!res) @@ -3851,6 +3876,25 @@ TypeId ConstraintSolver::simplifyIntersection(NotNull scope, Location loc return ::Luau::simplifyIntersection(builtinTypes, arena, std::move(parts)).result; } +TypeId ConstraintSolver::simplifyIntersection_DEPRECATED(NotNull scope, Location location, std::set parts) +{ + if (FFlag::DebugLuauEqSatSimplification) + { + TypeId ty = arena->addType(IntersectionType{std::vector(parts.begin(), parts.end())}); + + std::optional res = eqSatSimplify(simplifier, ty); + if (!res) + return ty; + + for (TypeId ty : res->newTypeFunctions) + pushConstraint(scope, location, ReduceConstraint{ty}); + + return res->result; + } + else + return ::Luau::simplifyIntersection_DEPRECATED(builtinTypes, arena, std::move(parts)).result; +} + TypeId ConstraintSolver::simplifyUnion(NotNull scope, Location location, TypeId left, TypeId right) { if (FFlag::DebugLuauEqSatSimplification) diff --git a/Analysis/src/DataFlowGraph.cpp b/Analysis/src/DataFlowGraph.cpp index 9dbcfd85..567ae624 100644 --- a/Analysis/src/DataFlowGraph.cpp +++ b/Analysis/src/DataFlowGraph.cpp @@ -574,7 +574,7 @@ ControlFlow DataFlowGraphBuilder::visit(AstStatLocal* l) if (i < l->values.size) { AstExpr* e = l->values.data[i]; - if (const AstExprTable* tbl = e->as()) + if (e->is()) { def = defs[i]; } @@ -1165,7 +1165,7 @@ void DataFlowGraphBuilder::visitType(AstType* t) return visitType(f); else if (auto tyof = t->as()) return visitType(tyof); - else if (auto o = t->as()) + else if (t->is()) return; else if (auto u = t->as()) return visitType(u); @@ -1173,9 +1173,9 @@ void DataFlowGraphBuilder::visitType(AstType* t) return visitType(i); else if (auto e = t->as()) return visitType(e); - else if (auto s = t->as()) + else if (t->is()) return; // ok - else if (auto s = t->as()) + else if (t->is()) return; // ok else if (auto g = t->as()) return visitType(g->type); @@ -1243,7 +1243,7 @@ void DataFlowGraphBuilder::visitTypePack(AstTypePack* p) return visitTypePack(e); else if (auto v = p->as()) return visitTypePack(v); - else if (auto g = p->as()) + else if (p->is()) return; // ok else handle->ice("Unknown AstTypePack in DataFlowGraphBuilder::visitTypePack"); diff --git a/Analysis/src/EmbeddedBuiltinDefinitions.cpp b/Analysis/src/EmbeddedBuiltinDefinitions.cpp index 8bb1a4ca..4469a084 100644 --- a/Analysis/src/EmbeddedBuiltinDefinitions.cpp +++ b/Analysis/src/EmbeddedBuiltinDefinitions.cpp @@ -1,8 +1,7 @@ // 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(LuauTypeCheckerVectorLerp) -LUAU_FASTFLAGVARIABLE(LuauRawGetHandlesNil) +LUAU_FASTFLAGVARIABLE(LuauTypeCheckerVectorLerp2) namespace Luau { @@ -60,60 +59,6 @@ declare function unpack(tab: {V}, i: number?, j: number?): ...V )BUILTIN_SRC"; -// Will be removed when LuauRawGetHandlesNil flag gets clipped -static constexpr const char* kBuiltinDefinitionBaseSrc_DEPRECATED = R"BUILTIN_SRC( - -@checked declare function require(target: any): any - -@checked declare function getfenv(target: any): { [string]: any } - -declare _G: any -declare _VERSION: string - -declare function gcinfo(): number - -declare function print(...: T...) - -declare function type(value: T): string -declare function typeof(value: T): string - --- `assert` has a magic function attached that will give more detailed type information -declare function assert(value: T, errorMessage: string?): T -declare function error(message: T, level: number?): never - -declare function tostring(value: T): string -declare function tonumber(value: T, radix: number?): number? - -declare function rawequal(a: T1, b: T2): boolean -declare function rawget(tab: {[K]: V}, k: K): V -declare function rawset(tab: {[K]: V}, k: K, v: V): {[K]: V} -declare function rawlen(obj: {[K]: V} | string): number - -declare function setfenv(target: number | (T...) -> R..., env: {[string]: any}): ((T...) -> R...)? - -declare function ipairs(tab: {V}): (({V}, number) -> (number?, V), {V}, number) - -declare function pcall(f: (A...) -> R..., ...: A...): (boolean, R...) - --- FIXME: The actual type of `xpcall` is: --- (f: (A...) -> R1..., err: (E) -> R2..., A...) -> (true, R1...) | (false, R2...) --- Since we can't represent the return value, we use (boolean, R1...). -declare function xpcall(f: (A...) -> R1..., err: (E) -> R2..., ...: A...): (boolean, R1...) - --- `select` has a magic function attached to provide more detailed type information -declare function select(i: string | number, ...: A...): ...any - --- FIXME: This type is not entirely correct - `loadstring` returns a function or --- (nil, string). -declare function loadstring(src: string, chunkname: string?): (((A...) -> any)?, string?) - -@checked declare function newproxy(mt: boolean?): any - --- Cannot use `typeof` here because it will produce a polytype when we expect a monotype. -declare function unpack(tab: {V}, i: number?, j: number?): ...V - -)BUILTIN_SRC"; - static constexpr const char* kBuiltinDefinitionBit32Src = R"BUILTIN_SRC( declare bit32: { @@ -339,7 +284,7 @@ declare vector: { clamp: @checked (vec: vector, min: vector, max: vector) -> vector, max: @checked (vector, ...vector) -> vector, min: @checked (vector, ...vector) -> vector, - lerp: @checked (vec1: vector, vec2: vector, t: number) -> number, + lerp: @checked (vec1: vector, vec2: vector, t: number) -> vector, zero: vector, one: vector, @@ -379,7 +324,7 @@ declare vector: { std::string getBuiltinDefinitionSource() { - std::string result = FFlag::LuauRawGetHandlesNil ? kBuiltinDefinitionBaseSrc : kBuiltinDefinitionBaseSrc_DEPRECATED; + std::string result = kBuiltinDefinitionBaseSrc; result += kBuiltinDefinitionBit32Src; result += kBuiltinDefinitionMathSrc; @@ -389,7 +334,7 @@ std::string getBuiltinDefinitionSource() result += kBuiltinDefinitionDebugSrc; result += kBuiltinDefinitionUtf8Src; result += kBuiltinDefinitionBufferSrc; - if (FFlag::LuauTypeCheckerVectorLerp) + if (FFlag::LuauTypeCheckerVectorLerp2) { result += kBuiltinDefinitionVectorSrc; } diff --git a/Analysis/src/EqSatSimplification.cpp b/Analysis/src/EqSatSimplification.cpp index a933d3ab..beae70f0 100644 --- a/Analysis/src/EqSatSimplification.cpp +++ b/Analysis/src/EqSatSimplification.cpp @@ -286,7 +286,7 @@ Id toId( // First, handle types which do not contain other types. They obviously // cannot participate in cycles, so we don't have to check for that. - if (auto freeTy = get(ty)) + if (get(ty)) return egraph.add(TOpaque{ty}); else if (get(ty)) return egraph.add(TOpaque{ty}); @@ -765,7 +765,7 @@ TypeId fromId( return arena->addType(SingletonType{StringSingleton{strings.asString(s->value())}}); else if (auto fun = node.get()) return fun->value(); - else if (auto tbl = node.get()) + else if (node.get()) { TypeId res = arena->addType(BlockedType{}); seen[rootId] = res; @@ -1310,7 +1310,7 @@ void unionWithType(EGraph& egraph, CanonicalizedType& ct, Id part) if (!ct.functionPart) ct.functionParts.insert(part); } - else if (auto tclass = isTag(egraph, part)) + else if (isTag(egraph, part)) unionClasses(egraph, ct.classParts, part); else if (isTag(egraph, part)) { @@ -1415,7 +1415,7 @@ bool subtract(EGraph& egraph, CanonicalizedType& ct, Id part) ct.tablePart.reset(); else if (etype->get()) ct.classParts.clear(); - else if (auto tclass = etype->get()) + else if (etype->get()) { auto it = std::find(ct.classParts.begin(), ct.classParts.end(), part); if (it != ct.classParts.end()) diff --git a/Analysis/src/Error.cpp b/Analysis/src/Error.cpp index 762720fe..e53b82a1 100644 --- a/Analysis/src/Error.cpp +++ b/Analysis/src/Error.cpp @@ -18,9 +18,9 @@ LUAU_FASTINTVARIABLE(LuauIndentTypeMismatchMaxTypeLength, 10) -LUAU_FASTFLAG(LuauSolverAgnosticStringification) LUAU_FASTFLAGVARIABLE(LuauNewNonStrictReportsOneIndexedErrors) LUAU_FASTFLAG(LuauSubtypingReportGenericBoundMismatches2) +LUAU_FASTFLAGVARIABLE(LuauNewNonStrictBetterCheckedFunctionErrorMessage) static std::string wrongNumberOfArgsString( size_t expectedCount, @@ -408,17 +408,7 @@ struct ErrorConverter auto it = mtt->props.find("__call"); if (it != mtt->props.end()) { - if (FFlag::LuauSolverAgnosticStringification) - { - return it->second.readTy; - } - else - { - if (FFlag::LuauSolverV2) - return it->second.readTy; - else - return it->second.type_DEPRECATED(); - } + return it->second.readTy; } else return std::nullopt; @@ -708,7 +698,7 @@ struct ErrorConverter if (tfit->typeArguments.size() != 2) return "Type function instance " + Luau::toString(e.ty) + " is ill-formed, and thus invalid"; - if (auto errType = get(tfit->typeArguments[1])) // Second argument to (index | rawget)<_,_> is not a type + if (get(tfit->typeArguments[1])) // Second argument to (index | rawget)<_,_> is not a type return "Second argument to " + tfit->function->name + "<" + Luau::toString(tfit->typeArguments[0]) + ", _> is not a valid index type"; else // Property `indexer` does not exist on type `indexee` return "Property '" + Luau::toString(tfit->typeArguments[1]) + "' does not exist on type '" + Luau::toString(tfit->typeArguments[0]) + @@ -767,25 +757,37 @@ struct ErrorConverter std::string operator()(const CheckedFunctionCallError& e) const { - // TODO: What happens if checkedFunctionName cannot be found?? - if (FFlag::LuauNewNonStrictReportsOneIndexedErrors) + if (FFlag::LuauNewNonStrictBetterCheckedFunctionErrorMessage) + { + return "the function '" + e.checkedFunctionName + "' expects to get a " + toString(e.expected) + " as its " + + toHumanReadableIndex(e.argumentIndex) + " argument, but is being given a " + toString(e.passed) + ""; + } + else + { + // TODO: What happens if checkedFunctionName cannot be found?? return "Function '" + e.checkedFunctionName + "' expects '" + toString(e.expected) + "' at argument #" + std::to_string(e.argumentIndex + 1) + ", but got '" + Luau::toString(e.passed) + "'"; - else - return "Function '" + e.checkedFunctionName + "' expects '" + toString(e.expected) + "' at argument #" + std::to_string(e.argumentIndex) + - ", but got '" + Luau::toString(e.passed) + "'"; + } } std::string operator()(const NonStrictFunctionDefinitionError& e) const { - if (e.functionName.empty()) + if (FFlag::LuauNewNonStrictBetterCheckedFunctionErrorMessage) { - return "Argument " + e.argument + " with type '" + toString(e.argumentType) + "' is used in a way that will run time error"; + std::string prefix = e.functionName.empty() ? "" : "in the function '" + e.functionName + "', '"; + return prefix + "the argument '" + e.argument + "' is used in a way that will error at runtime"; } else { - return "Argument " + e.argument + " with type '" + toString(e.argumentType) + "' in function '" + e.functionName + - "' is used in a way that will run time error"; + if (e.functionName.empty()) + { + return "Argument " + e.argument + " with type '" + toString(e.argumentType) + "' is used in a way that will run time error"; + } + else + { + return "Argument " + e.argument + " with type '" + toString(e.argumentType) + "' in function '" + e.functionName + + "' is used in a way that will run time error"; + } } } @@ -806,8 +808,17 @@ struct ErrorConverter std::string operator()(const CheckedFunctionIncorrectArgs& e) const { - return "Checked Function " + e.functionName + " expects " + std::to_string(e.expected) + " arguments, but received " + - std::to_string(e.actual); + + if (FFlag::LuauNewNonStrictBetterCheckedFunctionErrorMessage) + { + return "the function '" + e.functionName + "' will error at runtime if it is not called with " + std::to_string(e.expected) + + " arguments, but we are calling it here with " + std::to_string(e.actual) + " arguments"; + } + else + { + return "Checked Function " + e.functionName + " expects " + std::to_string(e.expected) + " arguments, but received " + + std::to_string(e.actual); + } } std::string operator()(const UnexpectedTypeInSubtyping& e) const diff --git a/Analysis/src/FragmentAutocomplete.cpp b/Analysis/src/FragmentAutocomplete.cpp index 53f0696b..2f29cbce 100644 --- a/Analysis/src/FragmentAutocomplete.cpp +++ b/Analysis/src/FragmentAutocomplete.cpp @@ -33,10 +33,7 @@ LUAU_FASTINT(LuauTarjanChildLimit) LUAU_FASTFLAGVARIABLE(DebugLogFragmentsFromAutocomplete) LUAU_FASTFLAG(LuauUseWorkspacePropToChooseSolver) LUAU_FASTFLAGVARIABLE(LuauFragmentRequiresCanBeResolvedToAModule) -LUAU_FASTFLAGVARIABLE(LuauPopulateSelfTypesInFragment) -LUAU_FASTFLAGVARIABLE(LuauForInProvidesRecommendations) -LUAU_FASTFLAGVARIABLE(LuauFragmentAutocompleteTakesInnermostRefinement) -LUAU_FASTFLAG(LuauSuggestHotComments) +LUAU_FASTFLAGVARIABLE(LuauForInRangesConsiderInLocation) namespace Luau { @@ -120,7 +117,7 @@ Location getFragmentLocation(AstStat* nearestStatement, const Position& cursorPo { Location nonEmpty{nearestStatement->location.begin, cursorPosition}; // If your sibling is a do block, do nothing - if (auto doEnd = nearestStatement->as()) + if (nearestStatement->as()) return empty; // If you're inside the body of the function and this is your sibling, empty fragment @@ -157,27 +154,20 @@ Location getFragmentLocation(AstStat* nearestStatement, const Position& cursorPo if (auto forStat = nearestStatement->as()) { - - if (FFlag::LuauForInProvidesRecommendations) - { - if (forStat->step && forStat->step->location.containsClosed(cursorPosition)) - return {forStat->step->location.begin, cursorPosition}; - if (forStat->to && forStat->to->location.containsClosed(cursorPosition)) - return {forStat->to->location.begin, cursorPosition}; - if (forStat->from && forStat->from->location.containsClosed(cursorPosition)) - return {forStat->from->location.begin, cursorPosition}; - } + if (forStat->step && forStat->step->location.containsClosed(cursorPosition)) + return {forStat->step->location.begin, cursorPosition}; + if (forStat->to && forStat->to->location.containsClosed(cursorPosition)) + return {forStat->to->location.begin, cursorPosition}; + if (forStat->from && forStat->from->location.containsClosed(cursorPosition)) + return {forStat->from->location.begin, cursorPosition}; if (!forStat->hasDo) return nonEmpty; else { - if (FFlag::LuauForInProvidesRecommendations) - { auto completeableExtents = Location{forStat->location.begin, forStat->doLocation.begin}; if (completeableExtents.containsClosed(cursorPosition)) return nonEmpty; - } return empty; } @@ -189,12 +179,15 @@ Location getFragmentLocation(AstStat* nearestStatement, const Position& cursorPo return nonEmpty; else { - if (FFlag::LuauForInProvidesRecommendations) + auto completeableExtents = Location{forIn->location.begin, forIn->doLocation.begin}; + if (completeableExtents.containsClosed(cursorPosition)) { - auto completeableExtents = Location{forIn->location.begin, forIn->doLocation.begin}; - if (completeableExtents.containsClosed(cursorPosition)) + if (!forIn->hasIn) + return nonEmpty; + else { - if (!forIn->hasIn) + // [for ... in ... do] - the cursor can either be between [for ... in] or [in ... do] + if (FFlag::LuauForInRangesConsiderInLocation && cursorPosition < forIn->inLocation.begin) return nonEmpty; else return Location{forIn->inLocation.begin, cursorPosition}; @@ -424,13 +417,10 @@ FragmentAutocompleteAncestryResult findAncestryForFragmentParse(AstStatBlock* st { if (globFun->location.contains(cursorPos)) { - if (FFlag::LuauPopulateSelfTypesInFragment) + if (auto local = globFun->func->self) { - if (auto local = globFun->func->self) - { - localStack.push_back(local); - localMap[local->name] = local; - } + localStack.push_back(local); + localMap[local->name] = local; } for (AstLocal* loc : globFun->func->args) @@ -701,8 +691,7 @@ void cloneTypesFromFragment( // end // // We could find another binding for `syms` and then set _that_. - if (FFlag::LuauFragmentAutocompleteTakesInnermostRefinement) - break; + break; } } } @@ -1479,61 +1468,31 @@ FragmentAutocompleteStatusResult tryFragmentAutocomplete( StringCompletionCallback stringCompletionCB ) { - if (FFlag::LuauSuggestHotComments) + bool isInHotComment = isWithinHotComment(context.freshParse.hotcomments, cursorPosition); + if (isWithinComment(context.freshParse.commentLocations, cursorPosition) && !isInHotComment) + return {FragmentAutocompleteStatus::Success, std::nullopt}; + // TODO: we should calculate fragmentEnd position here, by using context.newAstRoot and cursorPosition + try { - bool isInHotComment = isWithinHotComment(context.freshParse.hotcomments, cursorPosition); - if (isWithinComment(context.freshParse.commentLocations, cursorPosition) && !isInHotComment) - return {FragmentAutocompleteStatus::Success, std::nullopt}; - // TODO: we should calculate fragmentEnd position here, by using context.newAstRoot and cursorPosition - try - { - Luau::FragmentAutocompleteResult fragmentAutocomplete = Luau::fragmentAutocomplete( - frontend, - context.newSrc, - moduleName, - cursorPosition, - context.opts, - std::move(stringCompletionCB), - context.DEPRECATED_fragmentEndPosition, - context.freshParse.root, - context.reporter, - isInHotComment - ); - return {FragmentAutocompleteStatus::Success, std::move(fragmentAutocomplete)}; - } - catch (const Luau::InternalCompilerError& e) - { - if (FFlag::DebugLogFragmentsFromAutocomplete) - logLuau("tryFragmentAutocomplete exception", e.what()); - return {FragmentAutocompleteStatus::InternalIce, std::nullopt}; - } + Luau::FragmentAutocompleteResult fragmentAutocomplete = Luau::fragmentAutocomplete( + frontend, + context.newSrc, + moduleName, + cursorPosition, + context.opts, + std::move(stringCompletionCB), + context.DEPRECATED_fragmentEndPosition, + context.freshParse.root, + context.reporter, + isInHotComment + ); + return {FragmentAutocompleteStatus::Success, std::move(fragmentAutocomplete)}; } - else + catch (const Luau::InternalCompilerError& e) { - if (isWithinComment(context.freshParse.commentLocations, cursorPosition)) - return {FragmentAutocompleteStatus::Success, std::nullopt}; - // TODO: we should calculate fragmentEnd position here, by using context.newAstRoot and cursorPosition - try - { - Luau::FragmentAutocompleteResult fragmentAutocomplete = Luau::fragmentAutocomplete( - frontend, - context.newSrc, - moduleName, - cursorPosition, - context.opts, - std::move(stringCompletionCB), - context.DEPRECATED_fragmentEndPosition, - context.freshParse.root, - context.reporter - ); - return {FragmentAutocompleteStatus::Success, std::move(fragmentAutocomplete)}; - } - catch (const Luau::InternalCompilerError& e) - { - if (FFlag::DebugLogFragmentsFromAutocomplete) - logLuau("tryFragmentAutocomplete exception", e.what()); - return {FragmentAutocompleteStatus::InternalIce, std::nullopt}; - } + if (FFlag::DebugLogFragmentsFromAutocomplete) + logLuau("tryFragmentAutocomplete exception", e.what()); + return {FragmentAutocompleteStatus::InternalIce, std::nullopt}; } } @@ -1572,7 +1531,7 @@ FragmentAutocompleteResult fragmentAutocomplete( cursorPosition, frontend.fileResolver, std::move(callback), - FFlag::LuauSuggestHotComments && isInHotComment + isInHotComment ); freeze(tcResult.incrementalModule->internalTypes); reportWaypoint(reporter, FragmentAutocompleteWaypoint::AutocompleteEnd); diff --git a/Analysis/src/Frontend.cpp b/Analysis/src/Frontend.cpp index 5d48801b..c2f273a2 100644 --- a/Analysis/src/Frontend.cpp +++ b/Analysis/src/Frontend.cpp @@ -42,10 +42,11 @@ LUAU_FASTFLAGVARIABLE(DebugLuauForceStrictMode) LUAU_FASTFLAGVARIABLE(DebugLuauForceNonStrictMode) LUAU_FASTFLAGVARIABLE(LuauUseWorkspacePropToChooseSolver) LUAU_FASTFLAGVARIABLE(DebugLuauAlwaysShowConstraintSolvingIncomplete) -LUAU_FASTFLAG(LuauLimitDynamicConstraintSolving3) LUAU_FASTFLAG(LuauEmplaceNotPushBack) LUAU_FASTFLAG(LuauExplicitSkipBoundTypes) LUAU_FASTFLAG(LuauNoConstraintGenRecursionLimitIce) +LUAU_FASTFLAGVARIABLE(LuauBatchedExecuteTask) +LUAU_FASTFLAGVARIABLE(LuauAccumulateErrorsInOrder) namespace Luau { @@ -77,7 +78,8 @@ struct BuildQueueItem struct BuildQueueWorkState { - std::function task)> executeTask; + std::function task)> executeTask_DEPRECATED; + std::function> tasks)> executeTasks; std::vector buildQueueItems; @@ -284,18 +286,39 @@ ErrorVec accumulateErrors( Module& module = *modulePtr; - std::sort( - module.errors.begin(), - module.errors.end(), - [](const TypeError& e1, const TypeError& e2) -> bool - { - return e1.location.begin > e2.location.begin; - } - ); + if (FFlag::LuauAccumulateErrorsInOrder) + { + size_t prevSize = result.size(); + + // Append module errors in reverse order + result.insert(result.end(), module.errors.rbegin(), module.errors.rend()); + + // Sort them in the reverse order as well + std::stable_sort( + result.begin() + prevSize, + result.end(), + [](const TypeError& e1, const TypeError& e2) -> bool + { + return e1.location.begin > e2.location.begin; + } + ); + } + else + { + std::sort( + module.errors.begin(), + module.errors.end(), + [](const TypeError& e1, const TypeError& e2) -> bool + { + return e1.location.begin > e2.location.begin; + } + ); - result.insert(result.end(), module.errors.begin(), module.errors.end()); + result.insert(result.end(), module.errors.begin(), module.errors.end()); + } } + // Now we reverse errors from all modules and since they were inserted and sorted in reverse, it should be in order std::reverse(result.begin(), result.end()); return result; @@ -547,7 +570,7 @@ void Frontend::queueModuleCheck(const ModuleName& name) moduleQueue.push_back(name); } -std::vector Frontend::checkQueuedModules( +std::vector Frontend::checkQueuedModules_DEPRECATED( std::optional optionOverride, std::function task)> executeTask, std::function progress @@ -611,7 +634,7 @@ std::vector Frontend::checkQueuedModules( }; } - state->executeTask = executeTask; + state->executeTask_DEPRECATED = executeTask; state->remaining = state->buildQueueItems.size(); // Record dependencies between modules @@ -637,7 +660,7 @@ std::vector Frontend::checkQueuedModules( for (size_t i = 0; i < state->buildQueueItems.size(); i++) { if (state->buildQueueItems[i].dirtyDependencies == 0) - sendQueueItemTask(state, i); + sendQueueItemTask_DEPRECATED(state, i); } // If not a single item was found, a cycle in the graph was hit @@ -709,7 +732,7 @@ std::vector Frontend::checkQueuedModules( // Items cannot be submitted while holding the lock for (size_t i : nextItems) - sendQueueItemTask(state, i); + sendQueueItemTask_DEPRECATED(state, i); nextItems.clear(); if (state->processing == 0) @@ -737,6 +760,206 @@ std::vector Frontend::checkQueuedModules( return checkedModules; } +std::vector Frontend::checkQueuedModules( + std::optional optionOverride, + std::function> tasks)> executeTasks, + std::function progress +) +{ + FrontendOptions frontendOptions = optionOverride.value_or(options); + if (getLuauSolverMode() == SolverMode::New) + frontendOptions.forAutocomplete = false; + + // By taking data into locals, we make sure queue is cleared at the end, even if an ICE or a different exception is thrown + std::vector currModuleQueue; + std::swap(currModuleQueue, moduleQueue); + + DenseHashSet seen{{}}; + + std::shared_ptr state = std::make_shared(); + + for (const ModuleName& name : currModuleQueue) + { + if (seen.contains(name)) + continue; + + if (!isDirty(name, frontendOptions.forAutocomplete)) + { + seen.insert(name); + continue; + } + + std::vector queue; + bool cycleDetected = parseGraph( + queue, + name, + frontendOptions.forAutocomplete, + [&seen](const ModuleName& name) + { + return seen.contains(name); + } + ); + + addBuildQueueItems(state->buildQueueItems, queue, cycleDetected, seen, frontendOptions); + } + + if (state->buildQueueItems.empty()) + return {}; + + // We need a mapping from modules to build queue slots + std::unordered_map moduleNameToQueue; + + for (size_t i = 0; i < state->buildQueueItems.size(); i++) + { + BuildQueueItem& item = state->buildQueueItems[i]; + moduleNameToQueue[item.name] = i; + } + + // Default task execution is single-threaded and immediate + if (!executeTasks) + { + executeTasks = [](std::vector> tasks) + { + for (auto& task : tasks) + task(); + }; + } + + state->executeTasks = executeTasks; + state->remaining = state->buildQueueItems.size(); + + // Record dependencies between modules + for (size_t i = 0; i < state->buildQueueItems.size(); i++) + { + BuildQueueItem& item = state->buildQueueItems[i]; + + for (const ModuleName& dep : item.sourceNode->requireSet) + { + if (auto it = sourceNodes.find(dep); it != sourceNodes.end()) + { + if (it->second->hasDirtyModule(frontendOptions.forAutocomplete)) + { + item.dirtyDependencies++; + + state->buildQueueItems[moduleNameToQueue[dep]].reverseDeps.push_back(i); + } + } + } + } + + std::vector nextItems; + + // In the first pass, check all modules with no pending dependencies + for (size_t i = 0; i < state->buildQueueItems.size(); i++) + { + if (state->buildQueueItems[i].dirtyDependencies == 0) + nextItems.push_back(i); + } + + if (!nextItems.empty()) + { + sendQueueItemTasks(state, nextItems); + nextItems.clear(); + } + + // If not a single item was found, a cycle in the graph was hit + if (state->processing == 0) + sendQueueCycleItemTask(state); + + std::optional itemWithException; + bool cancelled = false; + + while (state->remaining != 0) + { + { + std::unique_lock guard(state->mtx); + + // If nothing is ready yet, wait + state->cv.wait( + guard, + [state] + { + return !state->readyQueueItems.empty(); + } + ); + + // Handle checked items + for (size_t i : state->readyQueueItems) + { + const BuildQueueItem& item = state->buildQueueItems[i]; + + // If exception was thrown, stop adding new items and wait for processing items to complete + if (item.exception) + itemWithException = i; + + if (item.module && item.module->cancelled) + cancelled = true; + + if (itemWithException || cancelled) + break; + + recordItemResult(item); + + // Notify items that were waiting for this dependency + for (size_t reverseDep : item.reverseDeps) + { + BuildQueueItem& reverseDepItem = state->buildQueueItems[reverseDep]; + + LUAU_ASSERT(reverseDepItem.dirtyDependencies != 0); + reverseDepItem.dirtyDependencies--; + + // In case of a module cycle earlier, check if unlocked an item that was already processed + if (!reverseDepItem.processing && reverseDepItem.dirtyDependencies == 0) + nextItems.push_back(reverseDep); + } + } + + LUAU_ASSERT(state->processing >= state->readyQueueItems.size()); + state->processing -= state->readyQueueItems.size(); + + LUAU_ASSERT(state->remaining >= state->readyQueueItems.size()); + state->remaining -= state->readyQueueItems.size(); + state->readyQueueItems.clear(); + } + + if (progress) + { + if (!progress(state->buildQueueItems.size() - state->remaining, state->buildQueueItems.size())) + cancelled = true; + } + + // Items cannot be submitted while holding the lock + if (!nextItems.empty()) + { + sendQueueItemTasks(state, nextItems); + nextItems.clear(); + } + + if (state->processing == 0) + { + // Typechecking might have been cancelled by user, don't return partial results + if (cancelled) + return {}; + + // We might have stopped because of a pending exception + if (itemWithException) + recordItemResult(state->buildQueueItems[*itemWithException]); + } + + // If we aren't done, but don't have anything processing, we hit a cycle + if (state->remaining != 0 && state->processing == 0) + sendQueueCycleItemTask(state); + } + + std::vector checkedModules; + checkedModules.reserve(state->buildQueueItems.size()); + + for (size_t i = 0; i < state->buildQueueItems.size(); i++) + checkedModules.push_back(std::move(state->buildQueueItems[i].name)); + + return checkedModules; +} + std::optional Frontend::getCheckResult(const ModuleName& name, bool accumulateNested, bool forAutocomplete) { if (getLuauSolverMode() == SolverMode::New) @@ -1228,7 +1451,7 @@ void Frontend::performQueueItemTask(std::shared_ptr state, state->cv.notify_one(); } -void Frontend::sendQueueItemTask(std::shared_ptr state, size_t itemPos) +void Frontend::sendQueueItemTask_DEPRECATED(std::shared_ptr state, size_t itemPos) { BuildQueueItem& item = state->buildQueueItems[itemPos]; @@ -1237,7 +1460,7 @@ void Frontend::sendQueueItemTask(std::shared_ptr state, siz state->processing++; - state->executeTask( + state->executeTask_DEPRECATED( [this, state, itemPos]() { performQueueItemTask(state, itemPos); @@ -1245,6 +1468,30 @@ void Frontend::sendQueueItemTask(std::shared_ptr state, siz ); } +void Frontend::sendQueueItemTasks(std::shared_ptr state, const std::vector& items) +{ + std::vector> tasks; + tasks.reserve(items.size()); + + for (size_t itemPos : items) + { + BuildQueueItem& item = state->buildQueueItems[itemPos]; + + LUAU_ASSERT(!item.processing); + item.processing = true; + + tasks.emplace_back( + [this, state, itemPos]() + { + performQueueItemTask(state, itemPos); + } + ); + } + + state->processing += items.size(); + state->executeTasks(std::move(tasks)); +} + void Frontend::sendQueueCycleItemTask(std::shared_ptr state) { for (size_t i = 0; i < state->buildQueueItems.size(); i++) @@ -1253,7 +1500,10 @@ void Frontend::sendQueueCycleItemTask(std::shared_ptr state if (!item.processing) { - sendQueueItemTask(std::move(state), i); + if (FFlag::LuauBatchedExecuteTask) + sendQueueItemTasks(std::move(state), {i}); + else + sendQueueItemTask_DEPRECATED(std::move(state), i); break; } } @@ -1622,37 +1872,34 @@ ModulePtr check( // NOTE: This used to be done prior to cloning the public interface, but // we now replace "internal" types with `*error-type*`. - if (FFlag::LuauLimitDynamicConstraintSolving3) + if (FFlag::DebugLuauForbidInternalTypes) { - if (FFlag::DebugLuauForbidInternalTypes) - { - InternalTypeFinder finder; + InternalTypeFinder finder; - // `result->returnType` is not filled in yet, so we - // traverse the return type of the root module. - finder.traverse(module->getModuleScope()->returnType); + // `result->returnType` is not filled in yet, so we + // traverse the return type of the root module. + finder.traverse(module->getModuleScope()->returnType); - for (const auto& [_, binding] : module->exportedTypeBindings) - finder.traverse(binding.type); + for (const auto& [_, binding] : module->exportedTypeBindings) + finder.traverse(binding.type); - for (const auto& [_, ty] : module->astTypes) - finder.traverse(ty); + for (const auto& [_, ty] : module->astTypes) + finder.traverse(ty); - for (const auto& [_, ty] : module->astExpectedTypes) - finder.traverse(ty); + for (const auto& [_, ty] : module->astExpectedTypes) + finder.traverse(ty); - for (const auto& [_, tp] : module->astTypePacks) - finder.traverse(tp); + for (const auto& [_, tp] : module->astTypePacks) + finder.traverse(tp); - for (const auto& [_, ty] : module->astResolvedTypes) - finder.traverse(ty); + for (const auto& [_, ty] : module->astResolvedTypes) + finder.traverse(ty); - for (const auto& [_, ty] : module->astOverloadResolvedTypes) - finder.traverse(ty); + for (const auto& [_, ty] : module->astOverloadResolvedTypes) + finder.traverse(ty); - for (const auto& [_, tp] : module->astResolvedTypePacks) - finder.traverse(tp); - } + for (const auto& [_, tp] : module->astResolvedTypePacks) + finder.traverse(tp); } @@ -1662,37 +1909,6 @@ ModulePtr check( else module->clonePublicInterface_DEPRECATED(builtinTypes, *iceHandler); - if (!FFlag::LuauLimitDynamicConstraintSolving3) - { - if (FFlag::DebugLuauForbidInternalTypes) - { - InternalTypeFinder finder; - - finder.traverse(module->returnType); - - for (const auto& [_, binding] : module->exportedTypeBindings) - finder.traverse(binding.type); - - for (const auto& [_, ty] : module->astTypes) - finder.traverse(ty); - - for (const auto& [_, ty] : module->astExpectedTypes) - finder.traverse(ty); - - for (const auto& [_, tp] : module->astTypePacks) - finder.traverse(tp); - - for (const auto& [_, ty] : module->astResolvedTypes) - finder.traverse(ty); - - for (const auto& [_, ty] : module->astOverloadResolvedTypes) - finder.traverse(ty); - - for (const auto& [_, tp] : module->astResolvedTypePacks) - finder.traverse(tp); - } - } - // It would be nice if we could freeze the arenas before doing type // checking, but we'll have to do some work to get there. // diff --git a/Analysis/src/Instantiation.cpp b/Analysis/src/Instantiation.cpp index 77b5fd63..a6af85aa 100644 --- a/Analysis/src/Instantiation.cpp +++ b/Analysis/src/Instantiation.cpp @@ -11,7 +11,6 @@ #include LUAU_FASTFLAG(LuauSolverV2) -LUAU_FASTFLAG(LuauParametrizedAttributeSyntax) namespace Luau { @@ -65,11 +64,8 @@ TypeId Instantiation::clean(TypeId ty) clone.magic = ftv->magic; clone.tags = ftv->tags; clone.argNames = ftv->argNames; - if (FFlag::LuauParametrizedAttributeSyntax) - { - clone.isDeprecatedFunction = ftv->isDeprecatedFunction; - clone.deprecatedInfo = ftv->deprecatedInfo; - } + clone.isDeprecatedFunction = ftv->isDeprecatedFunction; + clone.deprecatedInfo = ftv->deprecatedInfo; TypeId result = addType(std::move(clone)); // Annoyingly, we have to do this even if there are no generics, diff --git a/Analysis/src/LValue.cpp b/Analysis/src/LValue.cpp index 38dfe1ae..daf73ea7 100644 --- a/Analysis/src/LValue.cpp +++ b/Analysis/src/LValue.cpp @@ -80,7 +80,7 @@ std::optional tryGetLValue(const AstExpr& node) Symbol getBaseSymbol(const LValue& lvalue) { const LValue* current = &lvalue; - while (auto field = get(*current)) + while (get(*current)) current = baseof(*current); const Symbol* symbol = get(*current); diff --git a/Analysis/src/Linter.cpp b/Analysis/src/Linter.cpp index 5742c09a..16649a96 100644 --- a/Analysis/src/Linter.cpp +++ b/Analysis/src/Linter.cpp @@ -15,7 +15,6 @@ LUAU_FASTINTVARIABLE(LuauSuggestionDistance, 4) LUAU_FASTFLAG(LuauSolverV2) -LUAU_FASTFLAG(LuauParametrizedAttributeSyntax) namespace Luau { @@ -2295,7 +2294,7 @@ class LintDeprecatedApi : AstVisitor if (shouldReport) { - if (FFlag::LuauParametrizedAttributeSyntax && fty->deprecatedInfo != nullptr) + if (fty->deprecatedInfo != nullptr) { report(node->location, node->local->name.value, *fty->deprecatedInfo); } @@ -2315,7 +2314,7 @@ class LintDeprecatedApi : AstVisitor if (shouldReport) { - if (FFlag::LuauParametrizedAttributeSyntax && fty->deprecatedInfo != nullptr) + if (fty->deprecatedInfo != nullptr) { report(node->location, node->name.value, *fty->deprecatedInfo); } @@ -2399,7 +2398,7 @@ class LintDeprecatedApi : AstVisitor className = global->name.value; const char* functionName = node->index.value; - if (FFlag::LuauParametrizedAttributeSyntax && fty->deprecatedInfo != nullptr) + if (fty->deprecatedInfo != nullptr) { report(node->location, className, functionName, *fty->deprecatedInfo); } @@ -2440,7 +2439,7 @@ class LintDeprecatedApi : AstVisitor const char* functionName = node->index.value; - if (FFlag::LuauParametrizedAttributeSyntax && fty->deprecatedInfo != nullptr) + if (fty->deprecatedInfo != nullptr) { report(node->location, className, functionName, *fty->deprecatedInfo); } diff --git a/Analysis/src/Module.cpp b/Analysis/src/Module.cpp index 3638f997..01fa02a4 100644 --- a/Analysis/src/Module.cpp +++ b/Analysis/src/Module.cpp @@ -16,9 +16,7 @@ LUAU_FASTFLAG(LuauSolverV2); LUAU_FASTFLAG(LuauUseWorkspacePropToChooseSolver) -LUAU_FASTFLAG(LuauLimitDynamicConstraintSolving3) LUAU_FASTFLAGVARIABLE(LuauEmplaceNotPushBack) -LUAU_FASTFLAG(LuauSuggestHotComments) namespace Luau { @@ -203,41 +201,20 @@ struct ClonePublicInterface : Substitution if (isNewSolver()) { ttv->scope = nullptr; - if (FFlag::LuauLimitDynamicConstraintSolving3) - ttv->state = TableState::Sealed; + ttv->state = TableState::Sealed; } } if (isNewSolver()) { - if (FFlag::LuauLimitDynamicConstraintSolving3) + if (is(ty)) { - if (is(ty)) - { - internalTypeEscaped = true; - result = builtinTypes->errorType; - } - else if (auto genericty = getMutable(result)) - { - genericty->scope = nullptr; - } + internalTypeEscaped = true; + result = builtinTypes->errorType; } - else + else if (auto genericty = getMutable(result)) { - if (auto freety = getMutable(result)) - { - module->errors.emplace_back( - freety->scope->location, - module->name, - InternalError{"Free type is escaping its module; please report this bug at " - "https://github.com/luau-lang/luau/issues"} - ); - result = builtinTypes->errorType; - } - else if (auto genericty = getMutable(result)) - { - genericty->scope = nullptr; - } + genericty->scope = nullptr; } } @@ -248,32 +225,14 @@ struct ClonePublicInterface : Substitution { if (isNewSolver()) { - if (FFlag::LuauLimitDynamicConstraintSolving3) + if (is(tp)) { - if (is(tp)) - { - internalTypeEscaped = true; - return builtinTypes->errorTypePack; - } - - auto clonedTp = clone(tp); - if (auto gtp = getMutable(clonedTp)) - gtp->scope = nullptr; - return clonedTp; + internalTypeEscaped = true; + return builtinTypes->errorTypePack; } auto clonedTp = clone(tp); - if (auto ftp = getMutable(clonedTp)) - { - module->errors.emplace_back( - ftp->scope->location, - module->name, - InternalError{"Free type pack is escaping its module; please report this bug at " - "https://github.com/luau-lang/luau/issues"} - ); - clonedTp = builtinTypes->errorTypePack; - } - else if (auto gtp = getMutable(clonedTp)) + if (auto gtp = getMutable(clonedTp)) gtp->scope = nullptr; return clonedTp; } @@ -393,7 +352,7 @@ void Module::clonePublicInterface_DEPRECATED(NotNull builtinTypes, *tf = clonePublicInterface.cloneTypeFun(*tf); } - if (FFlag::LuauLimitDynamicConstraintSolving3 && clonePublicInterface.internalTypeEscaped) + if (clonePublicInterface.internalTypeEscaped) { errors.emplace_back( Location{}, // Not amazing but the best we can do. @@ -444,7 +403,7 @@ void Module::clonePublicInterface(NotNull builtinTypes, InternalEr *tf = clonePublicInterface.cloneTypeFun(*tf); } - if (FFlag::LuauLimitDynamicConstraintSolving3 && clonePublicInterface.internalTypeEscaped) + if (clonePublicInterface.internalTypeEscaped) { errors.emplace_back( Location{}, // Not amazing but the best we can do. diff --git a/Analysis/src/NativeStackGuard.cpp b/Analysis/src/NativeStackGuard.cpp new file mode 100644 index 00000000..3bfc40d3 --- /dev/null +++ b/Analysis/src/NativeStackGuard.cpp @@ -0,0 +1,123 @@ +// This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details + +#include "Luau/NativeStackGuard.h" +#include "Luau/Common.h" +#include + +LUAU_FASTFLAGVARIABLE(LuauUseNativeStackGuard); + +// The minimum number of available bytes in the stack's address space. +// If we have less, we want to fail and exit rather than risking an overflow. +LUAU_FASTINTVARIABLE(LuauStackGuardThreshold, 1024); + +#if defined(_MSC_VER) + +#define WIN32_LEAN_AND_MEAN +#include +#include + +namespace Luau +{ + +NativeStackGuard::NativeStackGuard() + : high(0) + , low(0) +{ + if (!FFlag::LuauUseNativeStackGuard) + return; + + GetCurrentThreadStackLimits((PULONG_PTR)&low, (PULONG_PTR)&high); +} + +bool NativeStackGuard::isOk() const +{ + if (!FFlag::LuauUseNativeStackGuard || FInt::LuauStackGuardThreshold <= 0) + return true; + + const uintptr_t sp = uintptr_t(_AddressOfReturnAddress()); + + const uintptr_t remaining = sp - low; + + return remaining > uintptr_t(FInt::LuauStackGuardThreshold); +} + +uintptr_t getStackAddressSpaceSize() +{ + uintptr_t low = 0; + uintptr_t high = 0; + GetCurrentThreadStackLimits((PULONG_PTR)&low, (PULONG_PTR)&high); + + return high - low; +} + +} + +#elif defined(__APPLE__) + +#include + +namespace Luau +{ + +NativeStackGuard::NativeStackGuard() + : high(0) + , low(0) +{ + if (!FFlag::LuauUseNativeStackGuard) + return; + + pthread_t self = pthread_self(); + char* addr = static_cast(pthread_get_stackaddr_np(self)); + size_t size = pthread_get_stacksize_np(self); + + low = uintptr_t(addr - size); + high = uintptr_t(addr); +} + +bool NativeStackGuard::isOk() const +{ + if (!FFlag::LuauUseNativeStackGuard || FInt::LuauStackGuardThreshold <= 0) + return true; + + const uintptr_t sp = uintptr_t(__builtin_frame_address(0)); + + const uintptr_t remaining = sp - low; + + return remaining > uintptr_t(FInt::LuauStackGuardThreshold); +} + +uintptr_t getStackAddressSpaceSize() +{ + pthread_t self = pthread_self(); + return pthread_get_stacksize_np(self); +} + +} + +#else + + +namespace Luau +{ + +NativeStackGuard::NativeStackGuard() + : high(0) + , low(0) +{ + (void)low; + (void)high; +} + +bool NativeStackGuard::isOk() const +{ + return true; +} + +uintptr_t getStackAddressSpaceSize() +{ + return 0; +} + +} + +#endif diff --git a/Analysis/src/NonStrictTypeChecker.cpp b/Analysis/src/NonStrictTypeChecker.cpp index 66c0a5d6..a09b28c0 100644 --- a/Analysis/src/NonStrictTypeChecker.cpp +++ b/Analysis/src/NonStrictTypeChecker.cpp @@ -20,11 +20,9 @@ LUAU_FASTFLAG(DebugLuauMagicTypes) -LUAU_FASTFLAGVARIABLE(LuauNewNonStrictMoreUnknownSymbols) -LUAU_FASTFLAGVARIABLE(LuauNewNonStrictNoErrorsPassingNever) -LUAU_FASTFLAGVARIABLE(LuauNewNonStrictSuppressesDynamicRequireErrors) LUAU_FASTFLAG(LuauEmplaceNotPushBack) LUAU_FASTFLAGVARIABLE(LuauUnreducedTypeFunctionsDontTriggerWarnings) +LUAU_FASTFLAGVARIABLE(LuauNonStrictFetchScopeOnce) namespace Luau { @@ -350,24 +348,11 @@ struct NonStrictTypeChecker NonStrictContext condB = visit(ifStatement->condition, ValueContext::RValue); NonStrictContext branchContext; - if (FFlag::LuauNewNonStrictMoreUnknownSymbols) + NonStrictContext thenBody = visit(ifStatement->thenbody); + if (ifStatement->elsebody) { - NonStrictContext thenBody = visit(ifStatement->thenbody); - if (ifStatement->elsebody) - { - NonStrictContext elseBody = visit(ifStatement->elsebody); - branchContext = NonStrictContext::conjunction(builtinTypes, arena, thenBody, elseBody); - } - } - else - { - // If there is no else branch, don't bother generating warnings for the then branch - we can't prove there is an error - if (ifStatement->elsebody) - { - NonStrictContext thenBody = visit(ifStatement->thenbody); - NonStrictContext elseBody = visit(ifStatement->elsebody); - branchContext = NonStrictContext::conjunction(builtinTypes, arena, thenBody, elseBody); - } + NonStrictContext elseBody = visit(ifStatement->elsebody); + branchContext = NonStrictContext::conjunction(builtinTypes, arena, thenBody, elseBody); } return NonStrictContext::disjunction(builtinTypes, arena, condB, branchContext); @@ -624,12 +609,9 @@ struct NonStrictTypeChecker NonStrictContext visit(AstExprCall* call) { - if (FFlag::LuauNewNonStrictMoreUnknownSymbols) - { - visit(call->func, ValueContext::RValue); - for (auto arg : call->args) - visit(arg, ValueContext::RValue); - } + visit(call->func, ValueContext::RValue); + for (auto arg : call->args) + visit(arg, ValueContext::RValue); NonStrictContext fresh{}; TypeId* originalCallTy = module->astOriginalCallTypes.find(call->func); @@ -713,22 +695,38 @@ struct NonStrictTypeChecker } // Populate the context and now iterate through each of the arguments to the call to find out if we satisfy the types - for (size_t i = 0; i < arguments.size(); i++) + if (FFlag::LuauNonStrictFetchScopeOnce) { - AstExpr* arg = arguments[i]; - if (auto runTimeFailureType = willRunTimeError(arg, fresh)) + NotNull scope{findInnermostScope(call->location)}; + for (size_t i = 0; i < arguments.size(); i++) { - if (FFlag::LuauUnreducedTypeFunctionsDontTriggerWarnings) - reportError(CheckedFunctionCallError{argTypes[i], *runTimeFailureType, functionName, i}, arg->location); - else + AstExpr* arg = arguments[i]; + if (auto runTimeFailureType = willRunTimeError(arg, fresh, scope)) { - if (FFlag::LuauNewNonStrictNoErrorsPassingNever) + 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 + } + } + } + else + { + for (size_t i = 0; i < arguments.size(); i++) + { + 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 (!get(follow(*runTimeFailureType))) + reportError(CheckedFunctionCallError{argTypes[i], *runTimeFailureType, functionName, i}, arg->location); + } } } } @@ -770,18 +768,35 @@ 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); - for (AstLocal* local : exprFn->args) + if (FFlag::LuauNonStrictFetchScopeOnce) { - if (std::optional ty = willRunTimeErrorFunctionDefinition(local, remainder)) + auto scope = pusher ? pusher->scope : NotNull{module->getModuleScope().get()}; + for (AstLocal* local : exprFn->args) { - const char* debugname = exprFn->debugname.value; - reportError(NonStrictFunctionDefinitionError{debugname ? debugname : "", local->name.value, *ty}, local->location); - } - remainder.remove(dfg->getDef(local)); + 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); + visit(local->annotation); + } } + 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)); + visit(local->annotation); + } + } visitGenerics(exprFn->generics, exprFn->genericPacks); visit(exprFn->returnAnnotation); @@ -1180,8 +1195,33 @@ struct NonStrictTypeChecker // TODO: weave in logger here? } + std::optional willRunTimeError(AstExpr* fragment, const NonStrictContext& context, NotNull scope) + { + 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 {}; + } + // If this fragment of the ast will run time error, return the type that causes this - std::optional willRunTimeError(AstExpr* fragment, const NonStrictContext& context) + // 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); @@ -1206,7 +1246,29 @@ struct NonStrictTypeChecker return {}; } - std::optional willRunTimeErrorFunctionDefinition(AstLocal* fragment, const NonStrictContext& context) + std::optional willRunTimeErrorFunctionDefinition(AstLocal* fragment, NotNull scope, const NonStrictContext& context) + { + 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 {}; + } + + // 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); @@ -1264,21 +1326,18 @@ void checkNonStrict( typeChecker.visit(sourceModule.root); unfreeze(module->interfaceTypes); copyErrors(module->errors, module->interfaceTypes, builtinTypes); - - if (FFlag::LuauNewNonStrictSuppressesDynamicRequireErrors) - { - module->errors.erase( - std::remove_if( - module->errors.begin(), - module->errors.end(), - [](auto err) - { - return get(err) != nullptr; - } + + module->errors.erase( + std::remove_if( + module->errors.begin(), + module->errors.end(), + [](auto err) + { + return get(err) != nullptr; + } ), - module->errors.end() + module->errors.end() ); - } freeze(module->interfaceTypes); } diff --git a/Analysis/src/Normalize.cpp b/Analysis/src/Normalize.cpp index 1ad5eb96..2c994677 100644 --- a/Analysis/src/Normalize.cpp +++ b/Analysis/src/Normalize.cpp @@ -22,9 +22,14 @@ LUAU_FASTINTVARIABLE(LuauNormalizeCacheLimit, 100000) LUAU_FASTINTVARIABLE(LuauNormalizeIntersectionLimit, 200) LUAU_FASTINTVARIABLE(LuauNormalizeUnionLimit, 100) LUAU_FASTFLAG(LuauSolverV2) -LUAU_FASTFLAGVARIABLE(LuauNormalizationReorderFreeTypeIntersect) LUAU_FASTFLAG(LuauUseWorkspacePropToChooseSolver) LUAU_FASTFLAG(LuauReduceSetTypeStackPressure) +LUAU_FASTFLAGVARIABLE(LuauImproveNormalizeExternTypeCheck) +LUAU_FASTFLAG(LuauPassBindableGenericsByReference) +LUAU_FASTFLAGVARIABLE(LuauNormalizerUnionTyvarsTakeMaxSize) +LUAU_FASTFLAGVARIABLE(LuauNormalizationPreservesAny) +LUAU_FASTFLAGVARIABLE(LuauNormalizerStepwiseFuel) +LUAU_FASTINTVARIABLE(LuauNormalizerInitialFuel, 3000) namespace Luau { @@ -37,6 +42,32 @@ static bool shouldEarlyExit(NormalizationResult res) return false; } +class NormalizerHitLimits : public std::exception +{ +}; + +struct FuelInitializer +{ + NotNull normalizer; + bool initializedFuel; + + explicit FuelInitializer(NotNull normalizer) + : normalizer(normalizer) + , initializedFuel(normalizer->initializeFuel()) + { + } + + FuelInitializer(const FuelInitializer& rhs) = delete; + + FuelInitializer& operator=(const FuelInitializer&) = delete; + + ~FuelInitializer() + { + if (initializedFuel) + normalizer->clearFuel(); + } +}; + NormalizedStringType::NormalizedStringType() {} NormalizedStringType::NormalizedStringType(bool isCofinite, std::map singletons) @@ -144,7 +175,8 @@ bool NormalizedFunctionType::isNever() const } NormalizedType::NormalizedType(NotNull builtinTypes) - : tops(builtinTypes->neverType) + : builtinTypes(builtinTypes) + , tops(builtinTypes->neverType) , booleans(builtinTypes->neverType) , errors(builtinTypes->neverType) , nils(builtinTypes->neverType) @@ -170,10 +202,21 @@ bool NormalizedType::isUnknown() const { if (auto ct = get(t)) { - if (ct->name == "class" && disj.empty()) + if (FFlag::LuauImproveNormalizeExternTypeCheck) { - isTopExternType = true; - break; + if (t == builtinTypes->externType && disj.empty()) + { + isTopExternType = true; + break; + } + } + else + { + if (ct->name == "userdata" && disj.empty()) + { + isTopExternType = true; + break; + } } } } @@ -328,7 +371,22 @@ NormalizationResult Normalizer::isInhabited(const NormalizedType* norm) { Set seen{nullptr}; - return isInhabited(norm, seen); + if (FFlag::LuauNormalizerStepwiseFuel) + { + try + { + FuelInitializer fi{NotNull{this}}; + return isInhabited(norm, seen); + } + catch (const NormalizerHitLimits&) + { + return NormalizationResult::HitLimits; + } + } + else + { + return isInhabited(norm, seen); + } } NormalizationResult Normalizer::isInhabited(const NormalizedType* norm, Set& seen) @@ -337,6 +395,9 @@ 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() || !norm->strings.isNever() || !norm->functions.isNever()) @@ -368,14 +429,38 @@ NormalizationResult Normalizer::isInhabited(TypeId ty) } Set seen{nullptr}; - NormalizationResult result = isInhabited(ty, seen); - if (cacheInhabitance && result == NormalizationResult::True) - cachedIsInhabited[ty] = true; - else if (cacheInhabitance && result == NormalizationResult::False) - cachedIsInhabited[ty] = false; + if (FFlag::LuauNormalizerStepwiseFuel) + { + try + { + FuelInitializer fi{NotNull{this}}; + NormalizationResult result = isInhabited(ty, seen); - return result; + 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 + { + + NormalizationResult result = isInhabited(ty, seen); + + if (cacheInhabitance && result == NormalizationResult::True) + cachedIsInhabited[ty] = true; + else if (cacheInhabitance && result == NormalizationResult::False) + cachedIsInhabited[ty] = false; + + return result; + } } NormalizationResult Normalizer::isInhabited(TypeId ty, Set& seen) @@ -384,6 +469,8 @@ NormalizationResult Normalizer::isInhabited(TypeId ty, Set& seen) if (!withinResourceLimits()) return NormalizationResult::HitLimits; + if (FFlag::LuauNormalizerStepwiseFuel) + consumeFuel(); // TODO: use log.follow(ty), CLI-64291 ty = follow(ty); @@ -439,11 +526,29 @@ NormalizationResult Normalizer::isIntersectionInhabited(TypeId left, TypeId righ { Set seen{nullptr}; SeenTablePropPairs seenTablePropPairs{{nullptr, nullptr}}; - return isIntersectionInhabited(left, right, seenTablePropPairs, seen); + if (FFlag::LuauNormalizerStepwiseFuel) + { + try + { + FuelInitializer fi{NotNull{this}}; + return isIntersectionInhabited(left, right, seenTablePropPairs, seen); + } + catch (const NormalizerHitLimits&) + { + return NormalizationResult::HitLimits; + } + } + else + { + return isIntersectionInhabited(left, right, seenTablePropPairs, seen); + } } NormalizationResult Normalizer::isIntersectionInhabited(TypeId left, TypeId right, SeenTablePropPairs& seenTablePropPairs, Set& seenSet) { + if (FFlag::LuauNormalizerStepwiseFuel) + consumeFuel(); + left = follow(left); right = follow(right); // We're asking if intersection is inhabited between left and right but we've already seen them .... @@ -814,9 +919,27 @@ std::shared_ptr Normalizer::normalize(TypeId ty) NormalizedType norm{builtinTypes}; Set seenSetTypes{nullptr}; SeenTablePropPairs seenTablePropPairs{{nullptr, nullptr}}; - NormalizationResult res = unionNormalWithTy(norm, ty, seenTablePropPairs, seenSetTypes); - if (res != NormalizationResult::True) - return 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 + { + NormalizationResult res = unionNormalWithTy(norm, ty, seenTablePropPairs, seenSetTypes); + if (res != NormalizationResult::True) + return nullptr; + } if (norm.isUnknown()) { @@ -841,8 +964,12 @@ NormalizationResult Normalizer::normalizeIntersections( { if (!arena) sharedState->iceHandler->ice("Normalizing types outside a module"); + + if (FFlag::LuauNormalizerStepwiseFuel) + consumeFuel(); + NormalizedType norm{builtinTypes}; - norm.tops = builtinTypes->anyType; + norm.tops = FFlag::LuauNormalizationPreservesAny ? builtinTypes->unknownType : builtinTypes->anyType; // Now we need to intersect the two types for (auto ty : intersections) { @@ -889,6 +1016,9 @@ const TypeIds* Normalizer::cacheTypeIds(TypeIds tys) TypeId Normalizer::unionType(TypeId here, TypeId there) { + if (FFlag::LuauNormalizerStepwiseFuel) + consumeFuel(); + here = follow(here); there = follow(there); @@ -935,6 +1065,9 @@ TypeId Normalizer::unionType(TypeId here, TypeId there) TypeId Normalizer::intersectionType(TypeId here, TypeId there) { + if (FFlag::LuauNormalizerStepwiseFuel) + consumeFuel(); + here = follow(here); there = follow(there); @@ -993,6 +1126,9 @@ void Normalizer::clearCaches() // ------- Normalizing unions TypeId Normalizer::unionOfTops(TypeId here, TypeId there) { + if (FFlag::LuauNormalizerStepwiseFuel) + consumeFuel(); + if (get(here) || get(there)) return there; else @@ -1001,6 +1137,9 @@ TypeId Normalizer::unionOfTops(TypeId here, TypeId there) TypeId Normalizer::unionOfBools(TypeId here, TypeId there) { + if (FFlag::LuauNormalizerStepwiseFuel) + consumeFuel(); + if (get(here)) return there; if (get(there)) @@ -1014,6 +1153,9 @@ TypeId Normalizer::unionOfBools(TypeId here, TypeId there) void Normalizer::unionExternTypesWithExternType(TypeIds& heres, TypeId there) { + if (FFlag::LuauNormalizerStepwiseFuel) + consumeFuel(); + if (heres.count(there)) return; @@ -1036,6 +1178,9 @@ void Normalizer::unionExternTypesWithExternType(TypeIds& heres, TypeId there) void Normalizer::unionExternTypes(TypeIds& heres, const TypeIds& theres) { + if (FFlag::LuauNormalizerStepwiseFuel) + consumeFuel(); + for (TypeId there : theres) unionExternTypesWithExternType(heres, there); } @@ -1053,6 +1198,9 @@ static bool isSubclass(TypeId test, TypeId parent) void Normalizer::unionExternTypesWithExternType(NormalizedExternType& heres, TypeId there) { + if (FFlag::LuauNormalizerStepwiseFuel) + consumeFuel(); + for (auto it = heres.ordering.begin(); it != heres.ordering.end();) { TypeId hereTy = *it; @@ -1131,6 +1279,8 @@ 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(); for (const TypeId thereTy : theres.ordering) { @@ -1220,6 +1370,9 @@ void Normalizer::unionExternTypes(NormalizedExternType& heres, const NormalizedE void Normalizer::unionStrings(NormalizedStringType& here, const NormalizedStringType& there) { + if (FFlag::LuauNormalizerStepwiseFuel) + consumeFuel(); + if (there.isString()) here.resetToString(); else if (here.isUnion() && there.isUnion()) @@ -1264,6 +1417,9 @@ void Normalizer::unionStrings(NormalizedStringType& here, const NormalizedString std::optional Normalizer::unionOfTypePacks(TypePackId here, TypePackId there) { + if (FFlag::LuauNormalizerStepwiseFuel) + consumeFuel(); + if (here == there) return here; @@ -1391,6 +1547,9 @@ std::optional Normalizer::unionOfTypePacks(TypePackId here, TypePack std::optional Normalizer::unionOfFunctions(TypeId here, TypeId there) { + if (FFlag::LuauNormalizerStepwiseFuel) + consumeFuel(); + if (get(here)) return here; @@ -1407,7 +1566,7 @@ std::optional Normalizer::unionOfFunctions(TypeId here, TypeId there) if (hftv->genericPacks != tftv->genericPacks) return std::nullopt; - std::optional argTypes = intersectionOfTypePacks(hftv->argTypes, tftv->argTypes); + std::optional argTypes = intersectionOfTypePacks_INTERNAL(hftv->argTypes, tftv->argTypes); if (!argTypes) return std::nullopt; @@ -1428,6 +1587,9 @@ std::optional Normalizer::unionOfFunctions(TypeId here, TypeId there) void Normalizer::unionFunctions(NormalizedFunctionType& heres, const NormalizedFunctionType& theres) { + if (FFlag::LuauNormalizerStepwiseFuel) + consumeFuel(); + if (heres.isTop) return; if (theres.isTop) @@ -1459,6 +1621,9 @@ void Normalizer::unionFunctions(NormalizedFunctionType& heres, const NormalizedF void Normalizer::unionFunctionsWithFunction(NormalizedFunctionType& heres, TypeId there) { + if (FFlag::LuauNormalizerStepwiseFuel) + consumeFuel(); + if (heres.isNever()) { TypeIds tmps; @@ -1491,6 +1656,9 @@ void Normalizer::unionTablesWithTable(TypeIds& heres, TypeId there) void Normalizer::unionTables(TypeIds& heres, const TypeIds& theres) { + if (FFlag::LuauNormalizerStepwiseFuel) + consumeFuel(); + for (TypeId there : theres) { if (there == builtinTypes->tableType) @@ -1527,6 +1695,9 @@ 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(); + here.isCacheable &= there.isCacheable; TypeId tops = unionOfTops(here.tops, there.tops); @@ -1539,8 +1710,20 @@ NormalizationResult Normalizer::unionNormals(NormalizedType& here, const Normali return NormalizationResult::True; } - if (here.tyvars.size() * there.tyvars.size() >= size_t(FInt::LuauNormalizeUnionLimit)) - return NormalizationResult::HitLimits; + 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++) { @@ -1562,9 +1745,12 @@ NormalizationResult Normalizer::unionNormals(NormalizedType& here, const Normali return res; } - // 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; + 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); @@ -1610,6 +1796,8 @@ bool Normalizer::useNewLuauSolver() const NormalizationResult Normalizer::intersectNormalWithNegationTy(TypeId toNegate, NormalizedType& intersect) { + if (FFlag::LuauNormalizerStepwiseFuel) + consumeFuel(); std::optional negated; @@ -1635,6 +1823,9 @@ NormalizationResult Normalizer::unionNormalWithTy( if (!withinResourceLimits()) return NormalizationResult::HitLimits; + if (FFlag::LuauNormalizerStepwiseFuel) + consumeFuel(); + there = follow(there); if (get(there) || get(there)) @@ -1679,7 +1870,7 @@ NormalizationResult Normalizer::unionNormalWithTy( seenSetTypes.insert(there); NormalizedType norm{builtinTypes}; - norm.tops = builtinTypes->anyType; + norm.tops = FFlag::LuauNormalizationPreservesAny ? builtinTypes->unknownType : builtinTypes->anyType; for (IntersectionTypeIterator it = begin(itv); it != end(itv); ++it) { NormalizationResult res = intersectNormalWithTy(norm, *it, seenTablePropPairs, seenSetTypes); @@ -1796,6 +1987,9 @@ NormalizationResult Normalizer::unionNormalWithTy( std::optional Normalizer::negateNormal(const NormalizedType& here) { + if (FFlag::LuauNormalizerStepwiseFuel) + consumeFuel(); + NormalizedType result{builtinTypes}; result.isCacheable = here.isCacheable; @@ -1892,6 +2086,9 @@ std::optional Normalizer::negateNormal(const NormalizedType& her TypeIds Normalizer::negateAll(const TypeIds& theres) { + if (FFlag::LuauNormalizerStepwiseFuel) + consumeFuel(); + TypeIds tys; for (TypeId there : theres) tys.insert(negate(there)); @@ -1900,6 +2097,9 @@ TypeIds Normalizer::negateAll(const TypeIds& theres) TypeId Normalizer::negate(TypeId there) { + if (FFlag::LuauNormalizerStepwiseFuel) + consumeFuel(); + there = follow(there); if (get(there)) return there; @@ -1929,6 +2129,9 @@ TypeId Normalizer::negate(TypeId there) void Normalizer::subtractPrimitive(NormalizedType& here, TypeId ty) { + if (FFlag::LuauNormalizerStepwiseFuel) + consumeFuel(); + const PrimitiveType* ptv = get(follow(ty)); LUAU_ASSERT(ptv); switch (ptv->type) @@ -1962,6 +2165,9 @@ void Normalizer::subtractPrimitive(NormalizedType& here, TypeId ty) void Normalizer::subtractSingleton(NormalizedType& here, TypeId ty) { + if (FFlag::LuauNormalizerStepwiseFuel) + consumeFuel(); + const SingletonType* stv = get(ty); LUAU_ASSERT(stv); @@ -2005,14 +2211,40 @@ void Normalizer::subtractSingleton(NormalizedType& here, TypeId ty) // ------- Normalizing intersections TypeId Normalizer::intersectionOfTops(TypeId here, TypeId there) { - if (get(here) || get(there)) - return here; + if (FFlag::LuauNormalizerStepwiseFuel) + consumeFuel(); + + if (FFlag::LuauNormalizationPreservesAny) + { + // NOTE: We need to wrap these in parens as C++'s parser isn't _quite_ + // able to recognize these are generic function calls after macro + // expansion. + LUAU_ASSERT((is(here))); + LUAU_ASSERT((is(there))); + + if (get(here) || get(there)) + return builtinTypes->neverType; + + if (get(here) || get(there)) + return builtinTypes->anyType; + + return builtinTypes->unknownType; + + } else - return there; + { + if (get(here) || get(there)) + return here; + else + return there; + } } TypeId Normalizer::intersectionOfBools(TypeId here, TypeId there) { + if (FFlag::LuauNormalizerStepwiseFuel) + consumeFuel(); + if (get(here)) return here; if (get(there)) @@ -2028,6 +2260,9 @@ TypeId Normalizer::intersectionOfBools(TypeId here, TypeId there) void Normalizer::intersectExternTypes(NormalizedExternType& heres, const NormalizedExternType& theres) { + if (FFlag::LuauNormalizerStepwiseFuel) + consumeFuel(); + if (theres.isNever()) { heres.resetToNever(); @@ -2153,6 +2388,9 @@ void Normalizer::intersectExternTypes(NormalizedExternType& heres, const Normali void Normalizer::intersectExternTypesWithExternType(NormalizedExternType& heres, TypeId there) { + if (FFlag::LuauNormalizerStepwiseFuel) + consumeFuel(); + for (auto it = heres.ordering.begin(); it != heres.ordering.end();) { TypeId hereTy = *it; @@ -2224,6 +2462,9 @@ void Normalizer::intersectExternTypesWithExternType(NormalizedExternType& heres, void Normalizer::intersectStrings(NormalizedStringType& here, const NormalizedStringType& there) { + if (FFlag::LuauNormalizerStepwiseFuel) + consumeFuel(); + /* There are 9 cases to worry about here Normalized Left | Normalized Right C1 string | string ===> trivial @@ -2290,6 +2531,25 @@ void Normalizer::intersectStrings(NormalizedStringType& here, const NormalizedSt std::optional Normalizer::intersectionOfTypePacks(TypePackId here, TypePackId there) { + LUAU_ASSERT(FFlag::LuauNormalizerStepwiseFuel); + + FuelInitializer fi{NotNull{this}}; + + try + { + return intersectionOfTypePacks_INTERNAL(here, there); + } + catch (const NormalizerHitLimits&) + { + return std::nullopt; + } +} + +std::optional Normalizer::intersectionOfTypePacks_INTERNAL(TypePackId here, TypePackId there) +{ + if (FFlag::LuauNormalizerStepwiseFuel) + consumeFuel(); + if (here == there) return here; @@ -2411,6 +2671,9 @@ std::optional Normalizer::intersectionOfTypePacks(TypePackId here, T std::optional Normalizer::intersectionOfTables(TypeId here, TypeId there, SeenTablePropPairs& seenTablePropPairs, Set& seenSet) { + if (FFlag::LuauNormalizerStepwiseFuel) + consumeFuel(); + if (here == there) return here; @@ -2641,6 +2904,9 @@ std::optional Normalizer::intersectionOfTables(TypeId here, TypeId there void Normalizer::intersectTablesWithTable(TypeIds& heres, TypeId there, SeenTablePropPairs& seenTablePropPairs, Set& seenSetTypes) { + if (FFlag::LuauNormalizerStepwiseFuel) + consumeFuel(); + TypeIds tmp; for (TypeId here : heres) { @@ -2653,6 +2919,9 @@ void Normalizer::intersectTablesWithTable(TypeIds& heres, TypeId there, SeenTabl void Normalizer::intersectTables(TypeIds& heres, const TypeIds& theres) { + if (FFlag::LuauNormalizerStepwiseFuel) + consumeFuel(); + TypeIds tmp; for (TypeId here : heres) { @@ -2671,6 +2940,9 @@ void Normalizer::intersectTables(TypeIds& heres, const TypeIds& theres) std::optional Normalizer::intersectionOfFunctions(TypeId here, TypeId there) { + if (FFlag::LuauNormalizerStepwiseFuel) + consumeFuel(); + const FunctionType* hftv = get(here); LUAU_ASSERT(hftv); const FunctionType* tftv = get(there); @@ -2694,7 +2966,7 @@ std::optional Normalizer::intersectionOfFunctions(TypeId here, TypeId th } else if (hftv->argTypes == tftv->argTypes) { - std::optional retTypesOpt = intersectionOfTypePacks(hftv->argTypes, tftv->argTypes); + std::optional retTypesOpt = intersectionOfTypePacks_INTERNAL(hftv->argTypes, tftv->argTypes); if (!retTypesOpt) return std::nullopt; argTypes = hftv->argTypes; @@ -2805,6 +3077,9 @@ 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(); + const FunctionType* hftv = get(here); if (!hftv) return std::nullopt; @@ -2832,6 +3107,9 @@ std::optional Normalizer::unionSaturatedFunctions(TypeId here, TypeId th void Normalizer::intersectFunctionsWithFunction(NormalizedFunctionType& heres, TypeId there) { + if (FFlag::LuauNormalizerStepwiseFuel) + consumeFuel(); + if (heres.isNever()) return; @@ -2864,6 +3142,9 @@ void Normalizer::intersectFunctionsWithFunction(NormalizedFunctionType& heres, T void Normalizer::intersectFunctions(NormalizedFunctionType& heres, const NormalizedFunctionType& theres) { + if (FFlag::LuauNormalizerStepwiseFuel) + consumeFuel(); + if (heres.isNever()) return; else if (theres.isNever()) @@ -2885,6 +3166,9 @@ NormalizationResult Normalizer::intersectTyvarsWithTy( Set& seenSetTypes ) { + if (FFlag::LuauNormalizerStepwiseFuel) + consumeFuel(); + for (auto it = here.begin(); it != here.end();) { NormalizedType& inter = *it->second; @@ -2906,6 +3190,9 @@ NormalizationResult Normalizer::intersectNormals(NormalizedType& here, const Nor if (!withinResourceLimits()) return NormalizationResult::HitLimits; + if (FFlag::LuauNormalizerStepwiseFuel) + consumeFuel(); + if (!get(there.tops)) { here.tops = intersectionOfTops(here.tops, there.tops); @@ -2919,26 +3206,26 @@ NormalizationResult Normalizer::intersectNormals(NormalizedType& here, const Nor // Limit based on worst-case expansion of the table/function intersections // This restriction can be relaxed when table intersection simplification is improved - if (here.tables.size() * there.tables.size() >= size_t(FInt::LuauNormalizeIntersectionLimit)) - return NormalizationResult::HitLimits; + 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; + if (here.functions.parts.size() * there.functions.parts.size() >= size_t(FInt::LuauNormalizeIntersectionLimit)) + return NormalizationResult::HitLimits; + } - if (FFlag::LuauNormalizationReorderFreeTypeIntersect) + for (auto& [tyvar, inter] : there.tyvars) { - for (auto& [tyvar, inter] : there.tyvars) + int index = tyvarIndex(tyvar); + if (ignoreSmallerTyvars < index) { - int index = tyvarIndex(tyvar); - if (ignoreSmallerTyvars < index) + auto [found, fresh] = here.tyvars.emplace(tyvar, std::make_unique(NormalizedType{builtinTypes})); + if (fresh) { - auto [found, fresh] = here.tyvars.emplace(tyvar, std::make_unique(NormalizedType{builtinTypes})); - if (fresh) - { - NormalizationResult res = unionNormals(*found->second, here, index); - if (res != NormalizationResult::True) - return res; - } + NormalizationResult res = unionNormals(*found->second, here, index); + if (res != NormalizationResult::True) + return res; } } } @@ -2955,24 +3242,6 @@ NormalizationResult Normalizer::intersectNormals(NormalizedType& here, const Nor intersectFunctions(here.functions, there.functions); intersectTables(here.tables, there.tables); - if (!FFlag::LuauNormalizationReorderFreeTypeIntersect) - { - for (auto& [tyvar, inter] : there.tyvars) - { - int index = tyvarIndex(tyvar); - if (ignoreSmallerTyvars < index) - { - auto [found, fresh] = here.tyvars.emplace(tyvar, std::make_unique(NormalizedType{builtinTypes})); - if (fresh) - { - NormalizationResult res = unionNormals(*found->second, here, index); - if (res != NormalizationResult::True) - return res; - } - } - } - } - for (auto it = here.tyvars.begin(); it != here.tyvars.end();) { TypeId tyvar = it->first; @@ -3011,6 +3280,9 @@ NormalizationResult Normalizer::intersectNormalWithTy( if (!withinResourceLimits()) return NormalizationResult::HitLimits; + if (FFlag::LuauNormalizerStepwiseFuel) + consumeFuel(); + there = follow(there); if (get(there) || get(there)) @@ -3057,7 +3329,7 @@ NormalizationResult Normalizer::intersectNormalWithTy( NormalizedTyvars tyvars = std::move(here.tyvars); - if (const FunctionType* utv = get(there)) + if (get(there)) { NormalizedFunctionType functions = std::move(here.functions); clearNormal(here); @@ -3148,9 +3420,9 @@ NormalizationResult Normalizer::intersectNormalWithTy( else if (const NegationType* ntv = get(there)) { TypeId t = follow(ntv->ty); - if (const PrimitiveType* ptv = get(t)) + if (get(t)) subtractPrimitive(here, ntv->ty); - else if (const SingletonType* stv = get(t)) + else if (get(t)) subtractSingleton(here, follow(ntv->ty)); else if (get(t)) { @@ -3379,6 +3651,34 @@ TypeId Normalizer::typeFromNormal(const NormalizedType& norm) return arena->addType(UnionType{std::move(result)}); } +bool Normalizer::initializeFuel() +{ + LUAU_ASSERT(FFlag::LuauNormalizerStepwiseFuel); + if (fuel) + return false; + + fuel = FInt::LuauNormalizerInitialFuel; + return true; +} + +void Normalizer::clearFuel() +{ + LUAU_ASSERT(FFlag::LuauNormalizerStepwiseFuel); + fuel = std::nullopt; +} + +void Normalizer::consumeFuel() +{ + LUAU_ASSERT(FFlag::LuauNormalizerStepwiseFuel); + if (fuel) + { + (*fuel)--; + if (fuel <= 0) + throw NormalizerHitLimits(); + } +} + + bool isSubtype( TypeId subTy, TypeId superTy, @@ -3456,7 +3756,8 @@ bool isSubtype( { Subtyping subtyping{builtinTypes, NotNull{&arena}, simplifier, NotNull{&normalizer}, NotNull{&typeFunctionRuntime}, NotNull{&ice}}; - return subtyping.isSubtype(subPack, superPack, scope).isSubtype; + return FFlag::LuauPassBindableGenericsByReference ? subtyping.isSubtype(subPack, superPack, scope, {}).isSubtype + : subtyping.isSubtype_DEPRECATED(subPack, superPack, scope).isSubtype; } else { @@ -3473,7 +3774,8 @@ bool isSubtype( { Subtyping subtyping{builtinTypes, NotNull{&arena}, simplifier, NotNull{&normalizer}, NotNull{&typeFunctionRuntime}, NotNull{&ice}}; - return subtyping.isSubtype(subPack, superPack, scope).isSubtype; + return FFlag::LuauPassBindableGenericsByReference ? subtyping.isSubtype(subPack, superPack, scope, {}).isSubtype + : subtyping.isSubtype_DEPRECATED(subPack, superPack, scope).isSubtype; } else { diff --git a/Analysis/src/OverloadResolution.cpp b/Analysis/src/OverloadResolution.cpp index 4b4c513d..c908709f 100644 --- a/Analysis/src/OverloadResolution.cpp +++ b/Analysis/src/OverloadResolution.cpp @@ -12,11 +12,8 @@ #include "Luau/Unifier2.h" LUAU_FASTFLAG(LuauLimitUnification) -LUAU_FASTFLAG(LuauReturnMappedGenericPacksFromSubtyping2) -LUAU_FASTFLAG(LuauSubtypingGenericsDoesntUseVariance) -LUAU_FASTFLAG(LuauVariadicAnyPackShouldBeErrorSuppressing) LUAU_FASTFLAG(LuauSubtypingReportGenericBoundMismatches2) -LUAU_FASTFLAG(LuauSubtypingGenericPacksDoesntUseVariance) +LUAU_FASTFLAG(LuauPassBindableGenericsByReference) namespace Luau { @@ -45,54 +42,32 @@ OverloadResolver::OverloadResolver( { } -std::pair OverloadResolver::selectOverload(TypeId ty, TypePackId argsPack, NotNull> uniqueTypes, bool useFreeTypeBounds) +std::pair OverloadResolver::selectOverload( + TypeId ty, + TypePackId argsPack, + NotNull> uniqueTypes, + bool useFreeTypeBounds +) { - auto tryOne = [&](TypeId f) - { - if (auto ftv = get(f)) - { - Subtyping::Variance variance = subtyping.variance; - subtyping.variance = Subtyping::Variance::Contravariant; - subtyping.uniqueTypes = uniqueTypes; - SubtypingResult r; - if (FFlag::LuauSubtypingGenericsDoesntUseVariance) - { - std::vector generics; - generics.reserve(ftv->generics.size()); - for (TypeId g : ftv->generics) - { - g = follow(g); - if (get(g)) - generics.emplace_back(g); - } - r = subtyping.isSubtype( - argsPack, ftv->argTypes, scope, !generics.empty() ? std::optional>{generics} : std::nullopt - ); - } - else - r = subtyping.isSubtype(argsPack, ftv->argTypes, scope); - subtyping.variance = variance; - - if (!useFreeTypeBounds && !r.assumedConstraints.empty()) - return false; - - if (r.isSubtype) - return true; - } - - return false; - }; - TypeId t = follow(ty); - if (tryOne(ty)) - return {Analysis::Ok, ty}; - - if (auto it = get(t)) + if (const FunctionType* fn = get(t)) + { + if (testFunctionTypeForOverloadSelection(fn, uniqueTypes, argsPack, useFreeTypeBounds)) + return {Analysis::Ok, ty}; + else + return {Analysis::OverloadIsNonviable, ty}; + } + else if (auto it = get(t)) { for (TypeId component : it) { - if (tryOne(component)) + const FunctionType* fn = get(follow(component)); + // Only consider function overloads with compatible arities + if (!fn || !isArityCompatible(argsPack, fn->argTypes, builtinTypes)) + continue; + + if (testFunctionTypeForOverloadSelection(fn, uniqueTypes, argsPack, useFreeTypeBounds)) return {Analysis::Ok, component}; } } @@ -100,7 +75,13 @@ std::pair OverloadResolver::selectOverload(T return {Analysis::OverloadIsNonviable, ty}; } -void OverloadResolver::resolve(TypeId fnTy, const TypePack* args, AstExpr* selfExpr, const std::vector* argExprs, NotNull> uniqueTypes) +void OverloadResolver::resolve( + TypeId fnTy, + const TypePack* args, + AstExpr* selfExpr, + const std::vector* argExprs, + NotNull> uniqueTypes +) { fnTy = follow(fnTy); @@ -117,6 +98,17 @@ void OverloadResolver::resolve(TypeId fnTy, const TypePack* args, AstExpr* selfE if (resolution.find(ty) != resolution.end()) continue; + if (const FunctionType* fn = get(follow(ty))) + { + // If the overload isn't arity compatible, report the mismatch and don't do more work + const TypePackId argPack = arena->addTypePack(*args); + if (!isArityCompatible(argPack, fn->argTypes, builtinTypes)) + { + add(ArityMismatch, ty, {}); + continue; + } + } + auto [analysis, errors] = checkOverload(ty, args, selfExpr, argExprs, uniqueTypes); add(analysis, ty, std::move(errors)); } @@ -154,7 +146,8 @@ std::optional OverloadResolver::testIsSubtype(const Location& location std::optional OverloadResolver::testIsSubtype(const Location& location, TypePackId subTy, TypePackId superTy) { - auto r = subtyping.isSubtype(subTy, superTy, scope); + auto r = FFlag::LuauPassBindableGenericsByReference ? subtyping.isSubtype(subTy, superTy, scope, {}) + : subtyping.isSubtype_DEPRECATED(subTy, superTy, scope); ErrorVec errors; if (r.normalizationTooComplex) @@ -261,6 +254,76 @@ void OverloadResolver::maybeEmplaceError( } } +bool OverloadResolver::isArityCompatible(const TypePackId candidate, const TypePackId desired, NotNull builtinTypes) const +{ + auto [candidateHead, candidateTail] = flatten(candidate); + auto [desiredHead, desiredTail] = flatten(desired); + + // Handle mismatched head sizes + if (candidateHead.size() < desiredHead.size()) + { + if (candidateTail) + return true; // A tail can fill in remaining values + + // If the candidate is shorter than desired and has no tail, it can only match if the extra desired args are all optional + for (size_t i = candidateHead.size(); i < desiredHead.size(); ++i) + { + if (const TypeId ty = follow(desiredHead[i]); !isOptionalType(ty, builtinTypes)) + return false; + } + } + + if (desiredTail && candidateHead.size() <= desiredHead.size() && !candidateTail) + { + // A non-tail candidate can't match a desired tail unless the tail accepts nils + // We don't allow generic packs to implicitly accept an empty pack here + TypePackId desiredTailTP = follow(*desiredTail); + + if (desiredTailTP == builtinTypes->unknownTypePack || desiredTailTP == builtinTypes->anyTypePack) + return true; + + if (const VariadicTypePack* vtp = get(desiredTailTP)) + return vtp->ty == builtinTypes->nilType; + + return false; + } + + // There aren't any other failure conditions; we don't care if we pass more args than needed + + return true; +} + +bool OverloadResolver::testFunctionTypeForOverloadSelection( + const FunctionType* ftv, + NotNull> uniqueTypes, + TypePackId argsPack, + bool useFreeTypeBounds +) +{ + Subtyping::Variance variance = subtyping.variance; + subtyping.variance = Subtyping::Variance::Contravariant; + subtyping.uniqueTypes = uniqueTypes; + std::vector generics; + generics.reserve(ftv->generics.size()); + for (TypeId g : ftv->generics) + { + g = follow(g); + if (get(g)) + generics.emplace_back(g); + } + SubtypingResult r = FFlag::LuauPassBindableGenericsByReference ? subtyping.isSubtype(argsPack, ftv->argTypes, scope, generics) + : subtyping.isSubtype_DEPRECATED(argsPack, ftv->argTypes, scope, generics); + subtyping.variance = variance; + + if (!useFreeTypeBounds && !r.assumedConstraints.empty()) + return false; + + if (r.isSubtype) + return true; + + return false; +} + std::pair OverloadResolver::checkOverload_( TypeId fnTy, const FunctionType* fn, @@ -341,86 +404,36 @@ std::pair OverloadResolver::checkOverload_ return {Analysis::Ok, {}}; } - if (FFlag::LuauReturnMappedGenericPacksFromSubtyping2) + const bool subPathArgTail = matchesPrefix(Path({TypePath::PackField::Arguments, TypePath::PackField::Tail}), reason.subPath); + const bool superPathArgs = matchesPrefix(Path(TypePath::PackField::Arguments), reason.superPath); + const TypePath::Component& lastSubComponent = reason.subPath.components.back(); + const bool subEndsInGenericPackMapping = get_if(&lastSubComponent) != nullptr; + + // If the function's argument list ends with a generic pack, and + // the subtype test failed because of that, we need to check the + // pack that the generic was mapped to in order to report an + // accurate CountMismatch error. + if (subPathArgTail && superPathArgs && subEndsInGenericPackMapping) { - if (FFlag::LuauSubtypingGenericPacksDoesntUseVariance) + const TypePack requiredMappedArgs = traverseForFlattenedPack(fnTy, reason.subPath, builtinTypes, arena); + const std::vector prospectiveHead = flatten(typ).first; + + const size_t requiredHeadSize = requiredMappedArgs.head.size(); + const size_t prospectiveHeadSize = prospectiveHead.size(); + + if (prospectiveHeadSize != requiredHeadSize) { - const bool subPathArgTail = matchesPrefix(Path({TypePath::PackField::Arguments, TypePath::PackField::Tail}), reason.subPath); - const bool superPathArgs = matchesPrefix(Path(TypePath::PackField::Arguments), reason.superPath); - const TypePath::Component& lastSubComponent = reason.subPath.components.back(); - const bool subEndsInGenericPackMapping = get_if(&lastSubComponent) != nullptr; - - // If the function's argument list ends with a generic pack, and - // the subtype test failed because of that, we need to check the - // pack that the generic was mapped to in order to report an - // accurate CountMismatch error. - if (subPathArgTail && superPathArgs && subEndsInGenericPackMapping) - { - const TypePack requiredMappedArgs = traverseForFlattenedPack(fnTy, reason.subPath, builtinTypes, arena); - const std::vector prospectiveHead = flatten(typ).first; - - const size_t requiredHeadSize = requiredMappedArgs.head.size(); - const size_t prospectiveHeadSize = prospectiveHead.size(); - - if (prospectiveHeadSize != requiredHeadSize) - { - TypeError error{ - fnExpr->location, - CountMismatch{ - requiredHeadSize, - requiredMappedArgs.tail.has_value() ? std::nullopt : std::optional{requiredHeadSize}, - prospectiveHeadSize, - CountMismatch::Arg - } - }; - - return {Analysis::ArityMismatch, {std::move(error)}}; + TypeError error{ + fnExpr->location, + CountMismatch{ + requiredHeadSize, + requiredMappedArgs.tail.has_value() ? std::nullopt : std::optional{requiredHeadSize}, + prospectiveHeadSize, + CountMismatch::Arg } - } - } - // If we have an arity mismatch with generic type pack parameters, then subPath matches Args :: Tail :: ... - // and superPath matches Args :: ... - else if (reason.subPath.components.size() >= 2 && reason.subPath.components[0] == TypePath::PackField::Arguments && - reason.subPath.components[1] == TypePath::PackField::Tail && reason.superPath.components.size() >= 1 && - reason.superPath.components[0] == TypePath::PackField::Arguments) - { - if (const auto [requiredHead, requiredTail] = flatten(fn->argTypes); requiredTail) - { - if (const auto genericTail = get(follow(requiredTail)); genericTail) - { - // Get the concrete type pack the generic is mapped to - const auto mappedGenHead = flatten_DEPRECATED(*requiredTail, sr.mappedGenericPacks_DEPRECATED).first; - - const auto prospectiveHead = flatten(typ).first; - - // We're just doing arity checking here - // We've flattened the type packs, so we can check prospectiveHead = requiredHead + mappedGenHead - // Super path reasoning is just args, so we can ignore the tails - const size_t neededHeadSize = requiredHead.size() + mappedGenHead.size(); - const size_t prospectiveHeadSize = prospectiveHead.size(); - if (prospectiveHeadSize != neededHeadSize) - { - TypeError error{fnExpr->location, CountMismatch{neededHeadSize, std::nullopt, prospectiveHeadSize, CountMismatch::Arg}}; - - return {Analysis::ArityMismatch, {error}}; - } - } - } - } - else if (reason.subPath == TypePath::Path{{TypePath::PackField::Arguments, TypePath::PackField::Tail}} && - reason.superPath == justArguments) - { - // We have an arity mismatch if the argument tail is a generic type pack - if (auto fnArgs = get(fn->argTypes)) - { - if (get(fnArgs->tail)) - { - auto [minParams, optMaxParams] = getParameterExtents(TxnLog::empty(), fn->argTypes); - TypeError error{fnExpr->location, CountMismatch{minParams, optMaxParams, args->head.size(), CountMismatch::Arg}}; + }; - return {Analysis::ArityMismatch, {std::move(error)}}; - } - } + return {Analysis::ArityMismatch, {std::move(error)}}; } } } @@ -451,57 +464,13 @@ std::pair OverloadResolver::checkOverload_ : argExprs->size() != 0 ? argExprs->back()->location : fnExpr->location; - // TODO: This optional can be unwrapped once we clip LuauSubtypingGenericPacksDoesntUseVariance and LuauReturnMappedGenericPacksFromSubtyping2 - std::optional failedSubTy; - if (FFlag::LuauSubtypingGenericPacksDoesntUseVariance) - failedSubTy = traverseForType(fnTy, reason.subPath, builtinTypes, arena); - else if (FFlag::LuauReturnMappedGenericPacksFromSubtyping2) - failedSubTy = traverseForType_DEPRECATED(fnTy, reason.subPath, builtinTypes, NotNull{&sr.mappedGenericPacks_DEPRECATED}, arena); - else - failedSubTy = traverseForType_DEPRECATED(fnTy, reason.subPath, builtinTypes); - - // TODO: This optional can be unwrapped once we clip LuauSubtypingGenericPacksDoesntUseVariance and LuauReturnMappedGenericPacksFromSubtyping2 - std::optional failedSuperTy; - if (FFlag::LuauSubtypingGenericPacksDoesntUseVariance) - failedSuperTy = traverseForType(prospectiveFunction, reason.superPath, builtinTypes, arena); - else if (FFlag::LuauReturnMappedGenericPacksFromSubtyping2) - failedSuperTy = traverseForType_DEPRECATED( - prospectiveFunction, reason.superPath, builtinTypes, NotNull{&sr.mappedGenericPacks_DEPRECATED}, arena - ); - else - failedSuperTy = traverseForType_DEPRECATED(prospectiveFunction, reason.superPath, builtinTypes); + std::optional failedSubTy = traverseForType(fnTy, reason.subPath, builtinTypes, arena); - if (FFlag::LuauReturnMappedGenericPacksFromSubtyping2) - maybeEmplaceError(&errors, argLocation, &reason, failedSubTy, failedSuperTy); - else if (failedSubTy && failedSuperTy) - { - switch (shouldSuppressErrors(normalizer, *failedSubTy).orElse(shouldSuppressErrors(normalizer, *failedSuperTy))) - { - case ErrorSuppression::Suppress: - break; - case ErrorSuppression::NormalizationFailed: - errors.emplace_back(argLocation, NormalizationTooComplex{}); - // intentionally fallthrough here since we couldn't prove this was error-suppressing - [[fallthrough]]; - case ErrorSuppression::DoNotSuppress: - // TODO extract location from the SubtypingResult path and argExprs - switch (reason.variance) - { - case SubtypingVariance::Covariant: - case SubtypingVariance::Contravariant: - errors.emplace_back(argLocation, TypeMismatch{*failedSubTy, *failedSuperTy, TypeMismatch::CovariantContext}); - break; - case SubtypingVariance::Invariant: - errors.emplace_back(argLocation, TypeMismatch{*failedSubTy, *failedSuperTy, TypeMismatch::InvariantContext}); - break; - default: - LUAU_ASSERT(0); - break; - } - } - } + std::optional failedSuperTy = traverseForType(prospectiveFunction, reason.superPath, builtinTypes, arena); + + maybeEmplaceError(&errors, argLocation, &reason, failedSubTy, failedSuperTy); } - else if (FFlag::LuauReturnMappedGenericPacksFromSubtyping2 && reason.superPath.components.size() > 1) + else if (reason.superPath.components.size() > 1) { // traverseForIndex only has a value if path is of form [...PackSlice, Index] if (const auto index = @@ -517,36 +486,15 @@ std::pair OverloadResolver::checkOverload_ LUAU_ASSERT(false); argLocation = fnExpr->location; } - std::optional failedSubTy = - FFlag::LuauSubtypingGenericPacksDoesntUseVariance - ? traverseForType(fnTy, reason.subPath, builtinTypes, arena) - : traverseForType_DEPRECATED(fnTy, reason.subPath, builtinTypes, NotNull{&sr.mappedGenericPacks_DEPRECATED}, arena); - std::optional failedSuperTy = - FFlag::LuauSubtypingGenericPacksDoesntUseVariance - ? traverseForType(prospectiveFunction, reason.superPath, builtinTypes, arena) - : traverseForType_DEPRECATED( - prospectiveFunction, reason.superPath, builtinTypes, NotNull{&sr.mappedGenericPacks_DEPRECATED}, arena - ); + std::optional failedSubTy = traverseForType(fnTy, reason.subPath, builtinTypes, arena); + std::optional failedSuperTy = traverseForType(prospectiveFunction, reason.superPath, builtinTypes, arena); maybeEmplaceError(&errors, argLocation, &reason, failedSubTy, failedSuperTy); } } - std::optional failedSubPack; - if (FFlag::LuauSubtypingGenericPacksDoesntUseVariance) - failedSubPack = traverseForPack(fnTy, reason.subPath, builtinTypes, arena); - else if (FFlag::LuauReturnMappedGenericPacksFromSubtyping2) - failedSubPack = traverseForPack_DEPRECATED(fnTy, reason.subPath, builtinTypes, NotNull{&sr.mappedGenericPacks_DEPRECATED}, arena); - else - failedSubPack = traverseForPack_DEPRECATED(fnTy, reason.subPath, builtinTypes); - - std::optional failedSuperPack; - if (FFlag::LuauSubtypingGenericPacksDoesntUseVariance) - failedSuperPack = traverseForPack(prospectiveFunction, reason.superPath, builtinTypes, arena); - else if (FFlag::LuauReturnMappedGenericPacksFromSubtyping2) - failedSuperPack = - traverseForPack_DEPRECATED(prospectiveFunction, reason.superPath, builtinTypes, NotNull{&sr.mappedGenericPacks_DEPRECATED}, arena); - else - failedSuperPack = traverseForPack_DEPRECATED(prospectiveFunction, reason.superPath, builtinTypes); + std::optional failedSubPack = traverseForPack(fnTy, reason.subPath, builtinTypes, arena); + + std::optional failedSuperPack = traverseForPack(prospectiveFunction, reason.superPath, builtinTypes, arena); if (failedSubPack && failedSuperPack) { @@ -559,12 +507,9 @@ std::pair OverloadResolver::checkOverload_ argLocation = argExprs->at(argExprs->size() - 1)->location; // TODO extract location from the SubtypingResult path and argExprs - if (FFlag::LuauVariadicAnyPackShouldBeErrorSuppressing) - { - auto errorSuppression = shouldSuppressErrors(normalizer, *failedSubPack).orElse(shouldSuppressErrors(normalizer, *failedSuperPack)); - if (errorSuppression == ErrorSuppression::Suppress) - break; - } + auto errorSuppression = shouldSuppressErrors(normalizer, *failedSubPack).orElse(shouldSuppressErrors(normalizer, *failedSuperPack)); + if (errorSuppression == ErrorSuppression::Suppress) + break; switch (reason.variance) { @@ -625,7 +570,6 @@ void OverloadResolver::add(Analysis analysis, TypeId ty, ErrorVec&& errors) nonFunctions.push_back(ty); break; case ArityMismatch: - LUAU_ASSERT(!errors.empty()); arityMismatches.emplace_back(ty, std::move(errors)); break; case OverloadIsNonviable: diff --git a/Analysis/src/RecursionCounter.cpp b/Analysis/src/RecursionCounter.cpp new file mode 100644 index 00000000..91203047 --- /dev/null +++ b/Analysis/src/RecursionCounter.cpp @@ -0,0 +1,40 @@ +// This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details + +#include "Luau/RecursionCounter.h" +#include "Luau/Error.h" + +namespace Luau +{ + +RecursionLimitException::RecursionLimitException(const std::string& system) + : InternalCompilerError("Internal recursion counter limit exceeded in " + system) +{ +} + +RecursionCounter::RecursionCounter(int* count) + : count(count) +{ + ++(*count); +} + +RecursionCounter::~RecursionCounter() +{ + LUAU_ASSERT(*count > 0); + --(*count); +} + +RecursionLimiter::RecursionLimiter(const std::string& system, int* count, int limit) + : RecursionCounter(count) +{ + if (!nativeStackGuard.isOk()) + { + throw InternalCompilerError("Stack overflow in " + system); + } + + if (limit > 0 && *count > limit) + { + throw RecursionLimitException(system); + } +} + +} diff --git a/Analysis/src/RequireTracer.cpp b/Analysis/src/RequireTracer.cpp index b286ad00..738fb894 100644 --- a/Analysis/src/RequireTracer.cpp +++ b/Analysis/src/RequireTracer.cpp @@ -71,19 +71,6 @@ struct RequireTracer : AstVisitor return true; } - AstExpr* getDependent_DEPRECATED(AstExpr* node) - { - if (AstExprLocal* expr = node->as()) - return locals[expr->local]; - else if (AstExprIndexName* expr = node->as()) - return expr->expr; - else if (AstExprIndexExpr* expr = node->as()) - return expr->expr; - else if (AstExprCall* expr = node->as(); expr && expr->self) - return expr->func->as()->expr; - else - return nullptr; - } AstNode* getDependent(AstNode* node) { if (AstExprLocal* expr = node->as()) diff --git a/Analysis/src/Scope.cpp b/Analysis/src/Scope.cpp index fad76f91..cd1a98a7 100644 --- a/Analysis/src/Scope.cpp +++ b/Analysis/src/Scope.cpp @@ -5,7 +5,6 @@ LUAU_FASTFLAG(LuauSolverV2); LUAU_FASTFLAGVARIABLE(LuauNoScopeShallNotSubsumeAll) -LUAU_FASTFLAG(LuauNameConstraintRestrictRecursiveTypes) namespace Luau { @@ -254,8 +253,6 @@ bool Scope::shouldWarnGlobal(std::string name) const bool Scope::isInvalidTypeAliasName(const std::string& name) const { - LUAU_ASSERT(FFlag::LuauNameConstraintRestrictRecursiveTypes); - for (auto scope = this; scope; scope = scope->parent.get()) { if (scope->invalidTypeAliasNames.contains(name)) diff --git a/Analysis/src/Simplify.cpp b/Analysis/src/Simplify.cpp index 4be86352..1079cdf2 100644 --- a/Analysis/src/Simplify.cpp +++ b/Analysis/src/Simplify.cpp @@ -21,10 +21,12 @@ LUAU_FASTFLAG(LuauSolverV2) LUAU_DYNAMIC_FASTINTVARIABLE(LuauSimplificationComplexityLimit, 8) LUAU_DYNAMIC_FASTINTVARIABLE(LuauTypeSimplificationIterationLimit, 128) LUAU_FASTFLAG(LuauRefineDistributesOverUnions) -LUAU_FASTFLAGVARIABLE(LuauSimplifyAnyAndUnion) LUAU_FASTFLAG(LuauReduceSetTypeStackPressure) -LUAU_FASTFLAG(LuauPushTypeConstraint) -LUAU_FASTFLAGVARIABLE(LuauMorePreciseExternTableRelation) +LUAU_FASTFLAG(LuauPushTypeConstraint2) +LUAU_FASTFLAGVARIABLE(LuauSimplifyRefinementOfReadOnlyProperty) +LUAU_FASTFLAGVARIABLE(LuauExternTableIndexersIntersect) +LUAU_FASTFLAGVARIABLE(LuauSimplifyMoveTableProps) +LUAU_FASTFLAGVARIABLE(LuauSimplifyIntersectionNoTreeSet) namespace Luau { @@ -42,7 +44,10 @@ struct TypeSimplifier TypeId mkNegation(TypeId ty) const; - TypeId intersectFromParts(std::set parts); + // Clip with LuauSimplifyIntersectionNoTreeSet + TypeId intersectFromParts_DEPRECATED(std::set parts); + + TypeId intersectFromParts(TypeIds parts); TypeId intersectUnionWithType(TypeId left, TypeId right); @@ -268,6 +273,11 @@ Relation relate(TypeId left, TypeId right, SimplifierSeenSet& seen); Relation relateTableToExternType(const TableType* table, const ExternType* cls, SimplifierSeenSet& seen) { + // If either the table or the extern type have an indexer, just bail. + // There's rapidly diminishing returns on doing something smart for + // indexers compared to refining exact members. + if (FFlag::LuauExternTableIndexersIntersect && (table->indexer || cls->indexer)) + return Relation::Intersects; for (auto& [name, prop] : table->props) { @@ -464,14 +474,14 @@ Relation relate(TypeId left, TypeId right, SimplifierSeenSet& seen) else if (get(right)) return flip(relate(right, left, seen)); - if (auto ut = get(left)) + if (get(left)) return Relation::Intersects; - else if (auto ut = get(right)) + else if (get(right)) return Relation::Intersects; if (auto ut = get(left)) { - if (FFlag::LuauPushTypeConstraint) + if (FFlag::LuauPushTypeConstraint2) { for (TypeId part : ut) { @@ -641,31 +651,7 @@ Relation relate(TypeId left, TypeId right, SimplifierSeenSet& seen) } if (auto re = get(right)) - { - if (FFlag::LuauMorePreciseExternTableRelation) - return relateTableToExternType(lt, re, seen); - - Relation overall = Relation::Coincident; - - for (auto& [name, prop] : lt->props) - { - if (auto propInExternType = re->props.find(name); propInExternType != re->props.end()) - { - LUAU_ASSERT(prop.readTy && propInExternType->second.readTy); - Relation propRel = relate(*prop.readTy, *propInExternType->second.readTy, seen); - - if (propRel == Relation::Disjoint) - return Relation::Disjoint; - - if (propRel == Relation::Coincident) - continue; - - overall = Relation::Intersects; - } - } - - return overall; - } + return relateTableToExternType(lt, re, seen); // TODO metatables @@ -685,22 +671,8 @@ Relation relate(TypeId left, TypeId right, SimplifierSeenSet& seen) return Relation::Disjoint; } - if (FFlag::LuauMorePreciseExternTableRelation) - { - if (auto tbl = get(right)) - return flip(relateTableToExternType(tbl, ct, seen)); - } - else - { - if (is(right)) - { - // FIXME: This could be better in that we can say a table only - // intersects with an extern type if they share a property, but - // for now it is within the contract of the function to claim - // the two intersect. - return Relation::Intersects; - } - } + if (auto tbl = get(right)) + return flip(relateTableToExternType(tbl, ct, seen)); return Relation::Disjoint; } @@ -731,7 +703,7 @@ TypeId TypeSimplifier::mkNegation(TypeId ty) const return result; } -TypeId TypeSimplifier::intersectFromParts(std::set parts) +TypeId TypeSimplifier::intersectFromParts_DEPRECATED(std::set parts) { if (0 == parts.size()) return builtinTypes->neverType; @@ -841,6 +813,129 @@ TypeId TypeSimplifier::intersectFromParts(std::set parts) return arena->addType(IntersectionType{std::vector{begin(newParts), end(newParts)}}); } +namespace { + +enum class Inhabited { + Yes, + No +}; + +Inhabited intersectOneWithIntersection(TypeSimplifier& simplifier, TypeIds& source, TypeIds& dest, TypeId candidate) +{ + if (dest.count(candidate) > 0) + return Inhabited::Yes; + + if (auto itv = get(candidate)) + { + for (auto subPart : itv) + { + if (intersectOneWithIntersection(simplifier, source, dest, subPart) == Inhabited::No) + return Inhabited::No; + } + + return Inhabited::Yes; + } + + if (source.empty()) + { + dest.insert(candidate); + return Inhabited::Yes; + } + + for (TypeId ty : source) + { + // All examples are presented in `candidate & ty` format. + switch (relate(candidate, ty)) + { + case Relation::Disjoint: + // If the candidate and a member of the intersection are + // disjoint, then the entire intersection is uninhabited, for + // example: + // + // boolean & string + return Inhabited::No; + case Relation::Subset: + // If the candidate is a _subset_ of the member of the + // intersection, then replace this entry if we don't already + // have the candidate in the set, e.g.: + // + // true & boolean + dest.insert(candidate); + break; + case Relation::Coincident: + case Relation::Superset: + // If the candidate and a member of the intersection are + // coincident, or the incoming part is a superset, then do + // nothing, e.g.: + // + // boolean & true + // boolean & boolean + dest.insert(ty); + break; + case Relation::Intersects: + { + // If the candidate and a member of the intersection may + // intersect, then attempt to replace the member with + // a simpler type, e.g.: + // + // boolean & ~(false?) + if (std::optional simplified = simplifier.basicIntersect(candidate, ty)) + dest.insert(*simplified); + else + { + dest.insert(candidate); + dest.insert(ty); + } + break; + } + } + } + + return Inhabited::Yes; +} +} + +TypeId TypeSimplifier::intersectFromParts(TypeIds parts) +{ + if (parts.size() == 0) + return builtinTypes->unknownType; + + if (parts.size() == 1) + return *parts.begin(); + + TypeIds source; + TypeIds dest; + + source.reserve(parts.size()); + dest.reserve(parts.size()); + + for (TypeId part : parts) + { + // We use the candidate, part, to construct a new intersection by + // intersecting every element in source against part, and then + // inserting it into dest. + if (intersectOneWithIntersection(*this, source, dest, part) == Inhabited::No) + return builtinTypes->neverType; + + // At this point, source will contain some intersection, and dest will contain + // the intersection we want to retain for the next interation. + + // We swap the two, so that we can use `source` as the basis for the next iteration. + std::swap(source, dest); + + // Then clear the `dest` without reallocating the underlying hashtable, to avoid + // allocating. + dest.clearWithoutRealloc(); + } + + IntersectionBuilder ib(arena, builtinTypes); + for (auto ty : source) + ib.add(ty); + + return ib.build(); + +} + TypeId TypeSimplifier::intersectUnionWithType(TypeId left, TypeId right) { const UnionType* leftUnion = get(left); @@ -990,58 +1085,117 @@ TypeId TypeSimplifier::intersectNegatedUnion(TypeId left, TypeId right) const UnionType* negatedUnion = get(negatedTy); LUAU_ASSERT(negatedUnion); - bool changed = false; - std::set newParts; - - for (TypeId part : negatedUnion) + if (FFlag::LuauSimplifyIntersectionNoTreeSet) { - Relation r = relate(part, right); - switch (r) + + bool changed = false; + TypeIds newParts; + + for (TypeId part : negatedUnion) { - case Relation::Disjoint: - // If A is disjoint from B, then ~A & B is just B. - // - // ~(false?) & true - // (~false & true) & (~nil & true) - // true & true - newParts.insert(right); - break; - case Relation::Coincident: - // If A is coincident with or a superset of B, then ~A & B is never. - // - // ~(false?) & false - // (~false & false) & (~nil & false) - // never & false - // - // fallthrough - case Relation::Superset: - // If A is a superset of B, then ~A & B is never. + Relation r = relate(part, right); + switch (r) + { + case Relation::Disjoint: + // If A is disjoint from B, then ~A & B is just B. + // + // ~(false?) & true + // (~false & true) & (~nil & true) + // true & true + newParts.insert(right); + break; + case Relation::Coincident: + // If A is coincident with or a superset of B, then ~A & B is never. // - // ~(boolean | nil) & true - // (~boolean & true) & (~boolean & nil) - // never & nil - return builtinTypes->neverType; - case Relation::Subset: - case Relation::Intersects: - // If A is a subset of B, then ~A & B is a bit more complicated. We need to think harder. + // ~(false?) & false + // (~false & false) & (~nil & false) + // never & false // - // ~(false?) & boolean - // (~false & boolean) & (~nil & boolean) - // true & boolean - TypeId simplified = intersectTypeWithNegation(mkNegation(part), right); - changed |= simplified != right; - if (get(simplified)) - changed = true; - else - newParts.insert(simplified); - break; + // fallthrough + case Relation::Superset: + // If A is a superset of B, then ~A & B is never. + // + // ~(boolean | nil) & true + // (~boolean & true) & (~boolean & nil) + // never & nil + return builtinTypes->neverType; + case Relation::Subset: + case Relation::Intersects: + // If A is a subset of B, then ~A & B is a bit more complicated. We need to think harder. + // + // ~(false?) & boolean + // (~false & boolean) & (~nil & boolean) + // true & boolean + TypeId simplified = intersectTypeWithNegation(mkNegation(part), right); + changed |= simplified != right; + if (get(simplified)) + changed = true; + else + newParts.insert(simplified); + break; + } } - } - if (!changed) - return right; - else + if (!changed) + return right; + return intersectFromParts(std::move(newParts)); + } + else + { + + bool changed = false; + std::set newParts; + + for (TypeId part : negatedUnion) + { + Relation r = relate(part, right); + switch (r) + { + case Relation::Disjoint: + // If A is disjoint from B, then ~A & B is just B. + // + // ~(false?) & true + // (~false & true) & (~nil & true) + // true & true + newParts.insert(right); + break; + case Relation::Coincident: + // If A is coincident with or a superset of B, then ~A & B is never. + // + // ~(false?) & false + // (~false & false) & (~nil & false) + // never & false + // + // fallthrough + case Relation::Superset: + // If A is a superset of B, then ~A & B is never. + // + // ~(boolean | nil) & true + // (~boolean & true) & (~boolean & nil) + // never & nil + return builtinTypes->neverType; + case Relation::Subset: + case Relation::Intersects: + // If A is a subset of B, then ~A & B is a bit more complicated. We need to think harder. + // + // ~(false?) & boolean + // (~false & boolean) & (~nil & boolean) + // true & boolean + TypeId simplified = intersectTypeWithNegation(mkNegation(part), right); + changed |= simplified != right; + if (get(simplified)) + changed = true; + else + newParts.insert(simplified); + break; + } + } + + if (!changed) + return right; + return intersectFromParts_DEPRECATED(std::move(newParts)); + } } std::optional TypeSimplifier::basicIntersectWithTruthy(TypeId target) const @@ -1140,50 +1294,100 @@ TypeId TypeSimplifier::intersectTypeWithNegation(TypeId left, TypeId right) if (auto ut = get(negatedTy)) { - // ~(A | B) & C - // (~A & C) & (~B & C) - bool changed = false; - std::set newParts; - - for (TypeId part : ut) + if (FFlag::LuauSimplifyIntersectionNoTreeSet) { - Relation r = relate(part, right); - switch (r) + // ~(A | B) & C + // (~A & C) & (~B & C) + bool changed = false; + TypeIds newParts; + + for (TypeId part : ut) { - case Relation::Coincident: - // ~(false?) & nil - // (~false & nil) & (~nil & nil) - // nil & never - // - // fallthrough - case Relation::Superset: - // ~(boolean | string) & true - // (~boolean & true) & (~boolean & string) - // never & string + Relation r = relate(part, right); + switch (r) + { + case Relation::Coincident: + // ~(false?) & nil + // (~false & nil) & (~nil & nil) + // nil & never + // + // fallthrough + case Relation::Superset: + // ~(boolean | string) & true + // (~boolean & true) & (~boolean & string) + // never & string - return builtinTypes->neverType; + return builtinTypes->neverType; - case Relation::Disjoint: - // ~nil & boolean - newParts.insert(right); - break; + case Relation::Disjoint: + // ~nil & boolean + newParts.insert(right); + break; - case Relation::Subset: - // ~false & boolean - // fallthrough - case Relation::Intersects: - // FIXME: The mkNegation here is pretty unfortunate. - // Memoizing this will probably be important. - changed = true; - newParts.insert(right); - newParts.insert(mkNegation(part)); + case Relation::Subset: + // ~false & boolean + // fallthrough + case Relation::Intersects: + // FIXME: The mkNegation here is pretty unfortunate. + // Memoizing this will probably be important. + changed = true; + newParts.insert(right); + newParts.insert(mkNegation(part)); + } } - } - if (!changed) - return right; - else + if (!changed) + return right; + return intersectFromParts(std::move(newParts)); + } + else + { + // ~(A | B) & C + // (~A & C) & (~B & C) + bool changed = false; + std::set newParts; + + for (TypeId part : ut) + { + Relation r = relate(part, right); + switch (r) + { + case Relation::Coincident: + // ~(false?) & nil + // (~false & nil) & (~nil & nil) + // nil & never + // + // fallthrough + case Relation::Superset: + // ~(boolean | string) & true + // (~boolean & true) & (~boolean & string) + // never & string + + return builtinTypes->neverType; + + case Relation::Disjoint: + // ~nil & boolean + newParts.insert(right); + break; + + case Relation::Subset: + // ~false & boolean + // fallthrough + case Relation::Intersects: + // FIXME: The mkNegation here is pretty unfortunate. + // Memoizing this will probably be important. + changed = true; + newParts.insert(right); + newParts.insert(mkNegation(part)); + } + } + + if (!changed) + return right; + + return intersectFromParts_DEPRECATED(std::move(newParts)); + } } if (auto rightUnion = get(right)) @@ -1311,49 +1515,101 @@ TypeId TypeSimplifier::intersectIntersectionWithType(TypeId left, TypeId right) return arena->addType(IntersectionType{{left, right}}); } - bool changed = false; - std::set newParts; - - for (TypeId part : leftIntersection) + if (FFlag::LuauSimplifyIntersectionNoTreeSet) { - Relation r = relate(part, right); - switch (r) + bool changed = false; + TypeIds newParts; + + for (TypeId part : leftIntersection) { - case Relation::Disjoint: - return builtinTypes->neverType; - case Relation::Coincident: - newParts.insert(part); - continue; - case Relation::Subset: - newParts.insert(part); - continue; - case Relation::Superset: - newParts.insert(right); - changed = true; - continue; - default: - newParts.insert(part); - newParts.insert(right); - changed = true; - continue; + Relation r = relate(part, right); + switch (r) + { + case Relation::Disjoint: + return builtinTypes->neverType; + case Relation::Coincident: + newParts.insert(part); + continue; + case Relation::Subset: + newParts.insert(part); + continue; + case Relation::Superset: + newParts.insert(right); + changed = true; + continue; + default: + newParts.insert(part); + newParts.insert(right); + changed = true; + continue; + } } - } - // It is sometimes the case that an intersection operation will result in - // clipping a free type from the result. - // - // eg (number & 'a) & string --> never - // - // We want to only report the free types that are part of the result. - for (TypeId part : newParts) - { - if (isTypeVariable(part)) - blockedTypes.insert(part); + // It is sometimes the case that an intersection operation will result in + // clipping a free type from the result. + // + // eg (number & 'a) & string --> never + // + // We want to only report the free types that are part of the result. + for (TypeId part : newParts) + { + if (isTypeVariable(part)) + blockedTypes.insert(part); + } + + if (!changed) + return left; + + return intersectFromParts(std::move(newParts)); } + else + { - if (!changed) - return left; - return intersectFromParts(std::move(newParts)); + bool changed = false; + std::set newParts; + + for (TypeId part : leftIntersection) + { + Relation r = relate(part, right); + switch (r) + { + case Relation::Disjoint: + return builtinTypes->neverType; + case Relation::Coincident: + newParts.insert(part); + continue; + case Relation::Subset: + newParts.insert(part); + continue; + case Relation::Superset: + newParts.insert(right); + changed = true; + continue; + default: + newParts.insert(part); + newParts.insert(right); + changed = true; + continue; + } + } + + // It is sometimes the case that an intersection operation will result in + // clipping a free type from the result. + // + // eg (number & 'a) & string --> never + // + // We want to only report the free types that are part of the result. + for (TypeId part : newParts) + { + if (isTypeVariable(part)) + blockedTypes.insert(part); + } + + if (!changed) + return left; + + return intersectFromParts_DEPRECATED(std::move(newParts)); + } } std::optional TypeSimplifier::basicIntersect(TypeId left, TypeId right) @@ -1414,9 +1670,12 @@ 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(); auto it = rt->props.find(propName); - if (it != rt->props.end() && leftProp.isShared() && it->second.isShared()) + if (it != rt->props.end() && leftPropIsRefinable && it->second.isShared()) { Relation r = relate(*leftProp.readTy, *it->second.readTy); @@ -1428,7 +1687,7 @@ std::optional TypeSimplifier::basicIntersect(TypeId left, TypeId right) case Relation::Coincident: return right; case Relation::Subset: - if (1 == rt->props.size()) + if (1 == rt->props.size() && leftProp.isShared()) return left; break; default: @@ -1457,11 +1716,25 @@ std::optional TypeSimplifier::basicIntersect(TypeId left, TypeId right) if (areDisjoint) { - TableType::Props mergedProps = lt->props; - for (const auto& [name, rightProp] : rt->props) - mergedProps[name] = rightProp; - return arena->addType(TableType{mergedProps, std::nullopt, TypeLevel{}, lt->scope, TableState::Sealed}); + if (FFlag::LuauSimplifyMoveTableProps) + { + TableType merged{TableState::Sealed, TypeLevel{}, lt->scope}; + merged.props = lt->props; + + for (const auto& [name, rightProp] : rt->props) + merged.props[name] = rightProp; + + return arena->addType(std::move(merged)); + } + else + { + TableType::Props mergedProps = lt->props; + for (const auto& [name, rightProp] : rt->props) + mergedProps[name] = rightProp; + + return arena->addType(TableType{mergedProps, std::nullopt, TypeLevel{}, lt->scope, TableState::Sealed}); + } } } @@ -1517,9 +1790,9 @@ TypeId TypeSimplifier::intersect(TypeId left, TypeId right) return right; if (get(right) && !get(left)) return left; - if (FFlag::LuauSimplifyAnyAndUnion && get(left) && get(right)) + if (get(left) && get(right)) return union_(builtinTypes->errorType, right); - if (FFlag::LuauSimplifyAnyAndUnion && get(left) && get(right)) + if (get(left) && get(right)) return union_(builtinTypes->errorType, left); if (get(left)) return arena->addType(UnionType{{right, builtinTypes->errorType}}); @@ -1565,19 +1838,19 @@ TypeId TypeSimplifier::intersect(TypeId left, TypeId right) return arena->addType(IntersectionType{{left, right}}); } - if (auto ut = get(left)) + if (get(left)) { if (get(right)) return intersectUnions(left, right); else return intersectUnionWithType(left, right); } - else if (auto ut = get(right)) + else if (get(right)) return intersectUnionWithType(right, left); - if (auto it = get(left)) + if (get(left)) return intersectIntersectionWithType(left, right); - else if (auto it = get(right)) + else if (get(right)) return intersectIntersectionWithType(right, left); if (get(left)) @@ -2087,7 +2360,7 @@ SimplifyResult simplifyIntersection(NotNull builtinTypes, NotNull< return SimplifyResult{res, std::move(s.blockedTypes)}; } -SimplifyResult simplifyIntersection(NotNull builtinTypes, NotNull arena, std::set parts) +SimplifyResult simplifyIntersection(NotNull builtinTypes, NotNull arena, TypeIds parts) { TypeSimplifier s{builtinTypes, arena}; @@ -2096,6 +2369,15 @@ SimplifyResult simplifyIntersection(NotNull builtinTypes, NotNull< return SimplifyResult{res, std::move(s.blockedTypes)}; } +SimplifyResult simplifyIntersection_DEPRECATED(NotNull builtinTypes, NotNull arena, std::set parts) +{ + TypeSimplifier s{builtinTypes, arena}; + + TypeId res = s.intersectFromParts_DEPRECATED(std::move(parts)); + + return SimplifyResult{res, std::move(s.blockedTypes)}; +} + SimplifyResult simplifyUnion(NotNull builtinTypes, NotNull arena, TypeId left, TypeId right) { TypeSimplifier s{builtinTypes, arena}; diff --git a/Analysis/src/Subtyping.cpp b/Analysis/src/Subtyping.cpp index b62b4da7..f4b8361a 100644 --- a/Analysis/src/Subtyping.cpp +++ b/Analysis/src/Subtyping.cpp @@ -8,6 +8,7 @@ #include "Luau/Normalize.h" #include "Luau/RecursionCounter.h" #include "Luau/Scope.h" +#include "Luau/ScopedSeenSet.h" #include "Luau/Substitution.h" #include "Luau/TxnLog.h" #include "Luau/Type.h" @@ -18,16 +19,20 @@ #include "Luau/TypePath.h" #include "Luau/TypeUtils.h" +LUAU_FASTFLAGVARIABLE(LuauIndividualRecursionLimits) +LUAU_DYNAMIC_FASTINTVARIABLE(LuauSubtypingRecursionLimit, 100) + LUAU_FASTFLAGVARIABLE(DebugLuauSubtypingCheckPathValidity) LUAU_FASTINTVARIABLE(LuauSubtypingReasoningLimit, 100) -LUAU_FASTFLAGVARIABLE(LuauReturnMappedGenericPacksFromSubtyping2) -LUAU_FASTFLAGVARIABLE(LuauSubtypingNegationsChecksNormalizationComplexity) -LUAU_FASTFLAGVARIABLE(LuauSubtypingGenericsDoesntUseVariance) LUAU_FASTFLAG(LuauEmplaceNotPushBack) LUAU_FASTFLAGVARIABLE(LuauSubtypingReportGenericBoundMismatches2) LUAU_FASTFLAGVARIABLE(LuauTrackUniqueness) -LUAU_FASTFLAGVARIABLE(LuauSubtypingGenericPacksDoesntUseVariance) LUAU_FASTFLAGVARIABLE(LuauSubtypingUnionsAndIntersectionsInGenericBounds) +LUAU_FASTFLAGVARIABLE(LuauIndexInMetatableSubtyping) +LUAU_FASTFLAGVARIABLE(LuauSubtypingPackRecursionLimits) +LUAU_FASTFLAGVARIABLE(LuauSubtypingPrimitiveAndGenericTableTypes) +LUAU_FASTFLAGVARIABLE(LuauPassBindableGenericsByReference) +LUAU_FASTFLAGVARIABLE(LuauTryFindSubstitutionReturnOptional) namespace Luau { @@ -75,13 +80,10 @@ MappedGenericEnvironment::MappedGenericFrame::MappedGenericFrame( : mappings(std::move(mappings)) , parentScopeIndex(parentScopeIndex) { - LUAU_ASSERT(FFlag::LuauSubtypingGenericPacksDoesntUseVariance); } MappedGenericEnvironment::LookupResult MappedGenericEnvironment::lookupGenericPack(TypePackId genericTp) const { - LUAU_ASSERT(FFlag::LuauSubtypingGenericPacksDoesntUseVariance); - genericTp = follow(genericTp); std::optional currentFrameIndex = currentScopeIndex; @@ -129,8 +131,6 @@ MappedGenericEnvironment::LookupResult MappedGenericEnvironment::lookupGenericPa void MappedGenericEnvironment::pushFrame(const std::vector& genericTps) { - LUAU_ASSERT(FFlag::LuauSubtypingGenericPacksDoesntUseVariance); - DenseHashMap> mappings{nullptr}; for (TypePackId tp : genericTps) @@ -148,7 +148,6 @@ void MappedGenericEnvironment::pushFrame(const std::vector& genericT void MappedGenericEnvironment::popFrame() { - LUAU_ASSERT(FFlag::LuauSubtypingGenericPacksDoesntUseVariance); LUAU_ASSERT(currentScopeIndex); if (currentScopeIndex) { @@ -159,7 +158,6 @@ void MappedGenericEnvironment::popFrame() bool MappedGenericEnvironment::bindGeneric(TypePackId genericTp, TypePackId bindeeTp) { - LUAU_ASSERT(FFlag::LuauSubtypingGenericPacksDoesntUseVariance); // We shouldn't bind generic type packs to themselves if (genericTp == bindeeTp) return true; @@ -199,37 +197,16 @@ static void assertReasoningValid_DEPRECATED(TID subTy, TID superTy, const Subtyp template static void assertReasoningValid(TID subTy, TID superTy, const SubtypingResult& result, NotNull builtinTypes, NotNull arena) { - LUAU_ASSERT(FFlag::LuauReturnMappedGenericPacksFromSubtyping2); - if (!FFlag::DebugLuauSubtypingCheckPathValidity) return; for (const SubtypingReasoning& reasoning : result.reasoning) { - if (FFlag::LuauSubtypingGenericPacksDoesntUseVariance) - { - LUAU_ASSERT(traverse(subTy, reasoning.subPath, builtinTypes, arena)); - LUAU_ASSERT(traverse(superTy, reasoning.superPath, builtinTypes, arena)); - } - else - { - LUAU_ASSERT(traverse_DEPRECATED(subTy, reasoning.subPath, builtinTypes, NotNull{&result.mappedGenericPacks_DEPRECATED}, arena)); - LUAU_ASSERT(traverse_DEPRECATED(superTy, reasoning.superPath, builtinTypes, NotNull{&result.mappedGenericPacks_DEPRECATED}, arena)); - } + LUAU_ASSERT(traverse(subTy, reasoning.subPath, builtinTypes, arena)); + LUAU_ASSERT(traverse(superTy, reasoning.superPath, builtinTypes, arena)); } } -template<> -void assertReasoningValid_DEPRECATED( - TableIndexer subIdx, - TableIndexer superIdx, - const SubtypingResult& result, - NotNull builtinTypes -) -{ - // Empty method to satisfy the compiler. -} - template<> void assertReasoningValid( TableIndexer subIdx, @@ -432,20 +409,10 @@ struct ApplyMappedGenerics : Substitution { NotNull builtinTypes; NotNull arena; - // TODO: make this NotNull when LuauSubtypingGenericsDoesntUseVariance is clipped - InternalErrorReporter* iceReporter; + NotNull iceReporter; SubtypingEnvironment& env; - // TODO: Clip with LuauSubtypingGenericsDoesntUseVariance - ApplyMappedGenerics(NotNull builtinTypes, NotNull arena, SubtypingEnvironment& env) - : Substitution(TxnLog::empty(), arena) - , builtinTypes(builtinTypes) - , arena(arena) - , env(env) - { - } - ApplyMappedGenerics( NotNull builtinTypes, NotNull arena, @@ -472,100 +439,79 @@ struct ApplyMappedGenerics : Substitution TypeId clean(TypeId ty) override { - if (FFlag::LuauSubtypingGenericsDoesntUseVariance) + const auto& [lowerBound, upperBound] = env.getMappedTypeBounds(ty, NotNull{iceReporter}); + + if (upperBound.empty() && lowerBound.empty()) + { + // No bounds for the generic we're mapping. + // In this case, unknown vs never is an arbitrary choice: + // ie, does it matter if we map add to add or add in the context of subtyping? + // We choose unknown here, since it's closest to the original behavior. + return builtinTypes->unknownType; + } + else if (!upperBound.empty()) { - const auto& [lowerBound, upperBound] = env.getMappedTypeBounds(ty, NotNull{iceReporter}); + TypeIds boundsToUse; - if (upperBound.empty() && lowerBound.empty()) + for (TypeId ub : upperBound) { - // No bounds for the generic we're mapping. + // quick and dirty check to avoid adding generic types + if (!get(ub)) + boundsToUse.insert(ub); + } + + if (boundsToUse.empty()) + { + // This case happens when we've collected no bounds for the generic we're mapping. // In this case, unknown vs never is an arbitrary choice: // ie, does it matter if we map add to add or add in the context of subtyping? // We choose unknown here, since it's closest to the original behavior. return builtinTypes->unknownType; } - else if (!upperBound.empty()) - { - TypeIds boundsToUse; - - for (TypeId ub : upperBound) - { - // quick and dirty check to avoid adding generic types - if (!get(ub)) - boundsToUse.insert(ub); - } + if (boundsToUse.size() == 1) + return *boundsToUse.begin(); - if (boundsToUse.empty()) - { - // This case happens when we've collected no bounds for the generic we're mapping. - // In this case, unknown vs never is an arbitrary choice: - // ie, does it matter if we map add to add or add in the context of subtyping? - // We choose unknown here, since it's closest to the original behavior. - return builtinTypes->unknownType; - } - if (boundsToUse.size() == 1) - return *boundsToUse.begin(); + return arena->addType(IntersectionType{boundsToUse.take()}); + } + else if (!lowerBound.empty()) + { + TypeIds boundsToUse; - return arena->addType(IntersectionType{boundsToUse.take()}); - } - else if (!lowerBound.empty()) + for (TypeId lb : lowerBound) { - TypeIds boundsToUse; - - for (TypeId lb : lowerBound) - { - // quick and dirty check to avoid adding generic types - if (!get(lb)) - boundsToUse.insert(lb); - } - - if (boundsToUse.empty()) - { - // This case happens when we've collected no bounds for the generic we're mapping. - // In this case, unknown vs never is an arbitrary choice: - // ie, does it matter if we map add to add or add in the context of subtyping? - // We choose unknown here, since it's closest to the original behavior. - return builtinTypes->unknownType; - } - else if (lowerBound.size() == 1) - return *boundsToUse.begin(); - else - return arena->addType(UnionType{boundsToUse.take()}); + // quick and dirty check to avoid adding generic types + if (!get(lb)) + boundsToUse.insert(lb); } - else + + if (boundsToUse.empty()) { - LUAU_ASSERT(!"Unreachable path"); + // This case happens when we've collected no bounds for the generic we're mapping. + // In this case, unknown vs never is an arbitrary choice: + // ie, does it matter if we map add to add or add in the context of subtyping? + // We choose unknown here, since it's closest to the original behavior. return builtinTypes->unknownType; } + else if (lowerBound.size() == 1) + return *boundsToUse.begin(); + else + return arena->addType(UnionType{boundsToUse.take()}); } else { - const auto& bounds = env.getMappedTypeBounds_DEPRECATED(ty); - - if (bounds.upperBound.empty()) - return builtinTypes->unknownType; - - if (bounds.upperBound.size() == 1) - return *begin(bounds.upperBound); - - return arena->addType(IntersectionType{std::vector(begin(bounds.upperBound), end(bounds.upperBound))}); + LUAU_ASSERT(!"Unreachable path"); + return builtinTypes->unknownType; } } TypePackId clean(TypePackId tp) override { - if (FFlag::LuauSubtypingGenericPacksDoesntUseVariance) - { - const MappedGenericEnvironment::LookupResult result = env.mappedGenericPacks.lookupGenericPack(tp); - if (const TypePackId* mappedGen = get_if(&result)) - return *mappedGen; - } - else if (auto it = env.getMappedPackBounds_DEPRECATED(tp)) - return *it; - + const MappedGenericEnvironment::LookupResult result = env.lookupGenericPack(tp); + if (const TypePackId* mappedGen = get_if(&result)) + return *mappedGen; // Clean is only called when isDirty found a pack bound LUAU_ASSERT(!"Unreachable"); - return nullptr; + return builtinTypes->anyTypePack; } bool ignoreChildren(TypeId ty) override @@ -573,16 +519,13 @@ struct ApplyMappedGenerics : Substitution if (get(ty)) return true; - if (FFlag::LuauSubtypingGenericsDoesntUseVariance) + if (const FunctionType* f = get(ty)) { - if (const FunctionType* f = get(ty)) + for (TypeId g : f->generics) { - for (TypeId g : f->generics) - { - if (const std::vector* bounds = env.mappedGenerics.find(g); bounds && !bounds->empty()) - // We don't want to mutate the generics of a function that's being subtyped - return true; - } + if (const std::vector* bounds = env.mappedGenerics.find(g); bounds && !bounds->empty()) + // We don't want to mutate the generics of a function that's being subtyped + return true; } } @@ -601,36 +544,39 @@ std::optional SubtypingEnvironment::applyMappedGenerics( NotNull iceReporter ) { - LUAU_ASSERT(FFlag::LuauSubtypingGenericsDoesntUseVariance); ApplyMappedGenerics amg{builtinTypes, arena, *this, iceReporter}; return amg.substitute(ty); } -std::optional SubtypingEnvironment::applyMappedGenerics_DEPRECATED(NotNull builtinTypes, NotNull arena, TypeId ty) +const TypeId* SubtypingEnvironment::tryFindSubstitution_DEPRECATED(TypeId ty) const { - ApplyMappedGenerics amg{builtinTypes, arena, *this}; - return amg.substitute(ty); -} + LUAU_ASSERT(!FFlag::LuauTryFindSubstitutionReturnOptional); -const TypeId* SubtypingEnvironment::tryFindSubstitution(TypeId ty) const -{ if (auto it = substitutions.find(ty)) return it; if (parent) - return parent->tryFindSubstitution(ty); + return parent->tryFindSubstitution_DEPRECATED(ty); return nullptr; } +std::optional SubtypingEnvironment::tryFindSubstitution(TypeId ty) const +{ + LUAU_ASSERT(FFlag::LuauTryFindSubstitutionReturnOptional); + + if (const TypeId* it = substitutions.find(ty)) + return *it; + + if (parent) + return parent->tryFindSubstitution(ty); + + return std::nullopt; +} + const SubtypingResult* SubtypingEnvironment::tryFindSubtypingResult(std::pair subAndSuper) const { - if (FFlag::LuauSubtypingGenericsDoesntUseVariance) - { - if (const auto it = seenSetCache.find(subAndSuper)) - return it; - } - else if (auto it = ephemeralCache.find(subAndSuper)) + if (const auto it = seenSetCache.find(subAndSuper)) return it; if (parent) @@ -641,37 +587,19 @@ const SubtypingResult* SubtypingEnvironment::tryFindSubtypingResult(std::pairempty()) - return true; - - if (parent) - return parent->containsMappedType(ty); - - return false; - } - else - { - if (mappedGenerics_DEPRECATED.contains(ty)) - return true; + ty = follow(ty); + if (const auto bounds = mappedGenerics.find(ty); bounds && !bounds->empty()) + return true; - if (parent) - return parent->containsMappedType(ty); + if (parent) + return parent->containsMappedType(ty); - return false; - } + return false; } bool SubtypingEnvironment::containsMappedPack(TypePackId tp) const { - if (FFlag::LuauSubtypingGenericPacksDoesntUseVariance) - { - if (const MappedGenericEnvironment::LookupResult lookupResult = mappedGenericPacks.lookupGenericPack(tp); get_if(&lookupResult)) - return true; - } - else if (mappedGenericPacks_DEPRECATED.contains(tp)) + if (const MappedGenericEnvironment::LookupResult lookupResult = mappedGenericPacks.lookupGenericPack(tp); get_if(&lookupResult)) return true; if (parent) @@ -682,7 +610,6 @@ bool SubtypingEnvironment::containsMappedPack(TypePackId tp) const SubtypingEnvironment::GenericBounds& SubtypingEnvironment::getMappedTypeBounds(TypeId ty, NotNull iceReporter) { - LUAU_ASSERT(FFlag::LuauSubtypingGenericsDoesntUseVariance); ty = follow(ty); std::vector* bounds = mappedGenerics.find(ty); if (bounds && !bounds->empty()) @@ -695,31 +622,15 @@ SubtypingEnvironment::GenericBounds& SubtypingEnvironment::getMappedTypeBounds(T iceReporter->ice("Trying to access bounds for a type with no in-scope bounds"); } -SubtypingEnvironment::GenericBounds_DEPRECATED& SubtypingEnvironment::getMappedTypeBounds_DEPRECATED(TypeId ty) -{ - LUAU_ASSERT(!FFlag::LuauSubtypingGenericsDoesntUseVariance); - if (auto it = mappedGenerics_DEPRECATED.find(ty)) - return *it; - - if (parent) - return parent->getMappedTypeBounds_DEPRECATED(ty); - - LUAU_ASSERT(!"Use containsMappedType before asking for bounds!"); - return mappedGenerics_DEPRECATED[ty]; -} - -TypePackId* SubtypingEnvironment::getMappedPackBounds_DEPRECATED(TypePackId tp) +MappedGenericEnvironment::LookupResult SubtypingEnvironment::lookupGenericPack(TypePackId tp) const { - LUAU_ASSERT(!FFlag::LuauSubtypingGenericPacksDoesntUseVariance); - - if (auto it = mappedGenericPacks_DEPRECATED.find(tp)) - return it; - - if (parent) - return parent->getMappedPackBounds_DEPRECATED(tp); - - // This fallback is reachable in valid cases, unlike the final part of getMappedTypeBounds - return nullptr; + MappedGenericEnvironment::LookupResult result = mappedGenericPacks.lookupGenericPack(tp); + if (get_if(&result)) + return result; + else if (parent) + return parent->lookupGenericPack(tp); + else + return result; } Subtyping::Subtyping( @@ -753,52 +664,8 @@ SubtypingResult Subtyping::isSubtype(TypeId subTy, TypeId superTy, NotNull(lb, builtinTypes->neverType); - TypeId upperBound = makeAggregateType(ub, builtinTypes->unknownType); - - std::shared_ptr nt = normalizer->normalize(upperBound); - // we say that the result is true if normalization failed because complex types are likely to be inhabited. - NormalizationResult res = nt ? normalizer->isInhabited(nt.get()) : NormalizationResult::True; - - if (!nt || res == NormalizationResult::HitLimits) - result.normalizationTooComplex = true; - else if (res == NormalizationResult::False) - { - /* If the normalized upper bound we're mapping to a generic is - * uninhabited, then we must consider the subtyping relation not to - * hold. - * - * This happens eg in () -> (T, T) <: () -> (string, number) - * - * T appears in covariant position and would have to be both string - * and number at once. - * - * No actual value is both a string and a number, so the test fails. - * - * TODO: We'll need to add explanitory context here. - */ - result.isSubtype = false; - } - - SubtypingEnvironment boundsEnv; - boundsEnv.parent = &env; - SubtypingResult boundsResult = isCovariantWith(boundsEnv, lowerBound, upperBound, scope); - boundsResult.reasoning.clear(); - - result.andAlso(boundsResult); - } - } + for (const auto& [_, bounds] : env.mappedGenerics) + LUAU_ASSERT(bounds.empty()); /* TODO: We presently don't store subtype test results in the persistent * cache if the left-side type is a generic function. @@ -812,19 +679,57 @@ SubtypingResult Subtyping::isSubtype(TypeId subTy, TypeId superTy, NotNull scope, const std::vector& bindableGenerics) +{ + LUAU_ASSERT(FFlag::LuauPassBindableGenericsByReference); + + SubtypingEnvironment env; + for (TypeId g : bindableGenerics) + env.mappedGenerics[follow(g)] = {SubtypingEnvironment::GenericBounds{}}; + + SubtypingResult result = isCovariantWith(env, subTp, superTp, scope); + + for (TypeId bg : bindableGenerics) + { + bg = follow(bg); + + LUAU_ASSERT(env.mappedGenerics.contains(bg)); + + if (const std::vector* bounds = env.mappedGenerics.find(bg)) + { + // Bounds should have exactly one entry + LUAU_ASSERT(bounds->size() == 1); + if (FFlag::LuauSubtypingReportGenericBoundMismatches2) + { + if (bounds->empty()) + continue; + if (const GenericType* gen = get(bg)) + result.andAlso(checkGenericBounds(bounds->back(), env, scope, gen->name)); + } + else if (!bounds->empty()) + result.andAlso(checkGenericBounds_DEPRECATED(bounds->back(), env, scope)); + } + } + + return result; +} -SubtypingResult Subtyping::isSubtype(TypePackId subTp, TypePackId superTp, NotNull scope, std::optional> bindableGenerics) +SubtypingResult Subtyping::isSubtype_DEPRECATED( + TypePackId subTp, + TypePackId superTp, + NotNull scope, + std::optional> bindableGenerics +) { + LUAU_ASSERT(!FFlag::LuauPassBindableGenericsByReference); + SubtypingEnvironment env; - if (FFlag::LuauSubtypingGenericsDoesntUseVariance && bindableGenerics) + if (bindableGenerics) { for (TypeId g : *bindableGenerics) env.mappedGenerics[follow(g)] = {SubtypingEnvironment::GenericBounds{}}; @@ -832,10 +737,7 @@ SubtypingResult Subtyping::isSubtype(TypePackId subTp, TypePackId superTp, NotNu SubtypingResult result = isCovariantWith(env, subTp, superTp, scope); - if (FFlag::LuauReturnMappedGenericPacksFromSubtyping2 && FFlag::LuauSubtypingGenericPacksDoesntUseVariance) - result.mappedGenericPacks_DEPRECATED = std::move(env.mappedGenericPacks_DEPRECATED); - - if (FFlag::LuauSubtypingGenericsDoesntUseVariance && bindableGenerics) + if (bindableGenerics) { for (TypeId bg : *bindableGenerics) { @@ -867,99 +769,56 @@ SubtypingResult Subtyping::cache(SubtypingEnvironment& env, SubtypingResult resu { const std::pair p{subTy, superTy}; - if (FFlag::LuauReturnMappedGenericPacksFromSubtyping2 && !FFlag::LuauSubtypingGenericPacksDoesntUseVariance) - result.mappedGenericPacks_DEPRECATED = env.mappedGenericPacks_DEPRECATED; - if (result.isCacheable) resultCache[p] = result; - else if (!FFlag::LuauSubtypingGenericsDoesntUseVariance) - env.ephemeralCache[p] = result; return result; } -namespace -{ -struct SeenSetPopper +SubtypingResult Subtyping::isCovariantWith(SubtypingEnvironment& env, TypeId subTy, TypeId superTy, NotNull scope) { - Subtyping::SeenSet* seenTypes; - std::pair pair; - - SeenSetPopper(Subtyping::SeenSet* seenTypes, std::pair pair) - : seenTypes(seenTypes) - , pair(pair) + UnifierCounters& counters = normalizer->sharedState->counters; + RecursionCounter rc(&counters.recursionCount); + if (FFlag::LuauIndividualRecursionLimits) { + if (DFInt::LuauSubtypingRecursionLimit > 0 && DFInt::LuauSubtypingRecursionLimit < counters.recursionCount) + return SubtypingResult{false, true}; } - - ~SeenSetPopper() + else { - seenTypes->erase(pair); + if (counters.recursionLimit > 0 && counters.recursionLimit < counters.recursionCount) + return SubtypingResult{false, true}; } -}; -struct SeenTypePackSetPopper -{ - Subtyping::SeenTypePackSet* seenTypes; - std::pair pair; + subTy = follow(subTy); + superTy = follow(superTy); - SeenTypePackSetPopper(Subtyping::SeenTypePackSet* seenTypes, std::pair pair) - : seenTypes(seenTypes) - , pair(std::move(pair)) + if (FFlag::LuauTryFindSubstitutionReturnOptional) { - LUAU_ASSERT(FFlag::LuauReturnMappedGenericPacksFromSubtyping2); - } - - SeenTypePackSetPopper(const SeenTypePackSetPopper&) = delete; - SeenTypePackSetPopper& operator=(const SeenTypePackSetPopper&) = delete; - SeenTypePackSetPopper(SeenTypePackSetPopper&&) = delete; - SeenTypePackSetPopper& operator=(SeenTypePackSetPopper&&) = delete; + if (std::optional subIt = env.tryFindSubstitution(subTy); subIt && *subIt) + subTy = *subIt; - ~SeenTypePackSetPopper() - { - LUAU_ASSERT(FFlag::LuauReturnMappedGenericPacksFromSubtyping2); - seenTypes->erase(pair); + if (std::optional superIt = env.tryFindSubstitution(superTy); superIt && *superIt) + subTy = *superIt; } -}; -} // namespace - -SubtypingResult Subtyping::isCovariantWith(SubtypingEnvironment& env, TypeId subTy, TypeId superTy, NotNull scope) -{ - UnifierCounters& counters = normalizer->sharedState->counters; - RecursionCounter rc(&counters.recursionCount); - - if (counters.recursionLimit > 0 && counters.recursionLimit < counters.recursionCount) - return SubtypingResult{false, true}; - - subTy = follow(subTy); - superTy = follow(superTy); - - if (const TypeId* subIt = env.tryFindSubstitution(subTy); subIt && *subIt) - subTy = *subIt; + else + { + if (const TypeId* subIt = env.tryFindSubstitution_DEPRECATED(subTy); subIt && *subIt) + subTy = *subIt; - if (const TypeId* superIt = env.tryFindSubstitution(superTy); superIt && *superIt) - superTy = *superIt; + if (const TypeId* superIt = env.tryFindSubstitution_DEPRECATED(superTy); superIt && *superIt) + superTy = *superIt; + } const SubtypingResult* cachedResult = resultCache.find({subTy, superTy}); if (cachedResult) { - if (FFlag::LuauReturnMappedGenericPacksFromSubtyping2 && !FFlag::LuauSubtypingGenericPacksDoesntUseVariance) - { - for (const auto& [genericTp, boundTp] : cachedResult->mappedGenericPacks_DEPRECATED) - env.mappedGenericPacks_DEPRECATED.try_insert(genericTp, boundTp); - } - return *cachedResult; } cachedResult = env.tryFindSubtypingResult({subTy, superTy}); if (cachedResult) { - if (FFlag::LuauReturnMappedGenericPacksFromSubtyping2 && !FFlag::LuauSubtypingGenericPacksDoesntUseVariance) - { - for (const auto& [genericTp, boundTp] : cachedResult->mappedGenericPacks_DEPRECATED) - env.mappedGenericPacks_DEPRECATED.try_insert(genericTp, boundTp); - } - return *cachedResult; } @@ -994,13 +853,11 @@ SubtypingResult Subtyping::isCovariantWith(SubtypingEnvironment& env, TypeId sub res.isSubtype = true; res.isCacheable = false; - if (FFlag::LuauSubtypingGenericsDoesntUseVariance) - env.seenSetCache[typePair] = res; + env.seenSetCache[typePair] = res; return res; } - - SeenSetPopper ssp{&seenTypes, typePair}; + ScopedSeenSet ssp{seenTypes, typePair}; // Within the scope to which a generic belongs, that generic should be // tested as though it were its upper bounds. We do not yet support bounded @@ -1058,8 +915,7 @@ SubtypingResult Subtyping::isCovariantWith(SubtypingEnvironment& env, TypeId sub result = {false}; else if (get(subTy)) result = {true}; - else if (auto subTypeFunctionInstance = get(subTy); - subTypeFunctionInstance && FFlag::LuauSubtypingGenericsDoesntUseVariance) + else if (auto subTypeFunctionInstance = get(subTy)) { bool mappedGenericsApplied = false; if (auto substSubTy = env.applyMappedGenerics(builtinTypes, arena, subTy, iceReporter)) @@ -1071,8 +927,7 @@ SubtypingResult Subtyping::isCovariantWith(SubtypingEnvironment& env, TypeId sub result = isCovariantWith(env, subTypeFunctionInstance, superTy, scope); result.isCacheable = !mappedGenericsApplied; } - else if (auto superTypeFunctionInstance = get(superTy); - superTypeFunctionInstance && FFlag::LuauSubtypingGenericsDoesntUseVariance) + else if (auto superTypeFunctionInstance = get(superTy)) { bool mappedGenericsApplied = false; if (auto substSuperTy = env.applyMappedGenerics(builtinTypes, arena, superTy, iceReporter)) @@ -1084,7 +939,7 @@ SubtypingResult Subtyping::isCovariantWith(SubtypingEnvironment& env, TypeId sub result = isCovariantWith(env, subTy, superTypeFunctionInstance, scope); result.isCacheable = !mappedGenericsApplied; } - else if (FFlag::LuauSubtypingGenericsDoesntUseVariance && (get(subTy) || get(superTy))) + else if (get(subTy) || get(superTy)) { if (const auto subBounds = env.mappedGenerics.find(subTy); subBounds && !subBounds->empty()) { @@ -1123,18 +978,6 @@ SubtypingResult Subtyping::isCovariantWith(SubtypingEnvironment& env, TypeId sub if (!result.isSubtype && !result.normalizationTooComplex) result = trySemanticSubtyping(env, subTy, superTy, scope, result); } - else if (!FFlag::LuauSubtypingGenericsDoesntUseVariance && get(subTy) && variance == Variance::Covariant) - { - bool ok = bindGeneric(env, subTy, superTy); - result.isSubtype = ok; - result.isCacheable = false; - } - else if (!FFlag::LuauSubtypingGenericsDoesntUseVariance && get(superTy) && variance == Variance::Contravariant) - { - bool ok = bindGeneric(env, subTy, superTy); - result.isSubtype = ok; - result.isCacheable = false; - } else if (auto pair = get2(subTy, superTy)) { // Any two free types are potentially subtypes of one another because @@ -1179,53 +1022,13 @@ SubtypingResult Subtyping::isCovariantWith(SubtypingEnvironment& env, TypeId sub { result = isCovariantWith(env, subNegation, superTy, scope); if (!result.isSubtype && !result.normalizationTooComplex) - { - if (FFlag::LuauSubtypingNegationsChecksNormalizationComplexity) - result = trySemanticSubtyping(env, subTy, superTy, scope, result); - else - { - SubtypingResult semantic = isCovariantWith(env, normalizer->normalize(subTy), normalizer->normalize(superTy), scope); - if (semantic.isSubtype) - { - semantic.reasoning.clear(); - result = semantic; - } - } - } + result = trySemanticSubtyping(env, subTy, superTy, scope, result); } else if (auto superNegation = get(superTy)) { result = isCovariantWith(env, subTy, superNegation, scope); if (!result.isSubtype && !result.normalizationTooComplex) - { - if (FFlag::LuauSubtypingNegationsChecksNormalizationComplexity) - result = trySemanticSubtyping(env, subTy, superTy, scope, result); - else - { - SubtypingResult semantic = isCovariantWith(env, normalizer->normalize(subTy), normalizer->normalize(superTy), scope); - if (semantic.isSubtype) - { - semantic.reasoning.clear(); - result = semantic; - } - } - } - } - else if (auto subTypeFunctionInstance = get(subTy)) - { - LUAU_ASSERT(!FFlag::LuauSubtypingGenericsDoesntUseVariance); - if (auto substSubTy = env.applyMappedGenerics_DEPRECATED(builtinTypes, arena, subTy)) - subTypeFunctionInstance = get(*substSubTy); - - result = isCovariantWith(env, subTypeFunctionInstance, superTy, scope); - } - else if (auto superTypeFunctionInstance = get(superTy)) - { - LUAU_ASSERT(!FFlag::LuauSubtypingGenericsDoesntUseVariance); - if (auto substSuperTy = env.applyMappedGenerics_DEPRECATED(builtinTypes, arena, superTy)) - superTypeFunctionInstance = get(*substSuperTy); - - result = isCovariantWith(env, subTy, superTypeFunctionInstance, scope); + result = trySemanticSubtyping(env, subTy, superTy, scope, result); } else if (auto p = get2(subTy, superTy)) result = isCovariantWith(env, p, scope); @@ -1265,28 +1068,51 @@ SubtypingResult Subtyping::isCovariantWith(SubtypingEnvironment& env, TypeId sub else if (auto p = get2(subTy, superTy)) result = isCovariantWith(env, p, scope); - if (FFlag::LuauReturnMappedGenericPacksFromSubtyping2) - assertReasoningValid(subTy, superTy, result, builtinTypes, arena); - else - assertReasoningValid_DEPRECATED(subTy, superTy, result, builtinTypes); + assertReasoningValid(subTy, superTy, result, builtinTypes, arena); return cache(env, std::move(result), subTy, superTy); } +/* + * Subtyping of packs is fairly involved. There are three parts to the test. + * + * 1. If both packs have types at their heads, we do a pairwise test for each + * pair of types. + * 2. If the finite parts of the packs are of inequal length and the pack on the + * opposite side has a tail, we test that. (eg + * testing concrete types against variadics or a generic pack) + * 3. Lastly, do a subtype test on non-finite tails. (eg between two generic + * packs or variadics) + */ SubtypingResult Subtyping::isCovariantWith(SubtypingEnvironment& env, TypePackId subTp, TypePackId superTp, NotNull scope) { - subTp = follow(subTp); - superTp = follow(superTp); + UnifierCounters& counters = normalizer->sharedState->counters; + std::optional rc; - std::optional popper = std::nullopt; - if (FFlag::LuauReturnMappedGenericPacksFromSubtyping2) + if (FFlag::LuauSubtypingPackRecursionLimits) { - std::pair typePair = {subTp, superTp}; - if (!seenPacks.insert(typePair)) - return SubtypingResult{true, false, false}; - popper.emplace(&seenPacks, std::move(typePair)); + rc.emplace(&counters.recursionCount); + + if (FFlag::LuauIndividualRecursionLimits) + { + if (DFInt::LuauSubtypingRecursionLimit > 0 && counters.recursionCount > DFInt::LuauSubtypingRecursionLimit) + return SubtypingResult{false, true}; + } + else + { + if (counters.recursionLimit > 0 && counters.recursionLimit < counters.recursionCount) + return SubtypingResult{false, true}; + } } + subTp = follow(subTp); + superTp = follow(superTp); + + std::pair typePair = {subTp, superTp}; + if (!seenPacks.insert(typePair)) + return SubtypingResult{true, false, false}; + ScopedSeenSet> popper{seenPacks, std::move(typePair)}; + auto [subHead, subTail] = flatten(subTp); auto [superHead, superTail] = flatten(superTp); @@ -1311,125 +1137,9 @@ SubtypingResult Subtyping::isCovariantWith(SubtypingEnvironment& env, TypePackId { if (subTail) { - if (auto vt = get(*subTail)) - { - for (size_t i = headSize; i < superHead.size(); ++i) - results.push_back(isCovariantWith(env, vt->ty, superHead[i], scope) - .withSubPath(TypePath::PathBuilder().tail().variadic().build()) - .withSuperComponent(TypePath::Index{i, TypePath::Index::Variant::Pack})); - } - else if (get(*subTail)) - { - if (FFlag::LuauSubtypingGenericPacksDoesntUseVariance) - { - MappedGenericEnvironment::LookupResult lookupResult = env.mappedGenericPacks.lookupGenericPack(*subTail); - SubtypingResult result; - if (get_if(&lookupResult)) - result = SubtypingResult{false, /* normalizationTooComplex */ false, /* isCacheable */ false} - .withSubComponent(TypePath::PackField::Tail) - .withSuperComponent(TypePath::PackSlice{headSize}); - else - { - TypePackId superTailPack = sliceTypePack(headSize, superTp, superHead, superTail, builtinTypes, arena); - - if (const TypePackId* mappedGen = get_if(&lookupResult)) - { - // Subtype against the mapped generic pack. - TypePackId subTpToCompare = *mappedGen; - - // If mappedGen has a hidden variadic tail, we clip it for better arity mismatch reporting. - const TypePack* tp = get(*mappedGen); - if (const VariadicTypePack* vtp = tp ? get(follow(tp->tail)) : nullptr; vtp && vtp->hidden) - subTpToCompare = arena->addTypePack(tp->head); - - result = isCovariantWith(env, subTpToCompare, superTailPack, scope) - .withSubPath(Path({TypePath::PackField::Tail, TypePath::GenericPackMapping{*mappedGen}})) - .withSuperComponent(TypePath::PackSlice{headSize}); - } - else - { - LUAU_ASSERT(get_if(&lookupResult)); - bool ok = env.mappedGenericPacks.bindGeneric(*subTail, superTailPack); - result = SubtypingResult{ok, /* normalizationTooComplex */ false, /* isCacheable */ false} - .withSubComponent(TypePath::PackField::Tail) - .withSuperComponent(TypePath::PackSlice{headSize}); - } - } - - results.push_back(result); - return SubtypingResult::all(results); - } - else if (variance == Variance::Covariant) - { - // For any non-generic type T: - // - // (X) -> () <: (T) -> () - - TypePackId superTailPack; - if (FFlag::LuauReturnMappedGenericPacksFromSubtyping2) - { - if (headSize == 0) - superTailPack = superTp; - else if (headSize == superHead.size()) - superTailPack = superTail ? *superTail : builtinTypes->emptyTypePack; - else - { - auto superHeadIter = begin(superHead); - for (size_t i = 0; i < headSize; ++i) - ++superHeadIter; - std::vector headSlice(std::move(superHeadIter), end(superHead)); - superTailPack = arena->addTypePack(std::move(headSlice), superTail); - } - } - else - { - // Possible optimization: If headSize == 0 then we can just use subTp as-is. - std::vector headSlice = std::vector(begin(superHead), begin(superHead) + headSize); - superTailPack = arena->addTypePack(std::move(headSlice), superTail); - } - - if (TypePackId* other = env.getMappedPackBounds_DEPRECATED(*subTail)) - { - if (FFlag::LuauReturnMappedGenericPacksFromSubtyping2) - { - const TypePack* tp = get(*other); - if (const VariadicTypePack* vtp = tp ? get(follow(tp->tail)) : nullptr; vtp && vtp->hidden) - { - TypePackId taillessTp = arena->addTypePack(tp->head); - results.push_back(isCovariantWith(env, taillessTp, superTailPack, scope) - .withSubComponent(TypePath::PackField::Tail) - .withSuperComponent(TypePath::PackSlice{headSize})); - } - else - results.push_back(isCovariantWith(env, *other, superTailPack, scope) - .withSubComponent(TypePath::PackField::Tail) - .withSuperComponent(TypePath::PackSlice{headSize})); - } - else - results.push_back(isCovariantWith(env, *other, superTailPack, scope).withSubComponent(TypePath::PackField::Tail)); - } - else - env.mappedGenericPacks_DEPRECATED.try_insert(*subTail, superTailPack); - - // FIXME? Not a fan of the early return here. It makes the - // control flow harder to reason about. - return SubtypingResult::all(results); - } - else - { - // For any non-generic type T: - // - // (T) -> () (X) -> () - // - return SubtypingResult{false}.withSubComponent(TypePath::PackField::Tail); - } - } - else if (get(*subTail)) - return SubtypingResult{true}.withSubComponent(TypePath::PackField::Tail); - else - return SubtypingResult{false} - .withSubComponent(TypePath::PackField::Tail) - .withError({scope->location, UnexpectedTypePackInSubtyping{*subTail}}); + std::optional sr = isSubTailCovariantWith(env, results, subTp, *subTail, superTp, headSize, superHead, superTail, scope); + if (sr) + return *sr; } else { @@ -1441,123 +1151,9 @@ SubtypingResult Subtyping::isCovariantWith(SubtypingEnvironment& env, TypePackId { if (superTail) { - if (auto vt = get(*superTail)) - { - for (size_t i = headSize; i < subHead.size(); ++i) - results.push_back(isCovariantWith(env, subHead[i], vt->ty, scope) - .withSubComponent(TypePath::Index{i, TypePath::Index::Variant::Pack}) - .withSuperPath(TypePath::PathBuilder().tail().variadic().build())); - } - else if (get(*superTail)) - { - if (FFlag::LuauSubtypingGenericPacksDoesntUseVariance) - { - MappedGenericEnvironment::LookupResult lookupResult = env.mappedGenericPacks.lookupGenericPack(*superTail); - SubtypingResult result; - if (get_if(&lookupResult)) - result = SubtypingResult{false, /* normalizationTooComplex */ false, /* isCacheable */ false} - .withSubComponent(TypePath::PackSlice{headSize}) - .withSuperComponent(TypePath::PackField::Tail); - else - { - TypePackId subTailPack = sliceTypePack(headSize, subTp, subHead, subTail, builtinTypes, arena); - - if (const TypePackId* mappedGen = get_if(&lookupResult)) - { - TypePackId superTpToCompare = *mappedGen; - - // Subtype against the mapped generic pack. - const TypePack* tp = get(*mappedGen); - if (const VariadicTypePack* vtp = tp ? get(follow(tp->tail)) : nullptr; vtp && vtp->hidden) - superTpToCompare = arena->addTypePack(tp->head); - - result = isCovariantWith(env, subTailPack, superTpToCompare, scope) - .withSubComponent(TypePath::PackSlice{headSize}) - .withSuperPath(Path({TypePath::PackField::Tail, TypePath::GenericPackMapping{*mappedGen}})); - } - else - { - LUAU_ASSERT(get_if(&lookupResult)); - bool ok = env.mappedGenericPacks.bindGeneric(*superTail, subTailPack); - result = SubtypingResult{ok, /* normalizationTooComplex */ false, /* isCacheable */ false} - .withSubComponent(TypePath::PackSlice{headSize}) - .withSuperComponent(TypePath::PackField::Tail); - } - } - - results.push_back(result); - return SubtypingResult::all(results); - } - else if (variance == Variance::Contravariant) - { - // For any non-generic type T: - // - // (X...) -> () <: (T) -> () - - TypePackId subTailPack; - if (FFlag::LuauReturnMappedGenericPacksFromSubtyping2) - { - if (headSize == 0) - subTailPack = subTp; - else if (headSize == subHead.size()) - subTailPack = subTail ? *subTail : builtinTypes->emptyTypePack; - else - { - auto subHeadIter = begin(subHead); - for (size_t i = 0; i < headSize; ++i) - ++subHeadIter; - std::vector headSlice(std::move(subHeadIter), end(subHead)); - subTailPack = arena->addTypePack(std::move(headSlice), subTail); - } - } - else - { - // Possible optimization: If headSize == 0 then we can just use subTp as-is. - std::vector headSlice = std::vector(begin(subHead), begin(subHead) + headSize); - subTailPack = arena->addTypePack(std::move(headSlice), subTail); - } - - if (TypePackId* other = env.getMappedPackBounds_DEPRECATED(*superTail)) - { - if (FFlag::LuauReturnMappedGenericPacksFromSubtyping2) - { - const TypePack* tp = get(*other); - if (const VariadicTypePack* vtp = tp ? get(follow(tp->tail)) : nullptr; vtp && vtp->hidden) - { - TypePackId taillessTp = arena->addTypePack(tp->head); - results.push_back(isCovariantWith(env, subTailPack, taillessTp, scope) - .withSubComponent(TypePath::PackSlice{headSize}) - .withSuperComponent(TypePath::PackField::Tail)); - } - else - results.push_back(isCovariantWith(env, subTailPack, *other, scope) - .withSubComponent(TypePath::PackSlice{headSize}) - .withSuperComponent(TypePath::PackField::Tail)); - } - else - results.push_back(isContravariantWith(env, subTailPack, *other, scope).withSuperComponent(TypePath::PackField::Tail)); - } - else - env.mappedGenericPacks_DEPRECATED.try_insert(*superTail, subTailPack); - - // FIXME? Not a fan of the early return here. It makes the - // control flow harder to reason about. - return SubtypingResult::all(results); - } - else - { - // For any non-generic type T: - // - // () -> T () -> X... - return SubtypingResult{false}.withSuperComponent(TypePath::PackField::Tail); - } - } - else if (get(*superTail)) - return SubtypingResult{true}.withSuperComponent(TypePath::PackField::Tail); - else - return SubtypingResult{false} - .withSuperComponent(TypePath::PackField::Tail) - .withError({scope->location, UnexpectedTypePackInSubtyping{*superTail}}); + std::optional sr = isCovariantWithSuperTail(env, results, subTp, headSize, subHead, subTail, superTp, *superTail, scope); + if (sr) + return *sr; } else return {false}; @@ -1569,159 +1165,19 @@ SubtypingResult Subtyping::isCovariantWith(SubtypingEnvironment& env, TypePackId { if (auto p = get2(*subTail, *superTail)) { - // Variadic component is added by the isCovariantWith - // implementation; no need to add it here. - results.push_back(isCovariantWith(env, p, scope).withBothComponent(TypePath::PackField::Tail)); + results.push_back(isTailCovariantWithTail(env, scope, *subTail, p.first, *superTail, p.second)); } - else if (get2(*subTail, *superTail)) + else if (auto p = get2(*subTail, *superTail)) { - if (FFlag::LuauSubtypingGenericPacksDoesntUseVariance) - { - MappedGenericEnvironment::LookupResult subLookupResult = env.mappedGenericPacks.lookupGenericPack(*subTail); - MappedGenericEnvironment::LookupResult superLookupResult = env.mappedGenericPacks.lookupGenericPack(*superTail); - - // match (subLookup, superLookupResult) { - // (TypePackId, _) => do covariant test - // (Unmapped, _) => bind the generic - // (_, TypePackId) => do covariant test - // (_, Unmapped) => bind the generic - // (_, _) => subtyping succeeds if the two generics are pointer-identical - // } - if (const TypePackId* currMapping = get_if(&subLookupResult)) - { - results.push_back(isCovariantWith(env, *currMapping, *superTail, scope) - .withSubPath(Path({TypePath::PackField::Tail, TypePath::GenericPackMapping{*currMapping}})) - .withSuperComponent(TypePath::PackField::Tail)); - } - else if (get_if(&subLookupResult)) - { - bool ok = env.mappedGenericPacks.bindGeneric(*subTail, *superTail); - results.push_back( - SubtypingResult{ok, /* normalizationTooComplex */ false, /* isCacheable */ false}.withBothComponent( - TypePath::PackField::Tail - ) - ); - } - else if (const TypePackId* currMapping = get_if(&superLookupResult)) - { - results.push_back(isCovariantWith(env, *subTail, *currMapping, scope) - .withSubComponent(TypePath::PackField::Tail) - .withSuperPath(Path({TypePath::PackField::Tail, TypePath::GenericPackMapping{*currMapping}}))); - } - else if (get_if(&superLookupResult)) - { - bool ok = env.mappedGenericPacks.bindGeneric(*superTail, *subTail); - results.push_back( - SubtypingResult{ok, /* normalizationTooComplex */ false, /* isCacheable */ false}.withBothComponent( - TypePath::PackField::Tail - ) - ); - } - else - { - // Sometimes, we compare generic packs inside the functions which are quantifying them. They're not bindable, but should still - // subtype against themselves. - results.push_back( - SubtypingResult{*subTail == *superTail, /* normalizationTooComplex */ false, /* isCacheable */ false}.withBothComponent( - TypePath::PackField::Tail - ) - ); - } - } - else - { - bool ok = bindGeneric_DEPRECATED(env, *subTail, *superTail); - results.push_back(SubtypingResult{ok}.withBothComponent(TypePath::PackField::Tail)); - } + results.push_back(isTailCovariantWithTail(env, scope, *subTail, p.first, *superTail, p.second)); } - else if (get2(*subTail, *superTail)) + else if (auto p = get2(*subTail, *superTail)) { - if (FFlag::LuauSubtypingGenericPacksDoesntUseVariance) - { - MappedGenericEnvironment::LookupResult lookupResult = env.mappedGenericPacks.lookupGenericPack(*superTail); - if (const TypePackId* currMapping = get_if(&lookupResult)) - { - results.push_back(isCovariantWith(env, *subTail, *currMapping, scope) - .withSubComponent(TypePath::PackField::Tail) - .withSuperPath(Path({TypePath::PackField::Tail, TypePath::GenericPackMapping{*currMapping}}))); - } - else if (get_if(&lookupResult)) - { - bool ok = env.mappedGenericPacks.bindGeneric(*superTail, *subTail); - results.push_back( - SubtypingResult{ok, /* normalizationTooComplex */ false, /* isCacheable */ false}.withBothComponent(TypePath::PackField::Tail) - ); - } - else - { - LUAU_ASSERT(get_if(&lookupResult)); - results.push_back( - SubtypingResult{false, /* normalizationTooComplex */ false, /* isCacheable */ false}.withBothComponent( - TypePath::PackField::Tail - ) - ); - } - } - else if (variance == Variance::Contravariant) - { - // (A...) -> number <: (...number) -> number - bool ok = bindGeneric_DEPRECATED(env, *subTail, *superTail); - - results.push_back(SubtypingResult{ok}.withBothComponent(TypePath::PackField::Tail)); - } - else - { - // (number) -> ...number (number) -> A... - results.push_back(SubtypingResult{false}.withBothComponent(TypePath::PackField::Tail)); - } + results.push_back(isTailCovariantWithTail(env, scope, *subTail, p.first, *superTail, p.second)); } else if (auto p = get2(*subTail, *superTail)) { - if (TypeId t = follow(p.second->ty); get(t) || get(t)) - { - // Extra magic rule: - // T... <: ...any - // T... <: ...unknown - // - // See https://github.com/luau-lang/luau/issues/767 - } - else if (FFlag::LuauSubtypingGenericPacksDoesntUseVariance) - { - MappedGenericEnvironment::LookupResult lookupResult = env.mappedGenericPacks.lookupGenericPack(*subTail); - if (const TypePackId* currMapping = get_if(&lookupResult)) - { - results.push_back(isCovariantWith(env, *currMapping, *superTail, scope) - .withSubPath(Path({TypePath::PackField::Tail, TypePath::GenericPackMapping{*currMapping}})) - .withSuperComponent(TypePath::PackField::Tail)); - } - else if (get_if(&lookupResult)) - { - bool ok = env.mappedGenericPacks.bindGeneric(*subTail, *superTail); - results.push_back( - SubtypingResult{ok, /* normalizationTooComplex */ false, /* isCacheable */ false}.withBothComponent(TypePath::PackField::Tail) - ); - } - else - { - LUAU_ASSERT(get_if(&lookupResult)); - results.push_back( - SubtypingResult{false, /* normalizationTooComplex */ false, /* isCacheable */ false}.withBothComponent( - TypePath::PackField::Tail - ) - ); - } - } - else if (variance == Variance::Contravariant) - { - // (...number) -> number (A...) -> number - results.push_back(SubtypingResult{false}.withBothComponent(TypePath::PackField::Tail)); - } - else - { - // () -> A... <: () -> ...number - bool ok = bindGeneric_DEPRECATED(env, *subTail, *superTail); - results.push_back(SubtypingResult{ok}.withBothComponent(TypePath::PackField::Tail)); - } + results.push_back(isTailCovariantWithTail(env, scope, *subTail, p.first, *superTail, p.second)); } else if (get(*subTail) || get(*superTail)) // error type is fine on either side @@ -1738,36 +1194,9 @@ SubtypingResult Subtyping::isCovariantWith(SubtypingEnvironment& env, TypePackId { return SubtypingResult{false}.withSubComponent(TypePath::PackField::Tail); } - else if (get(*subTail)) + else if (auto g = get(*subTail)) { - if (FFlag::LuauSubtypingGenericPacksDoesntUseVariance) - { - MappedGenericEnvironment::LookupResult lookupResult = env.mappedGenericPacks.lookupGenericPack(*subTail); - if (const TypePackId* currMapping = get_if(&lookupResult)) - results.push_back(isCovariantWith(env, *currMapping, builtinTypes->emptyTypePack, scope) - .withSubPath(Path({TypePath::PackField::Tail, TypePath::GenericPackMapping{*currMapping}}))); - else if (get_if(&lookupResult)) - { - bool ok = env.mappedGenericPacks.bindGeneric(*subTail, builtinTypes->emptyTypePack); - results.push_back( - SubtypingResult{ok, /* normalizationTooComplex */ false, /* isCacheable */ false}.withSubComponent(TypePath::PackField::Tail) - ); - } - else - { - LUAU_ASSERT(get_if(&lookupResult)); - results.push_back( - SubtypingResult{false, /* normalizationTooComplex */ false, /* isCacheable */ false}.withSubComponent( - TypePath::PackField::Tail - ) - ); - } - } - else - { - bool ok = bindGeneric_DEPRECATED(env, *subTail, builtinTypes->emptyTypePack); - return SubtypingResult{ok}.withSubComponent(TypePath::PackField::Tail); - } + return isTailCovariantWithTail(env, scope, *subTail, g, Nothing{}); } else return SubtypingResult{false} @@ -1788,40 +1217,9 @@ SubtypingResult Subtyping::isCovariantWith(SubtypingEnvironment& env, TypePackId * All variadic type packs are therefore supertypes of the empty type pack. */ } - else if (get(*superTail)) + else if (auto g = get(*superTail)) { - if (FFlag::LuauSubtypingGenericPacksDoesntUseVariance) - { - MappedGenericEnvironment::LookupResult lookupResult = env.mappedGenericPacks.lookupGenericPack(*superTail); - if (const TypePackId* currMapping = get_if(&lookupResult)) - results.push_back(isCovariantWith(env, builtinTypes->emptyTypePack, *currMapping, scope) - .withSuperPath(Path({TypePath::PackField::Tail, TypePath::GenericPackMapping{*currMapping}}))); - else if (get_if(&lookupResult)) - { - bool ok = env.mappedGenericPacks.bindGeneric(*superTail, builtinTypes->emptyTypePack); - results.push_back( - SubtypingResult{ok, /* normalizationTooComplex */ false, /* isCacheable */ false}.withSuperComponent( - TypePath::PackField::Tail - ) - ); - } - else - { - LUAU_ASSERT(get_if(&lookupResult)); - results.push_back( - SubtypingResult{false, /* normalizationTooComplex */ false, /* isCacheable */ false}.withSuperComponent( - TypePath::PackField::Tail - ) - ); - } - } - else if (variance == Variance::Contravariant) - { - bool ok = bindGeneric_DEPRECATED(env, builtinTypes->emptyTypePack, *superTail); - results.push_back(SubtypingResult{ok}.withSuperComponent(TypePath::PackField::Tail)); - } - else - results.push_back(SubtypingResult{false}.withSuperComponent(TypePath::PackField::Tail)); + results.push_back(isTailCovariantWithTail(env, scope, Nothing{}, *superTail, g)); } else return SubtypingResult{false} @@ -1831,14 +1229,319 @@ SubtypingResult Subtyping::isCovariantWith(SubtypingEnvironment& env, TypePackId SubtypingResult result = SubtypingResult::all(results); - if (FFlag::LuauReturnMappedGenericPacksFromSubtyping2) - assertReasoningValid(subTp, superTp, result, builtinTypes, arena); - else - assertReasoningValid_DEPRECATED(subTp, superTp, result, builtinTypes); + assertReasoningValid(subTp, superTp, result, builtinTypes, arena); return result; } +/* Check the tail of the subtype pack against a slice of the finite part of the + * pack in supertype position. For example, in the following type, the head of + * the pack in supertype position is longer than that of the subtype head: + * + * (number, string, any...) <: (number, string, boolean, thread, any...), + * + * This function handles the mismatched heads: any... <: (boolean, thread) + * + * Notably, this function does _not_ handle the test between the actual tail + * packs. + * + * The contract on this function is a bit strange. If the function returns a + * SubtypingResult, it should be considered to be the result for the entire pack + * subtyping relation. It is not necessary to further check the tails. + */ +std::optional Subtyping::isSubTailCovariantWith( + SubtypingEnvironment& env, + std::vector& outputResults, + TypePackId subTp, + TypePackId subTail, + TypePackId superTp, + size_t superHeadStartIndex, + const std::vector& superHead, + std::optional superTail, + NotNull scope +) +{ + if (auto vt = get(subTail)) + { + for (size_t i = superHeadStartIndex; i < superHead.size(); ++i) + outputResults.push_back(isCovariantWith(env, vt->ty, superHead[i], scope) + .withSubPath(TypePath::PathBuilder().tail().variadic().build()) + .withSuperComponent(TypePath::Index{i, TypePath::Index::Variant::Pack})); + return std::nullopt; + } + else if (get(subTail)) + { + MappedGenericEnvironment::LookupResult lookupResult = env.lookupGenericPack(subTail); + SubtypingResult result; + if (get_if(&lookupResult)) + result = SubtypingResult{false, /* normalizationTooComplex */ false, /* isCacheable */ false} + .withSubComponent(TypePath::PackField::Tail) + .withSuperComponent(TypePath::PackSlice{superHeadStartIndex}); + else + { + TypePackId superTailPack = sliceTypePack(superHeadStartIndex, superTp, superHead, superTail, builtinTypes, arena); + + if (const TypePackId* mappedGen = get_if(&lookupResult)) + { + // Subtype against the mapped generic pack. + TypePackId subTpToCompare = *mappedGen; + + // If mappedGen has a hidden variadic tail, we clip it for better arity mismatch reporting. + const TypePack* tp = get(*mappedGen); + if (const VariadicTypePack* vtp = tp ? get(follow(tp->tail)) : nullptr; vtp && vtp->hidden) + subTpToCompare = arena->addTypePack(tp->head); + + result = isCovariantWith(env, subTpToCompare, superTailPack, scope) + .withSubPath(Path({TypePath::PackField::Tail, TypePath::GenericPackMapping{*mappedGen}})) + .withSuperComponent(TypePath::PackSlice{superHeadStartIndex}); + } + else + { + LUAU_ASSERT(get_if(&lookupResult)); + bool ok = env.mappedGenericPacks.bindGeneric(subTail, superTailPack); + result = SubtypingResult{ok, /* normalizationTooComplex */ false, /* isCacheable */ false} + .withSubComponent(TypePath::PackField::Tail) + .withSuperComponent(TypePath::PackSlice{superHeadStartIndex}); + } + } + + outputResults.push_back(result); + return SubtypingResult::all(outputResults); + } + else if (get(subTail)) + return SubtypingResult{true}.withSubComponent(TypePath::PackField::Tail); + else + return SubtypingResult{false} + .withSubComponent(TypePath::PackField::Tail) + .withError({scope->location, UnexpectedTypePackInSubtyping{subTail}}); +} + +std::optional Subtyping::isCovariantWithSuperTail( + SubtypingEnvironment& env, + std::vector& results, + TypePackId subTp, + size_t subHeadStartIndex, + const std::vector& subHead, + std::optional subTail, + TypePackId superTp, + TypePackId superTail, + NotNull scope +) +{ + if (auto vt = get(superTail)) + { + for (size_t i = subHeadStartIndex; i < subHead.size(); ++i) + results.push_back(isCovariantWith(env, subHead[i], vt->ty, scope) + .withSubComponent(TypePath::Index{i, TypePath::Index::Variant::Pack}) + .withSuperPath(TypePath::PathBuilder().tail().variadic().build())); + return std::nullopt; + } + else if (get(superTail)) + { + MappedGenericEnvironment::LookupResult lookupResult = env.lookupGenericPack(superTail); + SubtypingResult result; + if (get_if(&lookupResult)) + result = SubtypingResult{false, /* normalizationTooComplex */ false, /* isCacheable */ false} + .withSubComponent(TypePath::PackSlice{subHeadStartIndex}) + .withSuperComponent(TypePath::PackField::Tail); + else + { + TypePackId subTailPack = sliceTypePack(subHeadStartIndex, subTp, subHead, subTail, builtinTypes, arena); + + if (const TypePackId* mappedGen = get_if(&lookupResult)) + { + TypePackId superTpToCompare = *mappedGen; + + // Subtype against the mapped generic pack. + const TypePack* tp = get(*mappedGen); + if (const VariadicTypePack* vtp = tp ? get(follow(tp->tail)) : nullptr; vtp && vtp->hidden) + superTpToCompare = arena->addTypePack(tp->head); + + result = isCovariantWith(env, subTailPack, superTpToCompare, scope) + .withSubComponent(TypePath::PackSlice{subHeadStartIndex}) + .withSuperPath(Path({TypePath::PackField::Tail, TypePath::GenericPackMapping{*mappedGen}})); + } + else + { + LUAU_ASSERT(get_if(&lookupResult)); + bool ok = env.mappedGenericPacks.bindGeneric(superTail, subTailPack); + result = SubtypingResult{ok, /* normalizationTooComplex */ false, /* isCacheable */ false} + .withSubComponent(TypePath::PackSlice{subHeadStartIndex}) + .withSuperComponent(TypePath::PackField::Tail); + } + } + + results.push_back(result); + return SubtypingResult::all(results); + } + else if (get(superTail)) + return SubtypingResult{true}.withSuperComponent(TypePath::PackField::Tail); + else + return SubtypingResult{false} + .withSuperComponent(TypePath::PackField::Tail) + .withError({scope->location, UnexpectedTypePackInSubtyping{superTail}}); +} + +SubtypingResult Subtyping::isTailCovariantWithTail( + SubtypingEnvironment& env, + NotNull scope, + TypePackId subTp, + const VariadicTypePack* sub, + TypePackId superTp, + const VariadicTypePack* super +) +{ + // A variadic type pack is a subtype of another variadic type pack if their + // respective element types have the same subtyping relationship. + return isCovariantWith(env, sub->ty, super->ty, scope) + .withBothComponent(TypePath::TypeField::Variadic) + .withBothComponent(TypePath::PackField::Tail); +} + +SubtypingResult Subtyping::isTailCovariantWithTail( + SubtypingEnvironment& env, + NotNull scope, + TypePackId subTp, + const GenericTypePack* sub, + TypePackId superTp, + const GenericTypePack* super +) +{ + MappedGenericEnvironment::LookupResult subLookupResult = env.lookupGenericPack(subTp); + MappedGenericEnvironment::LookupResult superLookupResult = env.lookupGenericPack(superTp); + + // match (subLookup, superLookupResult) { + // (TypePackId, _) => do covariant test + // (Unmapped, _) => bind the generic + // (_, TypePackId) => do covariant test + // (_, Unmapped) => bind the generic + // (_, _) => subtyping succeeds if the two generics are pointer-identical + // } + if (const TypePackId* currMapping = get_if(&subLookupResult)) + { + return isCovariantWith(env, *currMapping, superTp, scope) + .withSubPath(Path({TypePath::PackField::Tail, TypePath::GenericPackMapping{*currMapping}})) + .withSuperComponent(TypePath::PackField::Tail); + } + else if (get_if(&subLookupResult)) + { + bool ok = env.mappedGenericPacks.bindGeneric(subTp, superTp); + return SubtypingResult{ok, /* normalizationTooComplex */ false, /* isCacheable */ false}.withBothComponent(TypePath::PackField::Tail); + } + else if (const TypePackId* currMapping = get_if(&superLookupResult)) + { + return isCovariantWith(env, subTp, *currMapping, scope) + .withSubComponent(TypePath::PackField::Tail) + .withSuperPath(Path({TypePath::PackField::Tail, TypePath::GenericPackMapping{*currMapping}})); + } + else if (get_if(&superLookupResult)) + { + bool ok = env.mappedGenericPacks.bindGeneric(superTp, subTp); + return SubtypingResult{ok, /* normalizationTooComplex */ false, /* isCacheable */ false}.withBothComponent(TypePath::PackField::Tail); + } + else + { + // Sometimes, we compare generic packs inside the functions which are quantifying them. They're not bindable, but should still + // subtype against themselves. + return SubtypingResult{subTp == superTp, /* normalizationTooComplex */ false, /* isCacheable */ false}.withBothComponent( + TypePath::PackField::Tail + ); + } +} + +SubtypingResult Subtyping::isTailCovariantWithTail(SubtypingEnvironment& env, NotNull scope, TypePackId subTp, const VariadicTypePack* sub, TypePackId superTp, const GenericTypePack* super) +{ + MappedGenericEnvironment::LookupResult lookupResult = env.lookupGenericPack(superTp); + if (const TypePackId* currMapping = get_if(&lookupResult)) + { + return isCovariantWith(env, subTp, *currMapping, scope) + .withSubComponent(TypePath::PackField::Tail) + .withSuperPath(Path({TypePath::PackField::Tail, TypePath::GenericPackMapping{*currMapping}})); + } + else if (get_if(&lookupResult)) + { + bool ok = env.mappedGenericPacks.bindGeneric(superTp, subTp); + return SubtypingResult{ok, /* normalizationTooComplex */ false, /* isCacheable */ false}.withBothComponent(TypePath::PackField::Tail); + } + else + { + LUAU_ASSERT(get_if(&lookupResult)); + return SubtypingResult{false, /* normalizationTooComplex */ false, /* isCacheable */ false}.withBothComponent(TypePath::PackField::Tail); + } +} + +SubtypingResult Subtyping::isTailCovariantWithTail(SubtypingEnvironment& env, NotNull scope, TypePackId subTp, const GenericTypePack* sub, TypePackId superTp, const VariadicTypePack* super) +{ + if (TypeId t = follow(super->ty); get(t) || get(t)) + { + // Extra magic rule: + // T... <: ...any + // T... <: ...unknown + // + // See https://github.com/luau-lang/luau/issues/767 + return SubtypingResult{true}; + } + else + { + MappedGenericEnvironment::LookupResult lookupResult = env.lookupGenericPack(subTp); + if (const TypePackId* currMapping = get_if(&lookupResult)) + { + return isCovariantWith(env, *currMapping, superTp, scope) + .withSubPath(Path({TypePath::PackField::Tail, TypePath::GenericPackMapping{*currMapping}})) + .withSuperComponent(TypePath::PackField::Tail); + } + else if (get_if(&lookupResult)) + { + bool ok = env.mappedGenericPacks.bindGeneric(subTp, superTp); + return SubtypingResult{ok, /* normalizationTooComplex */ false, /* isCacheable */ false}.withBothComponent(TypePath::PackField::Tail); + } + else + { + LUAU_ASSERT(get_if(&lookupResult)); + return SubtypingResult{false, /* normalizationTooComplex */ false, /* isCacheable */ false}.withBothComponent( + TypePath::PackField::Tail + ); + } + } +} + +SubtypingResult Subtyping::isTailCovariantWithTail(SubtypingEnvironment& env, NotNull scope, TypePackId subTp, const GenericTypePack* sub, Nothing) +{ + MappedGenericEnvironment::LookupResult lookupResult = env.lookupGenericPack(subTp); + if (const TypePackId* currMapping = get_if(&lookupResult)) + return isCovariantWith(env, *currMapping, builtinTypes->emptyTypePack, scope) + .withSubPath(Path({TypePath::PackField::Tail, TypePath::GenericPackMapping{*currMapping}})); + else if (get_if(&lookupResult)) + { + bool ok = env.mappedGenericPacks.bindGeneric(subTp, builtinTypes->emptyTypePack); + return SubtypingResult{ok, /* normalizationTooComplex */ false, /* isCacheable */ false}.withSubComponent(TypePath::PackField::Tail); + } + else + { + LUAU_ASSERT(get_if(&lookupResult)); + return SubtypingResult{false, /* normalizationTooComplex */ false, /* isCacheable */ false}.withSubComponent(TypePath::PackField::Tail); + } +} + +SubtypingResult Subtyping::isTailCovariantWithTail(SubtypingEnvironment& env, NotNull scope, Nothing, TypePackId superTp, const GenericTypePack* super) +{ + MappedGenericEnvironment::LookupResult lookupResult = env.lookupGenericPack(superTp); + if (const TypePackId* currMapping = get_if(&lookupResult)) + return isCovariantWith(env, builtinTypes->emptyTypePack, *currMapping, scope) + .withSuperPath(Path({TypePath::PackField::Tail, TypePath::GenericPackMapping{*currMapping}})); + else if (get_if(&lookupResult)) + { + bool ok = env.mappedGenericPacks.bindGeneric(superTp, builtinTypes->emptyTypePack); + return SubtypingResult{ok, /* normalizationTooComplex */ false, /* isCacheable */ false}.withSuperComponent(TypePath::PackField::Tail); + } + else + { + LUAU_ASSERT(get_if(&lookupResult)); + return SubtypingResult{false, /* normalizationTooComplex */ false, /* isCacheable */ false}.withSuperComponent(TypePath::PackField::Tail); + } +} + + template SubtypingResult Subtyping::isContravariantWith(SubtypingEnvironment& env, SubTy&& subTy, SuperTy&& superTy, NotNull scope) { @@ -1866,10 +1569,7 @@ SubtypingResult Subtyping::isContravariantWith(SubtypingEnvironment& env, SubTy& } } - if (FFlag::LuauReturnMappedGenericPacksFromSubtyping2) - assertReasoningValid(subTy, superTy, result, builtinTypes, arena); - else - assertReasoningValid_DEPRECATED(subTy, superTy, result, builtinTypes); + assertReasoningValid(subTy, superTy, result, builtinTypes, arena); return result; } @@ -1888,10 +1588,7 @@ SubtypingResult Subtyping::isInvariantWith(SubtypingEnvironment& env, SubTy&& su reasoning.variance = SubtypingVariance::Invariant; } - if (FFlag::LuauReturnMappedGenericPacksFromSubtyping2) - assertReasoningValid(subTy, superTy, result, builtinTypes, arena); - else - assertReasoningValid_DEPRECATED(subTy, superTy, result, builtinTypes); + assertReasoningValid(subTy, superTy, result, builtinTypes, arena); return result; } @@ -2326,16 +2023,102 @@ SubtypingResult Subtyping::isCovariantWith(SubtypingEnvironment& env, const Meta { if (auto subTable = get(follow(subMt->table))) { - // Metatables cannot erase properties from the table they're attached to, so - // the subtyping rule for this is just if the table component is a subtype - // of the supertype table. - // - // There's a flaw here in that if the __index metamethod contributes a new - // field that would satisfy the subtyping relationship, we'll erroneously say - // that the metatable isn't a subtype of the table, even though they have - // compatible properties/shapes. We'll revisit this later when we have a - // better understanding of how important this is. - return isCovariantWith(env, subTable, superTable, /*forceCovariantTest*/ false, scope); + if (FFlag::LuauIndexInMetatableSubtyping) + { + auto doDefault = [&]() + { + return isCovariantWith(env, subTable, superTable, /* forceCovariantTest */ false, scope); + }; + + // My kingdom for `do` notation. + + // TODO CLI-169235: This logic is very mechanical and is, + // effectively a repeat of the logic for the `index<_, _>` + // type function. These should use similar logic. Otherwise + // this all constantly falls over for the same reasons + // structural subtypying falls over. + // + // Notably, this does not support `__index` as a function. + + auto subMTTable = get(follow(subMt->metatable)); + if (!subMTTable) + return doDefault(); + + auto __index = subMTTable->props.find("__index"); + if (__index == subMTTable->props.end()) + return doDefault(); + + // `read`-only __index sounds reasonable, but write-only + // or non-shared sounds weird. + if (!__index->second.readTy) + return doDefault(); + + auto __indexAsTable = get(follow(*__index->second.readTy)); + if (!__indexAsTable) + return doDefault(); + + // Consider the snippet: + // + // local ItemContainer = {} + // ItemContainer.__index = ItemContainer + // + // function ItemContainer.new() + // local self = {} + // setmetatable(self, ItemContainer) + // return self + // end + // + // function ItemContainer:removeItem(itemId, itemType) + // self:getItem(itemId, itemType) + // end + // + // function ItemContainer:getItem(itemId, itemType) end + // + // local container = ItemContainer.new() + // container:removeItem(0, "magic") + // + // When we go to check this, we're effectively asking whether + // `container` is a subtype of the first argument of + // `container.removeItem`. `container` has a metatable with the + // `__index` metamethod, so we need to include those fields in the + // subtype check. + // + // However, we need to include a read only view of those fields. + // Consider: + // + // local Foobar = {} + // Foobar.__index = Foobar + // Foobar.const = 42 + // + // local foobar = setmetatable({}, Foobar) + // + // local _: { const: number } = foobar + // + // This should error, as we cannot write to `const`. + + TableType fauxSubTable{*subTable}; + for (auto& [name, prop] : __indexAsTable->props) + { + if (prop.readTy && fauxSubTable.props.find(name) == fauxSubTable.props.end()) + fauxSubTable.props[name] = Property::readonly(*prop.readTy); + } + + return isCovariantWith(env, &fauxSubTable, superTable, /* forceCovariantTest */ false, scope); + + } + else + { + // Metatables cannot erase properties from the table they're attached to, so + // the subtyping rule for this is just if the table component is a subtype + // of the supertype table. + // + // There's a flaw here in that if the __index metamethod contributes a new + // field that would satisfy the subtyping relationship, we'll erroneously say + // that the metatable isn't a subtype of the table, even though they have + // compatible properties/shapes. We'll revisit this later when we have a + // better understanding of how important this is. + return isCovariantWith(env, subTable, superTable, /* forceCovariantTest */ false, scope); + } } else { @@ -2394,7 +2177,7 @@ SubtypingResult Subtyping::isCovariantWith( { SubtypingResult result; - if (FFlag::LuauSubtypingGenericsDoesntUseVariance && !subFunction->generics.empty()) + if (!subFunction->generics.empty()) { for (TypeId g : subFunction->generics) { @@ -2410,7 +2193,7 @@ SubtypingResult Subtyping::isCovariantWith( } } - if (FFlag::LuauSubtypingGenericPacksDoesntUseVariance && !subFunction->genericPacks.empty()) + if (!subFunction->genericPacks.empty()) { std::vector packs; packs.reserve(subFunction->genericPacks.size()); @@ -2458,7 +2241,7 @@ SubtypingResult Subtyping::isCovariantWith( ); } - if (FFlag::LuauSubtypingGenericsDoesntUseVariance && !subFunction->generics.empty()) + if (!subFunction->generics.empty()) { for (TypeId g : subFunction->generics) { @@ -2478,7 +2261,7 @@ SubtypingResult Subtyping::isCovariantWith( } } - if (FFlag::LuauSubtypingGenericPacksDoesntUseVariance && !subFunction->genericPacks.empty()) + if (!subFunction->genericPacks.empty()) { env.mappedGenericPacks.popFrame(); // This result isn't cacheable, because we may need it to populate the generic pack mapping environment again later @@ -2520,7 +2303,9 @@ SubtypingResult Subtyping::isCovariantWith(SubtypingEnvironment& env, const Prim } else if (subPrim->type == PrimitiveType::Table) { - const bool isSubtype = superTable->props.empty() && !superTable->indexer.has_value(); + const bool isSubtype = FFlag::LuauSubtypingPrimitiveAndGenericTableTypes + ? superTable->props.empty() && (!superTable->indexer.has_value() || superTable->state == TableState::Generic) + : superTable->props.empty() && !superTable->indexer.has_value(); return {isSubtype}; } @@ -2535,7 +2320,7 @@ SubtypingResult Subtyping::isCovariantWith( ) { SubtypingResult result{false}; - if (auto stringleton = get(subSingleton)) + if (get(subSingleton)) { if (auto metatable = getMetatable(builtinTypes->stringType, builtinTypes)) { @@ -2797,77 +2582,51 @@ SubtypingResult Subtyping::isCovariantWith( bool Subtyping::bindGeneric(SubtypingEnvironment& env, TypeId subTy, TypeId superTy) { - if (FFlag::LuauSubtypingGenericsDoesntUseVariance) - { - subTy = follow(subTy); - superTy = follow(superTy); - std::optional originalSubTyBounds = std::nullopt; - - if (const auto subBounds = env.mappedGenerics.find(subTy); subBounds && !subBounds->empty()) - { - LUAU_ASSERT(get(subTy)); - - originalSubTyBounds = SubtypingEnvironment::GenericBounds{subBounds->back()}; + subTy = follow(subTy); + superTy = follow(superTy); + std::optional originalSubTyBounds = std::nullopt; - auto& [lowerSubBounds, upperSubBounds] = subBounds->back(); + if (const auto subBounds = env.mappedGenerics.find(subTy); subBounds && !subBounds->empty()) + { + LUAU_ASSERT(get(subTy)); - if (const auto superBounds = env.mappedGenerics.find(superTy); superBounds && !superBounds->empty()) - { - LUAU_ASSERT(get(superTy)); + originalSubTyBounds = SubtypingEnvironment::GenericBounds{subBounds->back()}; - const auto& [lowerSuperBounds, upperSuperBounds] = superBounds->back(); - - maybeUpdateBounds(subTy, superTy, upperSubBounds, lowerSuperBounds, upperSuperBounds); - } - else - upperSubBounds.insert(superTy); - } - else if (env.containsMappedType(subTy)) - iceReporter->ice("attempting to modify bounds of a potentially visited generic"); + auto& [lowerSubBounds, upperSubBounds] = subBounds->back(); if (const auto superBounds = env.mappedGenerics.find(superTy); superBounds && !superBounds->empty()) { LUAU_ASSERT(get(superTy)); - auto& [lowerSuperBounds, upperSuperBounds] = superBounds->back(); - - if (originalSubTyBounds) - { - LUAU_ASSERT(get(subTy)); - - const auto& [originalLowerSubBound, originalUpperSubBound] = *originalSubTyBounds; + const auto& [lowerSuperBounds, upperSuperBounds] = superBounds->back(); - maybeUpdateBounds(superTy, subTy, lowerSuperBounds, originalUpperSubBound, originalLowerSubBound); - } - else - lowerSuperBounds.insert(subTy); + maybeUpdateBounds(subTy, superTy, upperSubBounds, lowerSuperBounds, upperSuperBounds); } - else if (env.containsMappedType(superTy)) - iceReporter->ice("attempting to modify bounds of a potentially visited generic"); + else + upperSubBounds.insert(superTy); } - else + else if (env.containsMappedType(subTy)) + iceReporter->ice("attempting to modify bounds of a potentially visited generic"); + + if (const auto superBounds = env.mappedGenerics.find(superTy); superBounds && !superBounds->empty()) { - if (variance == Variance::Covariant) - { - if (!get(subTy)) - return false; + LUAU_ASSERT(get(superTy)); - if (!env.mappedGenerics_DEPRECATED.find(subTy) && env.containsMappedType(subTy)) - iceReporter->ice("attempting to modify bounds of a potentially visited generic"); + auto& [lowerSuperBounds, upperSuperBounds] = superBounds->back(); - env.mappedGenerics_DEPRECATED[subTy].upperBound.insert(superTy); - } - else + if (originalSubTyBounds) { - if (!get(superTy)) - return false; + LUAU_ASSERT(get(subTy)); - if (!env.mappedGenerics_DEPRECATED.find(superTy) && env.containsMappedType(superTy)) - iceReporter->ice("attempting to modify bounds of a potentially visited generic"); + const auto& [originalLowerSubBound, originalUpperSubBound] = *originalSubTyBounds; - env.mappedGenerics_DEPRECATED[superTy].lowerBound.insert(subTy); + maybeUpdateBounds(superTy, subTy, lowerSuperBounds, originalUpperSubBound, originalLowerSubBound); } + else + lowerSuperBounds.insert(subTy); } + else if (env.containsMappedType(superTy)) + iceReporter->ice("attempting to modify bounds of a potentially visited generic"); return true; } @@ -2898,35 +2657,6 @@ SubtypingResult Subtyping::isCovariantWith( return isCovariantWith(env, subTy, ty, scope).withErrors(errors).withSuperComponent(TypePath::Reduction{ty}); } -/* - * If, when performing a subtyping test, we encounter a generic on the left - * side, it is permissible to tentatively bind that generic to the right side - * type. - */ -bool Subtyping::bindGeneric_DEPRECATED(SubtypingEnvironment& env, TypePackId subTp, TypePackId superTp) const -{ - LUAU_ASSERT(!FFlag::LuauSubtypingGenericPacksDoesntUseVariance); - if (variance == Variance::Contravariant) - std::swap(superTp, subTp); - - if (!get(subTp)) - return false; - - if (TypePackId* m = env.getMappedPackBounds_DEPRECATED(subTp)) - return *m == superTp; - - if (FFlag::LuauReturnMappedGenericPacksFromSubtyping2) - { - // We shouldn't bind generic type packs to themselves - if (subTp == superTp) - return true; - } - - env.mappedGenericPacks_DEPRECATED[subTp] = superTp; - - return true; -} - template TypeId Subtyping::makeAggregateType(const Container& container, TypeId orElse) { @@ -2988,7 +2718,6 @@ SubtypingResult Subtyping::checkGenericBounds( std::string_view genericName ) { - LUAU_ASSERT(FFlag::LuauSubtypingGenericsDoesntUseVariance); LUAU_ASSERT(FFlag::LuauSubtypingReportGenericBoundMismatches2); SubtypingResult result{true}; @@ -3107,7 +2836,6 @@ SubtypingResult Subtyping::checkGenericBounds_DEPRECATED( NotNull scope ) { - LUAU_ASSERT(FFlag::LuauSubtypingGenericsDoesntUseVariance); LUAU_ASSERT(!FFlag::LuauSubtypingReportGenericBoundMismatches2); SubtypingResult result{true}; diff --git a/Analysis/src/TableLiteralInference.cpp b/Analysis/src/TableLiteralInference.cpp index 357f3f1e..4ac04252 100644 --- a/Analysis/src/TableLiteralInference.cpp +++ b/Analysis/src/TableLiteralInference.cpp @@ -4,6 +4,8 @@ #include "Luau/Ast.h" #include "Luau/Common.h" +#include "Luau/ConstraintSolver.h" +#include "Luau/HashUtil.h" #include "Luau/Simplify.h" #include "Luau/Subtyping.h" #include "Luau/Type.h" @@ -12,6 +14,10 @@ #include "Luau/TypeUtils.h" #include "Luau/Unifier2.h" +LUAU_FASTFLAGVARIABLE(LuauPushTypeConstraintIntersection) +LUAU_FASTFLAGVARIABLE(LuauPushTypeConstraintSingleton) +LUAU_FASTFLAGVARIABLE(LuauPushTypeConstraintIndexer) +LUAU_FASTFLAGVARIABLE(LuauPushTypeConstraintLambdas) namespace Luau { @@ -25,25 +31,27 @@ struct BidirectionalTypePusher NotNull> astTypes; NotNull> astExpectedTypes; - NotNull builtinTypes; - NotNull arena; + NotNull solver; + NotNull constraint; NotNull unifier; NotNull subtyping; std::vector incompleteInferences; + DenseHashSet, PairHash> seen{{nullptr, nullptr}}; + BidirectionalTypePusher( NotNull> astTypes, NotNull> astExpectedTypes, - NotNull builtinTypes, - NotNull arena, + NotNull solver, + NotNull constraint, NotNull unifier, NotNull subtyping ) : astTypes{astTypes} , astExpectedTypes{astExpectedTypes} - , builtinTypes{builtinTypes} - , arena{arena} + , solver{solver} + , constraint{constraint} , unifier{unifier} , subtyping{subtyping} { @@ -54,11 +62,18 @@ struct BidirectionalTypePusher if (!astTypes->contains(expr)) { LUAU_ASSERT(false); - return builtinTypes->errorType; + return solver->builtinTypes->errorType; } TypeId exprType = *astTypes->find(expr); + if (FFlag::LuauPushTypeConstraintIntersection) + { + if (seen.contains({expectedType, expr})) + return exprType; + seen.insert({expectedType, expr}); + } + expectedType = follow(expectedType); exprType = follow(exprType); @@ -110,14 +125,46 @@ struct BidirectionalTypePusher if (expr->is()) { auto ft = get(exprType); - if (ft && get(ft->lowerBound) && fastIsSubtype(builtinTypes->stringType, ft->upperBound) && - fastIsSubtype(ft->lowerBound, builtinTypes->stringType)) + 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 we see a pattern like: + // + // local function foo(my_enum: "foo" | "bar" | T) -> T + // return my_enum + // end + // local var = foo("meow") + // + // ... where we are attempting to push a singleton onto any string + // literal, and the lower bound is still a singleton, then snap + // to said lower bound. + if (FFlag::LuauPushTypeConstraintLambdas) + { + solver->bind(constraint, exprType, ft->lowerBound); + } + else + { + emplaceType(asMutable(exprType), ft->lowerBound); + solver->unblock(exprType, expr->location); + } + return exprType; + } + // if the upper bound is a subtype of the expected type, we can push the expected type in Relation upperBoundRelation = relate(ft->upperBound, expectedType); if (upperBoundRelation == Relation::Subset || upperBoundRelation == Relation::Coincident) { - emplaceType(asMutable(exprType), expectedType); + if (FFlag::LuauPushTypeConstraintLambdas) + { + solver->bind(constraint, exprType, expectedType); + } + else + { + emplaceType(asMutable(exprType), expectedType); + solver->unblock(exprType, expr->location); + } return exprType; } @@ -127,7 +174,15 @@ struct BidirectionalTypePusher Relation lowerBoundRelation = relate(ft->lowerBound, expectedType); if (lowerBoundRelation == Relation::Subset || lowerBoundRelation == Relation::Coincident) { - emplaceType(asMutable(exprType), expectedType); + if (FFlag::LuauPushTypeConstraintLambdas) + { + solver->bind(constraint, exprType, expectedType); + } + else + { + emplaceType(asMutable(exprType), expectedType); + solver->unblock(exprType, expr->location); + } return exprType; } } @@ -135,14 +190,22 @@ struct BidirectionalTypePusher else if (expr->is()) { auto ft = get(exprType); - if (ft && get(ft->lowerBound) && fastIsSubtype(builtinTypes->booleanType, ft->upperBound) && - fastIsSubtype(ft->lowerBound, builtinTypes->booleanType)) + if (ft && get(ft->lowerBound) && fastIsSubtype(solver->builtinTypes->booleanType, ft->upperBound) && + fastIsSubtype(ft->lowerBound, solver->builtinTypes->booleanType)) { // if the upper bound is a subtype of the expected type, we can push the expected type in Relation upperBoundRelation = relate(ft->upperBound, expectedType); if (upperBoundRelation == Relation::Subset || upperBoundRelation == Relation::Coincident) { - emplaceType(asMutable(exprType), expectedType); + if (FFlag::LuauPushTypeConstraintLambdas) + { + solver->bind(constraint, exprType, expectedType); + } + else + { + emplaceType(asMutable(exprType), expectedType); + solver->unblock(exprType, expr->location); + } return exprType; } @@ -152,7 +215,15 @@ struct BidirectionalTypePusher Relation lowerBoundRelation = relate(ft->lowerBound, expectedType); if (lowerBoundRelation == Relation::Subset || lowerBoundRelation == Relation::Coincident) { - emplaceType(asMutable(exprType), expectedType); + if (FFlag::LuauPushTypeConstraintLambdas) + { + solver->bind(constraint, exprType, expectedType); + } + else + { + emplaceType(asMutable(exprType), expectedType); + solver->unblock(exprType, expr->location); + } return exprType; } } @@ -164,6 +235,7 @@ struct BidirectionalTypePusher if (auto ft = get(exprType); ft && fastIsSubtype(ft->upperBound, expectedType)) { emplaceType(asMutable(exprType), expectedType); + solver->unblock(exprType, expr->location); return exprType; } @@ -174,11 +246,41 @@ struct BidirectionalTypePusher return exprType; } + if (FFlag::LuauPushTypeConstraintLambdas) + { + if (auto exprLambda = expr->as()) + { + const auto lambdaTy = get(exprType); + const auto expectedLambdaTy = get(expectedType); + if (lambdaTy && expectedLambdaTy && !ContainsAnyGeneric::hasAnyGeneric(expectedType)) + { + const auto& [lambdaArgTys, _lambdaTail] = flatten(lambdaTy->argTypes); + const auto& [expectedLambdaArgTys, _expectedLambdaTail] = flatten(expectedLambdaTy->argTypes); - if (expr->is()) - // TODO: Push argument / return types into the lambda. - return exprType; + auto limit = std::min({lambdaArgTys.size(), expectedLambdaArgTys.size(), exprLambda->args.size}); + for (size_t argIndex = 0; argIndex < limit; argIndex++) + { + if (!exprLambda->args.data[argIndex]->annotation && get(follow(lambdaArgTys[argIndex]))) + solver->bind(NotNull{constraint}, lambdaArgTys[argIndex], expectedLambdaArgTys[argIndex]); + } + if (!exprLambda->returnAnnotation && get(follow(lambdaTy->retTypes))) + solver->bind(NotNull{constraint}, lambdaTy->retTypes, expectedLambdaTy->retTypes); + } + } + } + else + { + if (expr->is()) + { + // TODO: Push argument / return types into the lambda. + return exprType; + } + } + + + // TODO: CLI-169235: This probably ought to use the same logic as + // `index` to determine what the type of a given member is. if (auto exprTable = expr->as()) { const TableType* expectedTableTy = get(expectedType); @@ -189,11 +291,21 @@ struct BidirectionalTypePusher { std::vector parts{begin(utv), end(utv)}; - std::optional tt = extractMatchingTableType(parts, exprType, builtinTypes); + std::optional tt = extractMatchingTableType(parts, exprType, solver->builtinTypes); if (tt) (void)pushType(*tt, expr); } + else if (auto itv = get(expectedType); FFlag::LuauPushTypeConstraintIntersection && itv) + { + for (const auto part : itv) + (void)pushType(part, expr); + + // Reset the expected type for this expression prior, + // otherwise the expected type will be the last part + // of the intersection, which does not seem ideal. + (*astExpectedTypes)[expr] = expectedType; + } return exprType; } @@ -210,15 +322,23 @@ struct BidirectionalTypePusher { // If we have some type: // - // { [string]: T } + // { [T]: U } // // ... that we're trying to push into ... // // { foo = bar } // - // Then the intent is probably to push `T` into `bar`. - if (expectedTableTy->indexer && fastIsSubtype(builtinTypes->stringType, expectedTableTy->indexer->indexType)) - (void)pushType(expectedTableTy->indexer->indexResultType, item.value); + // Then the intent is probably to push `U` into `bar`. + if (FFlag::LuauPushTypeConstraintIndexer) + { + if (expectedTableTy->indexer) + (void)pushType(expectedTableTy->indexer->indexResultType, item.value); + } + else + { + if (expectedTableTy->indexer && fastIsSubtype(solver->builtinTypes->stringType, expectedTableTy->indexer->indexType)) + (void)pushType(expectedTableTy->indexer->indexResultType, item.value); + } // If it's just an extra property and the expected type // has no indexer, there's no work to do here. @@ -243,7 +363,7 @@ struct BidirectionalTypePusher { if (expectedTableTy->indexer) { - unifier->unify(expectedTableTy->indexer->indexType, builtinTypes->numberType); + unifier->unify(expectedTableTy->indexer->indexType, solver->builtinTypes->numberType); (void)pushType(expectedTableTy->indexer->indexResultType, item.value); } } @@ -274,15 +394,15 @@ struct BidirectionalTypePusher PushTypeResult pushTypeInto( NotNull> astTypes, NotNull> astExpectedTypes, - NotNull builtinTypes, - NotNull arena, + NotNull solver, + NotNull constraint, NotNull unifier, NotNull subtyping, TypeId expectedType, const AstExpr* expr ) { - BidirectionalTypePusher btp{astTypes, astExpectedTypes, builtinTypes, arena, unifier, subtyping}; + BidirectionalTypePusher btp{astTypes, astExpectedTypes, solver, constraint, unifier, subtyping}; (void)btp.pushType(expectedType, expr); return {std::move(btp.incompleteInferences)}; } diff --git a/Analysis/src/ToDot.cpp b/Analysis/src/ToDot.cpp index 86f94b9e..de953b23 100644 --- a/Analysis/src/ToDot.cpp +++ b/Analysis/src/ToDot.cpp @@ -439,7 +439,7 @@ void StateDot::visitChildren(TypePackId tp, int index) visitChild(vtp->ty, index); } - else if (const FreeTypePack* ftp = get(tp)) + else if (get(tp)) { formatAppend(result, "FreeTypePack %d", index); finishNodeLabel(tp); diff --git a/Analysis/src/ToString.cpp b/Analysis/src/ToString.cpp index 2054b18c..b7007cc7 100644 --- a/Analysis/src/ToString.cpp +++ b/Analysis/src/ToString.cpp @@ -21,7 +21,6 @@ LUAU_FASTFLAGVARIABLE(LuauEnableDenseTableAlias) LUAU_FASTFLAG(LuauSolverV2) -LUAU_FASTFLAGVARIABLE(LuauSolverAgnosticStringification) LUAU_FASTFLAG(LuauExplicitSkipBoundTypes) /* @@ -89,27 +88,10 @@ struct FindCyclicTypes final : TypeVisitor { if (!visited.insert(ty)) return false; - if (FFlag::LuauSolverAgnosticStringification) - { - LUAU_ASSERT(ft.lowerBound); - LUAU_ASSERT(ft.upperBound); - traverse(ft.lowerBound); - traverse(ft.upperBound); - } - else if (FFlag::LuauSolverV2) - { - // TODO: Replace these if statements with assert()s when we - // delete FFlag::LuauSolverV2. - // - // When the old solver is used, these pointers are always - // unused. When the new solver is used, they are never null. - - if (ft.lowerBound) - traverse(ft.lowerBound); - if (ft.upperBound) - traverse(ft.upperBound); - } - + LUAU_ASSERT(ft.lowerBound); + LUAU_ASSERT(ft.upperBound); + traverse(ft.lowerBound); + traverse(ft.upperBound); return false; } @@ -446,11 +428,7 @@ struct TypeStringifier void stringify(const std::string& name, const Property& prop) { - if (FFlag::LuauSolverV2 || FFlag::LuauSolverAgnosticStringification) - return _newStringify(name, prop); - - emitKey(name); - stringify(prop.type_DEPRECATED()); + return _newStringify(name, prop); } void stringify(TypePackId tp); @@ -509,96 +487,39 @@ struct TypeStringifier state.result.invalid = true; // Free types are guaranteed to have upper and lower bounds now. - if (FFlag::LuauSolverAgnosticStringification) + LUAU_ASSERT(ftv.lowerBound); + LUAU_ASSERT(ftv.upperBound); + const TypeId lowerBound = follow(ftv.lowerBound); + const TypeId upperBound = follow(ftv.upperBound); + if (get(lowerBound) && get(upperBound)) { - LUAU_ASSERT(ftv.lowerBound); - LUAU_ASSERT(ftv.upperBound); - const TypeId lowerBound = follow(ftv.lowerBound); - const TypeId upperBound = follow(ftv.upperBound); - if (get(lowerBound) && get(upperBound)) - { - state.emit("'"); - state.emit(state.getName(ty)); - if (FInt::DebugLuauVerboseTypeNames >= 1) - state.emit(ftv.polarity); - } - else - { - state.emit("("); - if (!get(lowerBound)) - { - stringify(lowerBound); - state.emit(" <: "); - } - state.emit("'"); - state.emit(state.getName(ty)); - - if (FInt::DebugLuauVerboseTypeNames >= 1) - state.emit(ftv.polarity); - - if (!get(upperBound)) - { - state.emit(" <: "); - stringify(upperBound); - } - state.emit(")"); - } - return; + state.emit("'"); + state.emit(state.getName(ty)); + if (FInt::DebugLuauVerboseTypeNames >= 1) + state.emit(ftv.polarity); } - else if (FFlag::LuauSolverV2 && ftv.lowerBound && ftv.upperBound) + else { - const TypeId lowerBound = follow(ftv.lowerBound); - const TypeId upperBound = follow(ftv.upperBound); - if (get(lowerBound) && get(upperBound)) + state.emit("("); + if (!get(lowerBound)) { - state.emit("'"); - state.emit(state.getName(ty)); - if (FInt::DebugLuauVerboseTypeNames >= 1) - state.emit(ftv.polarity); + stringify(lowerBound); + state.emit(" <: "); } - else + state.emit("'"); + state.emit(state.getName(ty)); + + if (FInt::DebugLuauVerboseTypeNames >= 1) + state.emit(ftv.polarity); + + if (!get(upperBound)) { - state.emit("("); - if (!get(lowerBound)) - { - stringify(lowerBound); - state.emit(" <: "); - } - state.emit("'"); - state.emit(state.getName(ty)); - - if (FInt::DebugLuauVerboseTypeNames >= 1) - state.emit(ftv.polarity); - - if (!get(upperBound)) - { - state.emit(" <: "); - stringify(upperBound); - } - state.emit(")"); + state.emit(" <: "); + stringify(upperBound); } - return; - } - - if (FInt::DebugLuauVerboseTypeNames >= 1) - state.emit("free-"); - - state.emit(state.getName(ty)); - - if (FFlag::LuauSolverAgnosticStringification && FInt::DebugLuauVerboseTypeNames >= 1) - state.emit(ftv.polarity); - else if (FFlag::LuauSolverV2 && FInt::DebugLuauVerboseTypeNames >= 1) - state.emit(ftv.polarity); - - - if (FInt::DebugLuauVerboseTypeNames >= 2) - { - state.emit("-"); - if (FFlag::LuauSolverV2 || FFlag::LuauSolverAgnosticStringification) - state.emitLevel(ftv.scope); - else - state.emit(ftv.level); + state.emit(")"); } + return; } void operator()(TypeId, const BoundType& btv) @@ -626,10 +547,7 @@ struct TypeStringifier if (FInt::DebugLuauVerboseTypeNames >= 2) { state.emit("-"); - if (FFlag::LuauSolverV2 || FFlag::LuauSolverAgnosticStringification) - state.emitLevel(gtv.scope); - else - state.emit(gtv.level); + state.emitLevel(gtv.scope); } } @@ -729,11 +647,8 @@ struct TypeStringifier state.emit(">"); } - if (FFlag::LuauSolverV2 || FFlag::LuauSolverAgnosticStringification) - { - if (ftv.isCheckedFunction) - state.emit("@checked "); - } + if (ftv.isCheckedFunction) + state.emit("@checked "); state.emit("("); @@ -826,35 +741,16 @@ struct TypeStringifier std::string openbrace = "@@@"; std::string closedbrace = "@@@?!"; - switch (state.opts.hideTableKind - ? ((FFlag::LuauSolverV2 || FFlag::LuauSolverAgnosticStringification) ? TableState::Sealed : TableState::Unsealed) - : ttv.state) + switch (state.opts.hideTableKind ? TableState::Sealed : ttv.state) { case TableState::Sealed: - if (FFlag::LuauSolverV2 || FFlag::LuauSolverAgnosticStringification) - { - openbrace = "{"; - closedbrace = "}"; - } - else - { - state.result.invalid = true; - openbrace = "{|"; - closedbrace = "|}"; - } + openbrace = "{"; + closedbrace = "}"; break; case TableState::Unsealed: - if (FFlag::LuauSolverV2 || FFlag::LuauSolverAgnosticStringification) - { - state.result.invalid = true; - openbrace = "{|"; - closedbrace = "|}"; - } - else - { - openbrace = "{"; - closedbrace = "}"; - } + state.result.invalid = true; + openbrace = "{|"; + closedbrace = "|}"; break; case TableState::Free: state.result.invalid = true; @@ -1356,10 +1252,7 @@ struct TypePackStringifier if (FInt::DebugLuauVerboseTypeNames >= 2) { state.emit("-"); - if (FFlag::LuauSolverV2 || FFlag::LuauSolverAgnosticStringification) - state.emitLevel(pack.scope); - else - state.emit(pack.level); + state.emitLevel(pack.scope); } state.emit("..."); @@ -1378,10 +1271,7 @@ struct TypePackStringifier if (FInt::DebugLuauVerboseTypeNames >= 2) { state.emit("-"); - if (FFlag::LuauSolverV2 || FFlag::LuauSolverAgnosticStringification) - state.emitLevel(pack.scope); - else - state.emit(pack.level); + state.emitLevel(pack.scope); } state.emit("..."); @@ -2064,6 +1954,29 @@ std::string dump(const Constraint& c) return s; } +// Converts the given number index into a human-readable string for that index to be used in errors. +// e.g. the index `0` becomes `1st`, `1` becomes `2nd`, `11` becomes `12th`, etc. +std::string toHumanReadableIndex(size_t number) +{ + size_t humanIndex = number + 1; + size_t finalDigit = humanIndex % 10; + + if (humanIndex > 10 && humanIndex < 20) + return std::to_string(humanIndex) + "th"; + + switch (finalDigit) + { + case 1: + return std::to_string(humanIndex) + "st"; + case 2: + return std::to_string(humanIndex) + "nd"; + case 3: + return std::to_string(humanIndex) + "rd"; + default: + return std::to_string(humanIndex) + "th"; + } +} + std::optional getFunctionNameAsString(const AstExpr& expr) { const AstExpr* curr = &expr; diff --git a/Analysis/src/Type.cpp b/Analysis/src/Type.cpp index b48aeb92..b17c263f 100644 --- a/Analysis/src/Type.cpp +++ b/Analysis/src/Type.cpp @@ -2,6 +2,7 @@ #include "Luau/Type.h" #include "Luau/BuiltinDefinitions.h" +#include "Luau/BuiltinTypeFunctions.h" #include "Luau/Common.h" #include "Luau/ConstraintSolver.h" #include "Luau/DenseHash.h" @@ -16,6 +17,7 @@ #include "Luau/VisitType.h" #include +#include #include #include #include @@ -30,8 +32,6 @@ LUAU_FASTINTVARIABLE(LuauTableTypeMaximumStringifierLength, 0) LUAU_FASTINT(LuauTypeInferRecursionLimit) LUAU_FASTFLAG(LuauInstantiateInSubtyping) LUAU_FASTFLAG(LuauUseWorkspacePropToChooseSolver) -LUAU_FASTFLAGVARIABLE(LuauSolverAgnosticVisitType) -LUAU_FASTFLAGVARIABLE(LuauSolverAgnosticSetType) namespace Luau { @@ -719,8 +719,7 @@ TypeId Property::type_DEPRECATED() const void Property::setType(TypeId ty) { readTy = ty; - if (FFlag::LuauSolverV2 || FFlag::LuauSolverAgnosticSetType) - writeTy = ty; + writeTy = ty; } void Property::makeShared() @@ -1004,6 +1003,7 @@ TypeId makeStringMetatable(NotNull builtinTypes, SolverMode mode); BuiltinTypes::BuiltinTypes() : arena(new TypeArena) , debugFreezeArena(FFlag::DebugLuauFreezeArena) + , typeFunctions(std::make_unique()) , nilType(arena->addType(Type{PrimitiveType{PrimitiveType::NilType}, /*persistent*/ true})) , numberType(arena->addType(Type{PrimitiveType{PrimitiveType::Number}, /*persistent*/ true})) , stringType(arena->addType(Type{PrimitiveType{PrimitiveType::String}, /*persistent*/ true})) @@ -1011,7 +1011,7 @@ BuiltinTypes::BuiltinTypes() , threadType(arena->addType(Type{PrimitiveType{PrimitiveType::Thread}, /*persistent*/ true})) , bufferType(arena->addType(Type{PrimitiveType{PrimitiveType::Buffer}, /*persistent*/ true})) , functionType(arena->addType(Type{PrimitiveType{PrimitiveType::Function}, /*persistent*/ true})) - , externType(arena->addType(Type{ExternType{"class", {}, std::nullopt, std::nullopt, {}, {}, {}, {}}, /*persistent*/ true})) + , externType(arena->addType(Type{ExternType{"userdata", {}, std::nullopt, std::nullopt, {}, {}, {}, {}}, /*persistent*/ true})) , tableType(arena->addType(Type{PrimitiveType{PrimitiveType::Table}, /*persistent*/ true})) , emptyTableType(arena->addType(Type{TableType{TableState::Sealed, TypeLevel{}, nullptr}, /*persistent*/ true})) , trueType(arena->addType(Type{SingletonType{BooleanSingleton{true}}, /*persistent*/ true})) diff --git a/Analysis/src/TypeChecker2.cpp b/Analysis/src/TypeChecker2.cpp index 8a7b8abb..1444f351 100644 --- a/Analysis/src/TypeChecker2.cpp +++ b/Analysis/src/TypeChecker2.cpp @@ -4,6 +4,7 @@ #include "Luau/Ast.h" #include "Luau/AstUtils.h" #include "Luau/AstQuery.h" +#include "Luau/BuiltinDefinitions.h" #include "Luau/Common.h" #include "Luau/DcrLogger.h" #include "Luau/DenseHash.h" @@ -33,21 +34,20 @@ LUAU_FASTFLAG(DebugLuauMagicTypes) -LUAU_FASTFLAG(LuauReturnMappedGenericPacksFromSubtyping2) -LUAU_FASTFLAG(LuauInferActualIfElseExprType2) LUAU_FASTFLAG(LuauNoMoreComparisonTypeFunctions) LUAU_FASTFLAG(LuauTrackUniqueness) -LUAU_FASTFLAG(LuauNameConstraintRestrictRecursiveTypes) -LUAU_FASTFLAG(LuauSubtypingGenericPacksDoesntUseVariance) LUAU_FASTFLAGVARIABLE(LuauIceLess) LUAU_FASTFLAG(LuauExplicitSkipBoundTypes) -LUAU_FASTFLAGVARIABLE(LuauAllowMixedTables) LUAU_FASTFLAGVARIABLE(LuauSimplifyIntersectionForLiteralSubtypeCheck) -LUAU_FASTFLAGVARIABLE(LuauRemoveGenericErrorForParams) LUAU_FASTFLAG(LuauNoConstraintGenRecursionLimitIce) LUAU_FASTFLAGVARIABLE(LuauAddErrorCaseForIncompatibleTypePacks) LUAU_FASTFLAGVARIABLE(LuauAddConditionalContextForTernary) +LUAU_FASTFLAGVARIABLE(LuauCheckForInWithSubtyping2) +LUAU_FASTFLAGVARIABLE(LuauNoOrderingTypeFunctions) +LUAU_FASTFLAG(LuauPassBindableGenericsByReference) +LUAU_FASTFLAG(LuauSimplifyIntersectionNoTreeSet) +LUAU_FASTFLAG(LuauAddRefinementToAssertions) namespace Luau { @@ -740,10 +740,7 @@ void TypeChecker2::visit(AstStatReturn* ret) { if (idx < head.size()) { - if (FFlag::LuauInferActualIfElseExprType2) - isSubtype &= testLiteralOrAstTypeIsSubtype(ret->list.data[idx], head[idx]); - else - isSubtype &= testPotentialLiteralIsSubtype(ret->list.data[idx], head[idx]); + isSubtype &= testLiteralOrAstTypeIsSubtype(ret->list.data[idx], head[idx]); actualHead.push_back(head[idx]); } else @@ -774,10 +771,7 @@ void TypeChecker2::visit(AstStatReturn* ret) else { auto lastType = head[ret->list.size - 1]; - if (FFlag::LuauInferActualIfElseExprType2) - isSubtype &= testLiteralOrAstTypeIsSubtype(lastExpr, lastType); - else - isSubtype &= testPotentialLiteralIsSubtype(lastExpr, lastType); + isSubtype &= testLiteralOrAstTypeIsSubtype(lastExpr, lastType); actualHead.push_back(lastType); } @@ -996,10 +990,16 @@ void TypeChecker2::visit(AstStatForIn* forInStatement) reportError(GenericError{"__iter metamethod's next() function does not return enough values"}, getLocation(forInStatement->values)); else reportError(GenericError{"next() does not return enough values"}, forInStatement->values.data[0]->location); + + if (FFlag::LuauCheckForInWithSubtyping2) + return; } - for (size_t i = 0; i < std::min(expectedVariableTypes.head.size(), variableTypes.size()); ++i) - testIsSubtype(variableTypes[i], expectedVariableTypes.head[i], forInStatement->vars.data[i]->location); + if (!FFlag::LuauCheckForInWithSubtyping2) + { + for (size_t i = 0; i < std::min(expectedVariableTypes.head.size(), variableTypes.size()); ++i) + testIsSubtype(variableTypes[i], expectedVariableTypes.head[i], forInStatement->vars.data[i]->location); + } // nextFn is going to be invoked with (arrayTy, startIndexTy) @@ -1029,27 +1029,61 @@ void TypeChecker2::visit(AstStatForIn* forInStatement) reportError(GenericError{"__iter metamethod must return (next[, table[, state]])"}, getLocation(forInStatement->values)); else reportError(CountMismatch{2, std::nullopt, firstIterationArgCount, CountMismatch::Arg}, forInStatement->values.data[0]->location); - } + if (FFlag::LuauCheckForInWithSubtyping2) + return; + } else if (actualArgCount < minCount) { if (isMm) reportError(GenericError{"__iter metamethod must return (next[, table[, state]])"}, getLocation(forInStatement->values)); else reportError(CountMismatch{2, std::nullopt, firstIterationArgCount, CountMismatch::Arg}, forInStatement->values.data[0]->location); - } + if (FFlag::LuauCheckForInWithSubtyping2) + return; + } - if (iterTys.size() >= 2 && flattenedArgTypes.head.size() > 0) + if (FFlag::LuauCheckForInWithSubtyping2) { - size_t valueIndex = forInStatement->values.size > 1 ? 1 : 0; - testIsSubtype(iterTys[1], flattenedArgTypes.head[0], forInStatement->values.data[valueIndex]->location); - } + const TypeId iterFunc = follow(iterTys[0]); + + std::vector prospectiveArgTypes = std::vector(iterTys.begin() + 1, iterTys.end()); + // Right pad with nils if needed + if (const TypePack* iterFuncArgs = get(follow(iterFtv->argTypes)); + iterFuncArgs && iterFuncArgs->head.size() > prospectiveArgTypes.size()) + prospectiveArgTypes.resize(iterFuncArgs->head.size(), builtinTypes->nilType); + const TypePackId prospectiveArgs = arena.addTypePack(prospectiveArgTypes, std::nullopt); + + std::vector prospectiveRetTypes = {}; + if (variableTypes.size() > 0) // Type inference intersects the control variable with ~nil, so we make it optional here + prospectiveRetTypes.emplace_back(arena.addType(UnionType{{variableTypes[0], builtinTypes->nilType}})); + if (variableTypes.size() > 1) + prospectiveRetTypes.emplace_back(variableTypes[1]); + // Right pad with anys, since not all the return values are used (eg for key in pairs(t)) + if (const TypePack* iterFuncRets = get(follow(iterFtv->retTypes)); + iterFuncRets && iterFuncRets->head.size() > prospectiveRetTypes.size()) + prospectiveRetTypes.resize(iterFuncRets->head.size(), builtinTypes->anyType); + // Add a variadic any tail because sometimes iterFunc returns a variadic pack (see forin_metatable_iter_mm) + const TypePackId prospectiveRets = arena.addTypePack(prospectiveRetTypes, builtinTypes->anyTypePack); - if (iterTys.size() == 3 && flattenedArgTypes.head.size() > 1) + const TypeId prospectiveFunction = arena.addType(FunctionType{prospectiveArgs, prospectiveRets, std::nullopt, isMm}); + + testIsSubtypeForInStat(iterFunc, prospectiveFunction, *forInStatement); + } + else { - size_t valueIndex = forInStatement->values.size > 2 ? 2 : 0; - testIsSubtype(iterTys[2], flattenedArgTypes.head[1], forInStatement->values.data[valueIndex]->location); + if (iterTys.size() >= 2 && flattenedArgTypes.head.size() > 0) + { + size_t valueIndex = forInStatement->values.size > 1 ? 1 : 0; + testIsSubtype(iterTys[1], flattenedArgTypes.head[0], forInStatement->values.data[valueIndex]->location); + } + + if (iterTys.size() == 3 && flattenedArgTypes.head.size() > 1) + { + size_t valueIndex = forInStatement->values.size > 2 ? 2 : 0; + testIsSubtype(iterTys[2], flattenedArgTypes.head[1], forInStatement->values.data[valueIndex]->location); + } } }; @@ -1290,13 +1324,10 @@ void TypeChecker2::visit(AstStatTypeAlias* stat) if (!module->astScopes.contains(stat)) return; - if (FFlag::LuauNameConstraintRestrictRecursiveTypes) + if (const Scope* scope = findInnermostScope(stat->location)) { - if (const Scope* scope = findInnermostScope(stat->location)) - { - if (scope->isInvalidTypeAliasName(stat->name.value)) - reportError(RecursiveRestraintViolation{}, stat->location); - } + if (scope->isInvalidTypeAliasName(stat->name.value)) + reportError(RecursiveRestraintViolation{}, stat->location); } visitGenerics(stat->generics, stat->genericPacks); @@ -1731,13 +1762,36 @@ void TypeChecker2::visitCall(AstExprCall* call) void TypeChecker2::visit(AstExprCall* call) { + std::optional flipper; + if (FFlag::LuauAddRefinementToAssertions) + { + // We want to preserve the existing conditional context if we are in a `typeof` call. + if (!matchTypeOf(*call)) + flipper.emplace(&typeContext, TypeContext::Default); + + visit(call->func, ValueContext::RValue); + } + else { InConditionalContext flipper(&typeContext, TypeContext::Default); visit(call->func, ValueContext::RValue); } - for (AstExpr* arg : call->args) - visit(arg, ValueContext::RValue); + if (FFlag::LuauAddRefinementToAssertions && matchAssert(*call) && call->args.size > 0) + { + { + InConditionalContext flipper(&typeContext); + visit(call->args.data[0], ValueContext::RValue); + } + + for (size_t i = 1; i < call->args.size; ++i) + visit(call->args.data[i], ValueContext::RValue); + } + else + { + for (AstExpr* arg : call->args) + visit(arg, ValueContext::RValue); + } visitCall(call); } @@ -2154,7 +2208,7 @@ static bool isOkToCompare( // Comparison with never is always ok. else if (NormalizationResult::True != normalizer.isInhabited(normLeft.get()) || - NormalizationResult::True != normalizer.isInhabited(normRight.get())) + NormalizationResult::True != normalizer.isInhabited(normRight.get())) return true; // Comparisons between different string singleton types is allowed even @@ -2390,16 +2444,23 @@ TypeId TypeChecker2::visit(AstExprBinary* expr, AstNode* overrideKey) { if (isComparison) { - reportError( - GenericError{format( - "Types '%s' and '%s' cannot be compared with %s because neither type's metatable has a '%s' metamethod", - toString(leftType).c_str(), - toString(rightType).c_str(), - toString(expr->op).c_str(), - it->second - )}, - expr->location - ); + if (FFlag::LuauNoOrderingTypeFunctions) + { + reportError(CannotCompareUnrelatedTypes{leftType, rightType, expr->op}, expr->location); + } + else + { + reportError( + GenericError{format( + "Types '%s' and '%s' cannot be compared with %s because neither type's metatable has a '%s' metamethod", + toString(leftType).c_str(), + toString(rightType).c_str(), + toString(expr->op).c_str(), + it->second + )}, + expr->location + ); + } } else { @@ -2421,15 +2482,22 @@ TypeId TypeChecker2::visit(AstExprBinary* expr, AstNode* overrideKey) { if (isComparison) { - reportError( - GenericError{format( - "Types '%s' and '%s' cannot be compared with %s because neither type has a metatable", - toString(leftType).c_str(), - toString(rightType).c_str(), - toString(expr->op).c_str() - )}, - expr->location - ); + if (FFlag::LuauNoOrderingTypeFunctions) + { + reportError(CannotCompareUnrelatedTypes{leftType, rightType, expr->op}, expr->location); + } + else + { + reportError( + GenericError{format( + "Types '%s' and '%s' cannot be compared with %s because neither type has a metatable", + toString(leftType).c_str(), + toString(rightType).c_str(), + toString(expr->op).c_str() + )}, + expr->location + ); + } } else { @@ -2481,16 +2549,37 @@ TypeId TypeChecker2::visit(AstExprBinary* expr, AstNode* overrideKey) if (normLeft && normalizer.isInhabited(normLeft.get()) == NormalizationResult::False) return builtinTypes->booleanType; - if (normLeft && normLeft->isExactlyNumber()) + if (FFlag::LuauNoOrderingTypeFunctions) { - testIsSubtype(rightType, builtinTypes->numberType, expr->right->location); - return builtinTypes->booleanType; - } + // This could be a little wasteful, as we already have normalized + // types, but correctly handles cases like `_: (T & number) <= _: (T & number)`. - if (normLeft && normLeft->isSubtypeOfString()) + if (subtyping->isSubtype(leftType, builtinTypes->numberType, scope).isSubtype) + { + testIsSubtype(rightType, builtinTypes->numberType, expr->right->location); + return builtinTypes->booleanType; + } + + if (subtyping->isSubtype(leftType, builtinTypes->stringType, scope).isSubtype) + { + testIsSubtype(rightType, builtinTypes->stringType, expr->right->location); + return builtinTypes->booleanType; + } + } + else { - testIsSubtype(rightType, builtinTypes->stringType, expr->right->location); - return builtinTypes->booleanType; + + if (normLeft && normLeft->isExactlyNumber()) + { + testIsSubtype(rightType, builtinTypes->numberType, expr->right->location); + return builtinTypes->booleanType; + } + + if (normLeft && normLeft->isSubtypeOfString()) + { + testIsSubtype(rightType, builtinTypes->stringType, expr->right->location); + return builtinTypes->booleanType; + } } reportError( @@ -2702,35 +2791,6 @@ void TypeChecker2::visit(AstTypeReference* ty) size_t typesRequired = alias->typeParams.size(); size_t packsRequired = alias->typePackParams.size(); - bool hasDefaultTypes = std::any_of( - alias->typeParams.begin(), - alias->typeParams.end(), - [](auto&& el) - { - return el.defaultValue.has_value(); - } - ); - - bool hasDefaultPacks = std::any_of( - alias->typePackParams.begin(), - alias->typePackParams.end(), - [](auto&& el) - { - return el.defaultValue.has_value(); - } - ); - - if (!FFlag::LuauRemoveGenericErrorForParams) - { - if (!ty->hasParameterList) - { - if ((!alias->typeParams.empty() && !hasDefaultTypes) || (!alias->typePackParams.empty() && !hasDefaultPacks)) - { - reportError(GenericError{"Type parameter list is required"}, ty->location); - } - } - } - size_t typesProvided = 0; size_t extraTypes = 0; size_t packsProvided = 0; @@ -2805,17 +2865,9 @@ void TypeChecker2::visit(AstTypeReference* ty) } } - if (FFlag::LuauRemoveGenericErrorForParams) - { - // If the type parameter list is explicitly provided, allow an empty type pack to satisfy the expected pack count. - if (extraTypes == 0 && packsProvided + 1 == packsRequired && ty->hasParameterList) - packsProvided += 1; - } - else - { - if (extraTypes == 0 && packsProvided + 1 == packsRequired) - packsProvided += 1; - } + // If the type parameter list is explicitly provided, allow an empty type pack to satisfy the expected pack count. + if (extraTypes == 0 && packsProvided + 1 == packsRequired && ty->hasParameterList) + packsProvided += 1; if (typesProvided != typesRequired || packsProvided != packsRequired) { @@ -2956,22 +3008,9 @@ Reasonings TypeChecker2::explainReasonings_(TID subTy, TID superTy, Location loc if (reasoning.subPath.empty() && reasoning.superPath.empty()) continue; - std::optional optSubLeaf; - if (FFlag::LuauSubtypingGenericPacksDoesntUseVariance) - optSubLeaf = traverse(subTy, reasoning.subPath, builtinTypes, subtyping->arena); - else if (FFlag::LuauReturnMappedGenericPacksFromSubtyping2) - optSubLeaf = traverse_DEPRECATED(subTy, reasoning.subPath, builtinTypes, NotNull{&r.mappedGenericPacks_DEPRECATED}, subtyping->arena); - else - optSubLeaf = traverse_DEPRECATED(subTy, reasoning.subPath, builtinTypes); - - std::optional optSuperLeaf; - if (FFlag::LuauSubtypingGenericPacksDoesntUseVariance) - optSuperLeaf = traverse(superTy, reasoning.superPath, builtinTypes, subtyping->arena); - else if (FFlag::LuauReturnMappedGenericPacksFromSubtyping2) - optSuperLeaf = - traverse_DEPRECATED(superTy, reasoning.superPath, builtinTypes, NotNull{&r.mappedGenericPacks_DEPRECATED}, subtyping->arena); - else - optSuperLeaf = traverse_DEPRECATED(superTy, reasoning.superPath, builtinTypes); + std::optional optSubLeaf = traverse(subTy, reasoning.subPath, builtinTypes, subtyping->arena); + + std::optional optSuperLeaf = traverse(superTy, reasoning.superPath, builtinTypes, subtyping->arena); if (!optSubLeaf || !optSuperLeaf) { @@ -3119,29 +3158,26 @@ bool TypeChecker2::testPotentialLiteralIsSubtype(AstExpr* expr, TypeId expectedT auto exprType = follow(lookupType(expr)); expectedType = follow(expectedType); - if (FFlag::LuauInferActualIfElseExprType2) + if (auto group = expr->as()) { - if (auto group = expr->as()) - { - return testPotentialLiteralIsSubtype(group->expr, expectedType); - } - else if (auto ifElse = expr->as()) - { - bool passes = testPotentialLiteralIsSubtype(ifElse->trueExpr, expectedType); - passes &= testPotentialLiteralIsSubtype(ifElse->falseExpr, expectedType); - return passes; - } - else if (auto binExpr = expr->as(); binExpr && binExpr->op == AstExprBinary::Or) - { - // In this case: `{ ... } or { ... }` is literal _enough_ that - // we should do this covariant check. - auto relaxedExpectedLhs = module->internalTypes.addType(UnionType{{builtinTypes->falsyType, expectedType}}); - bool passes = testPotentialLiteralIsSubtype(binExpr->left, relaxedExpectedLhs); - passes &= testPotentialLiteralIsSubtype(binExpr->right, expectedType); - return passes; - } - // FIXME: We probably should do a check for `and` here. + return testPotentialLiteralIsSubtype(group->expr, expectedType); + } + else if (auto ifElse = expr->as()) + { + bool passes = testPotentialLiteralIsSubtype(ifElse->trueExpr, expectedType); + passes &= testPotentialLiteralIsSubtype(ifElse->falseExpr, expectedType); + return passes; + } + else if (auto binExpr = expr->as(); binExpr && binExpr->op == AstExprBinary::Or) + { + // In this case: `{ ... } or { ... }` is literal _enough_ that + // we should do this covariant check. + auto relaxedExpectedLhs = module->internalTypes.addType(UnionType{{builtinTypes->falsyType, expectedType}}); + bool passes = testPotentialLiteralIsSubtype(binExpr->left, relaxedExpectedLhs); + passes &= testPotentialLiteralIsSubtype(binExpr->right, expectedType); + return passes; } + // FIXME: We probably should do a check for `and` here. auto exprTable = expr->as(); auto exprTableType = get(exprType); @@ -3171,10 +3207,22 @@ bool TypeChecker2::testPotentialLiteralIsSubtype(AstExpr* expr, TypeId expectedT { // If we _happen_ to have an intersection of tables, let's try to // construct it and use it as the input to this algorithm. - std::set parts{begin(itv), end(itv)}; - TypeId simplified = simplifyIntersection(builtinTypes, NotNull{&module->internalTypes}, std::move(parts)).result; - if (is(simplified)) - return testPotentialLiteralIsSubtype(expr, simplified); + if (FFlag::LuauSimplifyIntersectionNoTreeSet) + { + TypeIds parts; + parts.insert(begin(itv), end(itv)); + TypeId simplified = simplifyIntersection(builtinTypes, NotNull{&module->internalTypes}, std::move(parts)).result; + if (is(simplified)) + return testPotentialLiteralIsSubtype(expr, simplified); + } + else + { + + std::set parts{begin(itv), end(itv)}; + TypeId simplified = simplifyIntersection_DEPRECATED(builtinTypes, NotNull{&module->internalTypes}, std::move(parts)).result; + if (is(simplified)) + return testPotentialLiteralIsSubtype(expr, simplified); + } } } @@ -3196,16 +3244,8 @@ bool TypeChecker2::testPotentialLiteralIsSubtype(AstExpr* expr, TypeId expectedT { NotNull scope{findInnermostScope(expr->location)}; - if (FFlag::LuauAllowMixedTables) - { - auto result = subtyping->isSubtype(/* subTy */ builtinTypes->numberType, /* superTy */ expectedTableType->indexer->indexType, scope); - isArrayLike = result.isSubtype || isErrorSuppressing(expr->location, expectedTableType->indexer->indexType); - } - else - { - auto result = subtyping->isSubtype(/* subTy */ expectedTableType->indexer->indexType, /* superTy */ builtinTypes->numberType, scope); - isArrayLike = result.isSubtype; - } + auto result = subtyping->isSubtype(/* subTy */ builtinTypes->numberType, /* superTy */ expectedTableType->indexer->indexType, scope); + isArrayLike = result.isSubtype || isErrorSuppressing(expr->location, expectedTableType->indexer->indexType); } bool isSubtype = true; @@ -3303,7 +3343,8 @@ bool TypeChecker2::testIsSubtype(TypeId subTy, TypeId superTy, Location location bool TypeChecker2::testIsSubtype(TypePackId subTy, TypePackId superTy, Location location) { NotNull scope{findInnermostScope(location)}; - SubtypingResult r = subtyping->isSubtype(subTy, superTy, scope); + SubtypingResult r = FFlag::LuauPassBindableGenericsByReference ? subtyping->isSubtype(subTy, superTy, scope, {}) + : subtyping->isSubtype_DEPRECATED(subTy, superTy, scope); if (!isErrorSuppressing(location, subTy)) { @@ -3320,6 +3361,103 @@ bool TypeChecker2::testIsSubtype(TypePackId subTy, TypePackId superTy, Location return r.isSubtype; } +void TypeChecker2::maybeReportSubtypingError(const TypeId subTy, const TypeId superTy, const Location& location) +{ + LUAU_ASSERT(FFlag::LuauCheckForInWithSubtyping2); + switch (shouldSuppressErrors(NotNull{&normalizer}, subTy).orElse(shouldSuppressErrors(NotNull{&normalizer}, superTy))) + { + case ErrorSuppression::Suppress: + return; + case ErrorSuppression::NormalizationFailed: + reportError(NormalizationTooComplex{}, location); + break; + case ErrorSuppression::DoNotSuppress: + break; + default: + break; + } + + reportError(TypeMismatch{superTy, subTy}, location); +} + +void TypeChecker2::testIsSubtypeForInStat(const TypeId iterFunc, const TypeId prospectiveFunc, const AstStatForIn& forInStat) +{ + LUAU_ASSERT(FFlag::LuauCheckForInWithSubtyping2); + LUAU_ASSERT(get(follow(iterFunc))); + LUAU_ASSERT(get(follow(prospectiveFunc))); + + const Location& iterFuncLocation = forInStat.values.data[0]->location; + + const NotNull scope{findInnermostScope(iterFuncLocation)}; + SubtypingResult r = subtyping->isSubtype(iterFunc, prospectiveFunc, scope); + + if (!isErrorSuppressing(iterFuncLocation, iterFunc)) + { + for (auto& e : r.errors) + e.location = iterFuncLocation; + } + reportErrors(std::move(r.errors)); + + if (r.normalizationTooComplex) + reportError(NormalizationTooComplex{}, iterFuncLocation); + + if (r.isSubtype) + return; + + for (auto& reasoning : r.reasoning) + { + // We can give more specific errors if superPath reasoning is of form [Arguments|Returns, PackIndex[n]] + if (reasoning.subPath.empty() || reasoning.superPath.components.size() != 2) + { + maybeReportSubtypingError(prospectiveFunc, iterFunc, iterFuncLocation); + return; + } + + const TypePath::PackField* pf = get_if(&reasoning.superPath.components[0]); + + if (!pf || *pf == TypePath::PackField::Tail) + { + maybeReportSubtypingError(prospectiveFunc, iterFunc, iterFuncLocation); + return; + } + + const TypePath::Index* index = get_if(&reasoning.superPath.components[1]); + if (!index || index->variant != TypePath::Index::Variant::Pack) + { + maybeReportSubtypingError(prospectiveFunc, iterFunc, iterFuncLocation); + return; + } + + std::optional subLeaf = traverseForType(iterFunc, reasoning.subPath, builtinTypes, subtyping->arena); + if (!subLeaf) + continue; + + std::optional superLeaf = traverseForType(prospectiveFunc, reasoning.superPath, builtinTypes, subtyping->arena); + if (!superLeaf) + continue; + + if (*pf == TypePath::PackField::Arguments) + { + // The first component of `forInStat.values` is the iterator function itself + Location loc = index->index + 1 >= forInStat.values.size + ? iterFuncLocation + : forInStat.values.data[index->index + 1]->location; + maybeReportSubtypingError(*subLeaf, *superLeaf, loc); + } + else if (*pf == TypePath::PackField::Returns) + { + Location loc = index->index > forInStat.vars.size ? iterFuncLocation : forInStat.vars.data[index->index]->location; + maybeReportSubtypingError(*subLeaf, *superLeaf, loc); + } + else + { + LUAU_ASSERT(!"Unknown PackField type"); + maybeReportSubtypingError(prospectiveFunc, iterFunc, iterFuncLocation); + return; + } + } +} + void TypeChecker2::reportError(TypeErrorData data, const Location& location) { if (auto utk = get_if(&data)) diff --git a/Analysis/src/TypeFunction.cpp b/Analysis/src/TypeFunction.cpp index daa63b72..0e2fc6db 100644 --- a/Analysis/src/TypeFunction.cpp +++ b/Analysis/src/TypeFunction.cpp @@ -35,6 +35,7 @@ LUAU_FASTFLAG(DebugLuauEqSatSimplification) LUAU_FASTFLAGVARIABLE(DebugLuauLogTypeFamilies) LUAU_FASTFLAG(LuauExplicitSkipBoundTypes) +LUAU_FASTFLAGVARIABLE(LuauEnqueueUnionsOfDistributedTypeFunctions) namespace Luau { @@ -380,9 +381,22 @@ struct TypeFunctionReducer result.messages.emplace_back(location, UserDefinedTypeFunctionError{std::move(message)}); if (reduction.result) + { replace(subject, *reduction.result); + if (FFlag::LuauEnqueueUnionsOfDistributedTypeFunctions) + { + for (auto ty : reduction.freshTypes) + { + if constexpr (std::is_same_v) + queuedTys.push_back(ty); + else if constexpr (std::is_same_v) + queuedTps.push_back(ty); + } + } + } else { + LUAU_ASSERT(reduction.freshTypes.empty()); irreducible.insert(subject); if (reduction.error.has_value()) diff --git a/Analysis/src/TypeFunctionReductionGuesser.cpp b/Analysis/src/TypeFunctionReductionGuesser.cpp index e419ab8a..b8e7fa59 100644 --- a/Analysis/src/TypeFunctionReductionGuesser.cpp +++ b/Analysis/src/TypeFunctionReductionGuesser.cpp @@ -184,7 +184,7 @@ TypeFunctionReductionGuessResult TypeFunctionReductionGuesser::guessTypeFunction recommendedAnnotation = builtins->unknownType; else recommendedAnnotation = follow(*guessedReturnType); - if (auto t = get(recommendedAnnotation)) + if (get(recommendedAnnotation)) recommendedAnnotation = builtins->unknownType; toInfer.clear(); @@ -270,14 +270,14 @@ std::optional TypeFunctionReductionGuesser::tryAssignOperandType(TypeId { // Because we collect innermost instances first, if we see a type function instance as an operand, // We try to check if we guessed a type for it - if (auto tfit = get(ty)) + if (get(ty)) { if (functionReducesTo.contains(ty)) return {functionReducesTo[ty]}; } // If ty is a generic, we need to check if we inferred a substitution - if (auto gt = get(ty)) + if (get(ty)) { if (substitutable.contains(ty)) return {substitutable[ty]}; @@ -338,12 +338,12 @@ void TypeFunctionReductionGuesser::inferTypeFunctionSubstitutions(TypeId ty, con { TypeId arg = follow(instance->typeArguments[i]); TypeId inference = follow(result.operandInference[i]); - if (auto tfit = get(arg)) + if (get(arg)) { if (!functionReducesTo.contains(arg)) functionReducesTo.try_insert(arg, inference); } - else if (auto gt = get(arg)) + else if (get(arg)) substitutable[arg] = inference; } } diff --git a/Analysis/src/TypeFunctionRuntime.cpp b/Analysis/src/TypeFunctionRuntime.cpp index 2dfd21d5..96aa8a47 100644 --- a/Analysis/src/TypeFunctionRuntime.cpp +++ b/Analysis/src/TypeFunctionRuntime.cpp @@ -275,7 +275,7 @@ static std::string getTag(lua_State* L, TypeFunctionTypeId ty) return "never"; else if (get(ty)) return "any"; - else if (auto s = get(ty)) + else if (get(ty)) return "singleton"; else if (get(ty)) return "negation"; @@ -893,7 +893,7 @@ static int setTableIndexer(lua_State* L) TypeFunctionTypeId key = getTypeUserData(L, 2); TypeFunctionTypeId value = getTypeUserData(L, 3); - if (auto tfnt = get(key)) + if (get(key)) { tftt->indexer = std::nullopt; return 0; @@ -2373,11 +2373,11 @@ class TypeFunctionCloner break; } } - else if (auto u = get(ty)) + else if (get(ty)) target = typeFunctionRuntime->typeArena.allocate(TypeFunctionUnknownType{}); - else if (auto a = get(ty)) + else if (get(ty)) target = typeFunctionRuntime->typeArena.allocate(TypeFunctionNeverType{}); - else if (auto a = get(ty)) + else if (get(ty)) target = typeFunctionRuntime->typeArena.allocate(TypeFunctionAnyType{}); else if (auto s = get(ty)) { @@ -2386,20 +2386,20 @@ class TypeFunctionCloner else if (auto ss = get(s)) target = typeFunctionRuntime->typeArena.allocate(TypeFunctionSingletonType{TypeFunctionStringSingleton{ss->value}}); } - else if (auto u = get(ty)) + else if (get(ty)) target = typeFunctionRuntime->typeArena.allocate(TypeFunctionUnionType{{}}); - else if (auto i = get(ty)) + else if (get(ty)) target = typeFunctionRuntime->typeArena.allocate(TypeFunctionIntersectionType{{}}); - else if (auto n = get(ty)) + else if (get(ty)) target = typeFunctionRuntime->typeArena.allocate(TypeFunctionNegationType{{}}); - else if (auto t = get(ty)) + else if (get(ty)) target = typeFunctionRuntime->typeArena.allocate(TypeFunctionTableType{{}, std::nullopt, std::nullopt}); - else if (auto f = get(ty)) + else if (get(ty)) { TypeFunctionTypePackId emptyTypePack = typeFunctionRuntime->typePackArena.allocate(TypeFunctionTypePack{}); target = typeFunctionRuntime->typeArena.allocate(TypeFunctionFunctionType{{}, {}, emptyTypePack, emptyTypePack}); } - else if (auto c = get(ty)) + else if (get(ty)) target = ty; // Don't copy a class since they are immutable else if (auto g = get(ty)) target = typeFunctionRuntime->typeArena.allocate(TypeFunctionGenericType{g->isNamed, g->isPack, g->name}); @@ -2418,9 +2418,9 @@ class TypeFunctionCloner // Create a shallow serialization TypeFunctionTypePackId target = {}; - if (auto tPack = get(tp)) + if (get(tp)) target = typeFunctionRuntime->typePackArena.allocate(TypeFunctionTypePack{{}}); - else if (auto vPack = get(tp)) + else if (get(tp)) target = typeFunctionRuntime->typePackArena.allocate(TypeFunctionVariadicTypePack{}); else if (auto gPack = get(tp)) target = typeFunctionRuntime->typePackArena.allocate(TypeFunctionGenericTypePack{gPack->isNamed, gPack->name}); diff --git a/Analysis/src/TypeFunctionRuntimeBuilder.cpp b/Analysis/src/TypeFunctionRuntimeBuilder.cpp index e9b09d47..a5d5b26c 100644 --- a/Analysis/src/TypeFunctionRuntimeBuilder.cpp +++ b/Analysis/src/TypeFunctionRuntimeBuilder.cpp @@ -173,11 +173,11 @@ class TypeFunctionSerializer } } } - else if (auto u = get(ty)) + else if (get(ty)) target = typeFunctionRuntime->typeArena.allocate(TypeFunctionUnknownType{}); - else if (auto a = get(ty)) + else if (get(ty)) target = typeFunctionRuntime->typeArena.allocate(TypeFunctionNeverType{}); - else if (auto a = get(ty)) + else if (get(ty)) target = typeFunctionRuntime->typeArena.allocate(TypeFunctionAnyType{}); else if (auto s = get(ty)) { @@ -191,22 +191,22 @@ class TypeFunctionSerializer state->errors.push_back(error); } } - else if (auto u = get(ty)) + else if (get(ty)) target = typeFunctionRuntime->typeArena.allocate(TypeFunctionUnionType{{}}); - else if (auto i = get(ty)) + else if (get(ty)) target = typeFunctionRuntime->typeArena.allocate(TypeFunctionIntersectionType{{}}); - else if (auto n = get(ty)) + else if (get(ty)) target = typeFunctionRuntime->typeArena.allocate(TypeFunctionNegationType{{}}); - else if (auto t = get(ty)) + else if (get(ty)) target = typeFunctionRuntime->typeArena.allocate(TypeFunctionTableType{{}, std::nullopt, std::nullopt}); - else if (auto m = get(ty)) + else if (get(ty)) target = typeFunctionRuntime->typeArena.allocate(TypeFunctionTableType{{}, std::nullopt, std::nullopt}); - else if (auto f = get(ty)) + else if (get(ty)) { TypeFunctionTypePackId emptyTypePack = typeFunctionRuntime->typePackArena.allocate(TypeFunctionTypePack{}); target = typeFunctionRuntime->typeArena.allocate(TypeFunctionFunctionType{{}, {}, emptyTypePack, emptyTypePack}); } - else if (auto c = get(ty)) + else if (get(ty)) { // Since there aren't any new class types being created in type functions, we will deserialize by using a direct reference to the original // class @@ -241,9 +241,9 @@ class TypeFunctionSerializer // Create a shallow serialization TypeFunctionTypePackId target = {}; - if (auto tPack = get(tp)) + if (get(tp)) target = typeFunctionRuntime->typePackArena.allocate(TypeFunctionTypePack{{}}); - else if (auto vPack = get(tp)) + else if (get(tp)) target = typeFunctionRuntime->typePackArena.allocate(TypeFunctionVariadicTypePack{}); else if (auto gPack = get(tp)) { @@ -683,11 +683,11 @@ class TypeFunctionDeserializer state->ctx->ice->ice("Deserializing user defined type function arguments: mysterious type is being deserialized"); } } - else if (auto u = get(ty)) + else if (get(ty)) target = state->ctx->builtins->unknownType; - else if (auto n = get(ty)) + else if (get(ty)) target = state->ctx->builtins->neverType; - else if (auto a = get(ty)) + else if (get(ty)) target = state->ctx->builtins->anyType; else if (auto s = get(ty)) { @@ -698,11 +698,11 @@ class TypeFunctionDeserializer else state->ctx->ice->ice("Deserializing user defined type function arguments: mysterious type is being deserialized"); } - else if (auto u = get(ty)) + else if (get(ty)) target = state->ctx->arena->addTV(Type(UnionType{{}})); - else if (auto i = get(ty)) + else if (get(ty)) target = state->ctx->arena->addTV(Type(IntersectionType{{}})); - else if (auto n = get(ty)) + else if (get(ty)) target = state->ctx->arena->addType(NegationType{state->ctx->builtins->unknownType}); else if (auto t = get(ty); t && !t->metatable.has_value()) target = state->ctx->arena->addType(TableType{TableType::Props{}, std::nullopt, TypeLevel{}, TableState::Sealed}); @@ -711,7 +711,7 @@ class TypeFunctionDeserializer TypeId emptyTable = state->ctx->arena->addType(TableType{TableType::Props{}, std::nullopt, TypeLevel{}, TableState::Sealed}); target = state->ctx->arena->addType(MetatableType{emptyTable, emptyTable}); } - else if (auto f = get(ty)) + else if (get(ty)) { TypePackId emptyTypePack = state->ctx->arena->addTypePack(TypePack{}); target = state->ctx->arena->addType(FunctionType{emptyTypePack, emptyTypePack, {}, false}); @@ -762,11 +762,11 @@ class TypeFunctionDeserializer // Create a shallow deserialization TypePackId target = {}; - if (auto tPack = get(tp)) + if (get(tp)) { target = state->ctx->arena->addTypePack(TypePack{}); } - else if (auto vPack = get(tp)) + else if (get(tp)) { target = state->ctx->arena->addTypePack(VariadicTypePack{}); } diff --git a/Analysis/src/TypeIds.cpp b/Analysis/src/TypeIds.cpp index 1366a35e..56666cd7 100644 --- a/Analysis/src/TypeIds.cpp +++ b/Analysis/src/TypeIds.cpp @@ -35,6 +35,23 @@ void TypeIds::clear() hash = 0; } +void TypeIds::clearWithoutRealloc() +{ + // Our goal for this function is to logically clear the TypeIds without + // resetting either of the underlying allocations, so that we can reuse + // this TypeIds in a loop without paying allocation costs every time. + // + // According to the C++ standard, clear does not affect the result of capacity, + // so we should be able to guarantee that the underlying data is not reallocted. + // https://en.cppreference.com/w/cpp/container/vector/clear.html + order.clear(); + // `DenseHashTable` exposes a "threshold" for clearing the underlying data. + // We just pass in max `size_t` here to ensure that the underlying data is + // never cleared. + types.clear(std::numeric_limits::max()); + hash = 0; +} + TypeId TypeIds::front() const { return order.at(0); diff --git a/Analysis/src/TypeInfer.cpp b/Analysis/src/TypeInfer.cpp index 5e56abad..fb99401c 100644 --- a/Analysis/src/TypeInfer.cpp +++ b/Analysis/src/TypeInfer.cpp @@ -32,8 +32,6 @@ LUAU_FASTFLAG(LuauKnowsTheDataModel3) LUAU_FASTFLAGVARIABLE(DebugLuauFreezeDuringUnification) LUAU_FASTFLAG(LuauInstantiateInSubtyping) LUAU_FASTFLAG(LuauUseWorkspacePropToChooseSolver) -LUAU_FASTFLAG(LuauParametrizedAttributeSyntax) -LUAU_FASTFLAG(LuauNameConstraintRestrictRecursiveTypes) namespace Luau { @@ -1853,14 +1851,11 @@ ControlFlow TypeChecker::check(const ScopePtr& scope, const AstStatDeclareFuncti for (const auto& el : global.paramNames) ftv->argNames.push_back(FunctionArgument{el.first.value, el.second}); - if (FFlag::LuauParametrizedAttributeSyntax) + AstAttr* deprecatedAttr = global.getAttribute(AstAttr::Type::Deprecated); + ftv->isDeprecatedFunction = deprecatedAttr != nullptr; + if (deprecatedAttr) { - AstAttr* deprecatedAttr = global.getAttribute(AstAttr::Type::Deprecated); - ftv->isDeprecatedFunction = deprecatedAttr != nullptr; - if (deprecatedAttr) - { - ftv->deprecatedInfo = std::make_shared(deprecatedAttr->deprecatedInfo()); - } + ftv->deprecatedInfo = std::make_shared(deprecatedAttr->deprecatedInfo()); } Name fnName(global.name.value); @@ -2011,7 +2006,7 @@ WithPredicate TypeChecker::checkExpr(const ScopePtr& scope, const AstExp { return {pack->head.empty() ? nilType : pack->head[0], std::move(result.predicates)}; } - else if (const FreeTypePack* ftp = get(retPack)) + else if (get(retPack)) { TypeId head = freshType(scope->level); TypePackId pack = addTypePack(TypePackVar{TypePack{{head}, freshTypePack(scope->level)}}); @@ -3501,7 +3496,7 @@ TypeId TypeChecker::checkLValueBinding(const ScopePtr& scope, const AstExprIndex } } - if (const ExternType* exprExternType = get(exprType)) + if (get(exprType)) { if (isNonstrictMode()) return unknownType; @@ -3941,14 +3936,11 @@ std::pair TypeChecker::checkFunctionSignature( for (AstLocal* local : expr.args) ftv->argNames.push_back(FunctionArgument{local->name.value, local->location}); - if (FFlag::LuauParametrizedAttributeSyntax) + AstAttr* deprecatedAttr = expr.getAttribute(AstAttr::Type::Deprecated); + ftv->isDeprecatedFunction = deprecatedAttr != nullptr; + if (deprecatedAttr) { - AstAttr* deprecatedAttr = expr.getAttribute(AstAttr::Type::Deprecated); - ftv->isDeprecatedFunction = deprecatedAttr != nullptr; - if (deprecatedAttr) - { - ftv->deprecatedInfo = std::make_shared(deprecatedAttr->deprecatedInfo()); - } + ftv->deprecatedInfo = std::make_shared(deprecatedAttr->deprecatedInfo()); } return std::make_pair(funTy, funScope); @@ -4541,7 +4533,7 @@ std::unique_ptr> TypeChecker::checkCallOverload( if (get(fn)) return std::make_unique>(uninhabitableTypePack); - if (auto ftv = get(fn)) + if (get(fn)) { // fn is one of the overloads of actualFunctionType, which // has been instantiated, so is a monotype. We can therefore @@ -5795,14 +5787,11 @@ TypeId TypeChecker::resolveTypeWorker(const ScopePtr& scope, const AstType& anno ftv->argNames.push_back(std::nullopt); } - if (FFlag::LuauParametrizedAttributeSyntax) + AstAttr* deprecatedAttr = func->getAttribute(AstAttr::Type::Deprecated); + ftv->isDeprecatedFunction = deprecatedAttr != nullptr; + if (deprecatedAttr) { - AstAttr* deprecatedAttr = func->getAttribute(AstAttr::Type::Deprecated); - ftv->isDeprecatedFunction = deprecatedAttr != nullptr; - if (deprecatedAttr) - { - ftv->deprecatedInfo = std::make_shared(deprecatedAttr->deprecatedInfo()); - } + ftv->deprecatedInfo = std::make_shared(deprecatedAttr->deprecatedInfo()); } return fnType; @@ -5950,10 +5939,8 @@ TypeId TypeChecker::instantiateTypeFun( } if (applyTypeFunction.encounteredForwardedType) { - if (FFlag::LuauNameConstraintRestrictRecursiveTypes) - reportError(TypeError{location, RecursiveRestraintViolation{}}); - else - reportError(TypeError{location, GenericError{"Recursive type being used with different parameters"}}); + reportError(TypeError{location, RecursiveRestraintViolation{}}); + return errorRecoveryType(scope); } diff --git a/Analysis/src/TypePack.cpp b/Analysis/src/TypePack.cpp index 6ebeda69..d085887c 100644 --- a/Analysis/src/TypePack.cpp +++ b/Analysis/src/TypePack.cpp @@ -5,9 +5,6 @@ #include "Luau/TxnLog.h" #include "Luau/TypeArena.h" -LUAU_FASTFLAG(LuauReturnMappedGenericPacksFromSubtyping2) -LUAU_FASTFLAG(LuauSubtypingGenericPacksDoesntUseVariance) - namespace Luau { @@ -455,41 +452,6 @@ std::pair, std::optional> flatten(TypePackId tp, return {flattened, tail}; } -std::pair, std::optional> flatten_DEPRECATED( - TypePackId tp, - const DenseHashMap& mappedGenericPacks -) -{ - LUAU_ASSERT(FFlag::LuauReturnMappedGenericPacksFromSubtyping2); - LUAU_ASSERT(!FFlag::LuauSubtypingGenericPacksDoesntUseVariance); - - tp = mappedGenericPacks.contains(tp) ? *mappedGenericPacks.find(tp) : tp; - - std::vector flattened; - std::optional tail = std::nullopt; - DenseHashSet seenGenericPacks{nullptr}; - - while (tp) - { - TypePackIterator it(tp); - - for (; it != end(tp); ++it) - flattened.push_back(*it); - - if (const auto tpTail = it.tail(); tpTail && !seenGenericPacks.contains(*tpTail) && mappedGenericPacks.contains(*tpTail)) - { - tp = *mappedGenericPacks.find(*tpTail); - seenGenericPacks.insert(*tpTail); - continue; - } - - tail = it.tail(); - break; - } - - return {flattened, tail}; -} - bool isVariadic(TypePackId tp) { return isVariadic(tp, *TxnLog::empty()); @@ -547,14 +509,12 @@ LUAU_NOINLINE Unifiable::Bound* emplaceTypePack(TypeP TypePackId sliceTypePack( const size_t sliceIndex, const TypePackId toBeSliced, - std::vector& head, + const std::vector& head, const std::optional tail, const NotNull builtinTypes, const NotNull arena ) { - LUAU_ASSERT(FFlag::LuauSubtypingGenericPacksDoesntUseVariance); - if (sliceIndex == 0) return toBeSliced; else if (sliceIndex == head.size()) diff --git a/Analysis/src/TypePath.cpp b/Analysis/src/TypePath.cpp index 06b45f5d..4241e262 100644 --- a/Analysis/src/TypePath.cpp +++ b/Analysis/src/TypePath.cpp @@ -5,6 +5,7 @@ #include "Luau/Anyification.h" #include "Luau/Common.h" #include "Luau/DenseHash.h" +#include "Luau/ToString.h" #include "Luau/Type.h" #include "Luau/TypeArena.h" #include "Luau/TypeFwd.h" @@ -16,9 +17,9 @@ #include LUAU_FASTFLAG(LuauSolverV2); -LUAU_FASTFLAG(LuauReturnMappedGenericPacksFromSubtyping2) LUAU_FASTFLAG(LuauEmplaceNotPushBack) -LUAU_FASTFLAG(LuauSubtypingGenericPacksDoesntUseVariance) +LUAU_FASTFLAGVARIABLE(LuauConsiderErrorSuppressionInTypes) +LUAU_FASTFLAG(LuauNewNonStrictBetterCheckedFunctionErrorMessage) // Maximum number of steps to follow when traversing a path. May not always // equate to the number of components in a path, depending on the traversal @@ -68,7 +69,6 @@ bool Reduction::operator==(const Reduction& other) const bool GenericPackMapping::operator==(const GenericPackMapping& other) const { - LUAU_ASSERT(FFlag::LuauSubtypingGenericPacksDoesntUseVariance); return mappedType == other.mappedType; } @@ -156,7 +156,6 @@ size_t PathHash::operator()(const Reduction& reduction) const size_t PathHash::operator()(const GenericPackMapping& mapping) const { - LUAU_ASSERT(FFlag::LuauSubtypingGenericPacksDoesntUseVariance); return std::hash()(mapping.mappedType); } @@ -314,7 +313,6 @@ PathBuilder& PathBuilder::packSlice(size_t start_index) PathBuilder& PathBuilder::mappedGenericPack(TypePackId mappedType) { - LUAU_ASSERT(FFlag::LuauSubtypingGenericPacksDoesntUseVariance); components.emplace_back(GenericPackMapping{mappedType}); return *this; } @@ -326,28 +324,6 @@ namespace struct TraversalState { - // Clip the below two constructors with LuauSubtypingGenericPacksDoesntUseVariance - TraversalState(TypeId root, NotNull builtinTypes, const DenseHashMap* mappedGenericPacks, TypeArena* arena) - : current(root) - , builtinTypes(builtinTypes) - , mappedGenericPacks_DEPRECATED(mappedGenericPacks) - , arena(arena) - { - } - - TraversalState( - TypePackId root, - NotNull builtinTypes, - const DenseHashMap* mappedGenericPacks, - TypeArena* arena - ) - : current(root) - , builtinTypes(builtinTypes) - , mappedGenericPacks_DEPRECATED(mappedGenericPacks) - , arena(arena) - { - } - TraversalState(TypeId root, const NotNull builtinTypes, TypeArena* arena) : current(root) , builtinTypes(builtinTypes) @@ -364,11 +340,9 @@ struct TraversalState TypeOrPack current; NotNull builtinTypes; - // TODO: Clip with LuauSubtypingGenericPacksDoesntUseVariance - const DenseHashMap* mappedGenericPacks_DEPRECATED = nullptr; - // TODO: make NotNull when LuauReturnMappedGenericPacksFromSubtyping2 is clipped - TypeArena* arena = nullptr; + NotNull arena; int steps = 0; + bool encounteredErrorSuppression = false; void updateCurrent(TypeId ty) { @@ -472,68 +446,93 @@ struct TraversalState if (auto currentType = get(current)) { + bool updatedCurrent = false; + + if (FFlag::LuauConsiderErrorSuppressionInTypes && get(*currentType)) + { + encounteredErrorSuppression = true; + return false; + } + if (auto u = get(*currentType)) { auto it = begin(u); - std::advance(it, index.index); - if (it != end(u)) + if (FFlag::LuauConsiderErrorSuppressionInTypes) { - updateCurrent(*it); - return true; + // 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)) + { + updateCurrent(*it); + return true; + } } } else if (auto i = get(*currentType)) { auto it = begin(i); - std::advance(it, index.index); - if (it != end(i)) + if (FFlag::LuauConsiderErrorSuppressionInTypes) { - updateCurrent(*it); - return true; + // 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 - { - auto currentPack = get(current); - LUAU_ASSERT(currentPack); - if (FFlag::LuauReturnMappedGenericPacksFromSubtyping2 && !FFlag::LuauSubtypingGenericPacksDoesntUseVariance) - { - if (const auto tp = get(*currentPack)) + else { - auto it = begin(*currentPack); - - size_t i = 0; - for (; i < index.index && it != end(*currentPack); ++i) - ++it; + std::advance(it, index.index); - if (it != end(*currentPack)) + if (it != end(i)) { updateCurrent(*it); return true; } - else if (tp->tail && mappedGenericPacks_DEPRECATED && mappedGenericPacks_DEPRECATED->contains(*tp->tail)) - { - updateCurrent(*mappedGenericPacks_DEPRECATED->find(*tp->tail)); - LUAU_ASSERT(index.index >= i); - return traverse(TypePath::Index{index.index - i, TypePath::Index::Variant::Pack}); - } } } - else + + if (FFlag::LuauConsiderErrorSuppressionInTypes) + return updatedCurrent; + } + else + { + auto currentPack = get(current); + LUAU_ASSERT(currentPack); + if (get(*currentPack)) { - if (get(*currentPack)) - { - auto it = begin(*currentPack); + auto it = begin(*currentPack); - for (size_t i = 0; i < index.index && it != end(*currentPack); ++i) - ++it; + for (size_t i = 0; i < index.index && it != end(*currentPack); ++i) + ++it; - if (it != end(*currentPack)) - { - updateCurrent(*it); - return true; - } + if (it != end(*currentPack)) + { + updateCurrent(*it); + return true; } } } @@ -656,12 +655,7 @@ struct TraversalState if (auto tail = it.tail()) { - if (FFlag::LuauReturnMappedGenericPacksFromSubtyping2 && !FFlag::LuauSubtypingGenericPacksDoesntUseVariance && - mappedGenericPacks_DEPRECATED && mappedGenericPacks_DEPRECATED->contains(*tail)) - updateCurrent(*mappedGenericPacks_DEPRECATED->find(*tail)); - - else - updateCurrent(*tail); + updateCurrent(*tail); return true; } } @@ -674,37 +668,14 @@ struct TraversalState bool traverse(const TypePath::PackSlice slice) { - // TODO: clip these checks once LuauReturnMappedGenericPacksFromSubtyping2 is clipped - // arena and mappedGenericPacks_DEPRECATED should be NonNull once that happens - LUAU_ASSERT(FFlag::LuauReturnMappedGenericPacksFromSubtyping2); - if (FFlag::LuauSubtypingGenericPacksDoesntUseVariance) - { - LUAU_ASSERT(arena); - - if (!arena) - return false; - } - if (checkInvariants()) return false; - // TODO: clip this check once LuauReturnMappedGenericPacksFromSubtyping2 is clipped - // arena and mappedGenericPacks should be NonNull once that happens - if (!FFlag::LuauSubtypingGenericPacksDoesntUseVariance) - { - if (FFlag::LuauReturnMappedGenericPacksFromSubtyping2) - LUAU_ASSERT(arena && mappedGenericPacks_DEPRECATED); - else if (!arena || !mappedGenericPacks_DEPRECATED) - return false; - } - const TypePackId* currentPack = get(current); if (!currentPack) return false; - auto [flatHead, flatTail] = FFlag::LuauSubtypingGenericPacksDoesntUseVariance - ? flatten(*currentPack) - : flatten_DEPRECATED(*currentPack, *mappedGenericPacks_DEPRECATED); + auto [flatHead, flatTail] = flatten(*currentPack); if (flatHead.size() <= slice.start_index) return false; @@ -731,8 +702,6 @@ struct TraversalState bool traverse(const TypePath::GenericPackMapping mapping) { - LUAU_ASSERT(FFlag::LuauSubtypingGenericPacksDoesntUseVariance); - if (checkInvariants()) return false; @@ -832,10 +801,7 @@ std::string toString(const TypePath::Path& path, bool prefixDot) result << "~~>"; } else if constexpr (std::is_same_v) - { - LUAU_ASSERT(FFlag::LuauSubtypingGenericPacksDoesntUseVariance); result << "~"; - } else { static_assert(always_false_v, "Unhandled Component variant"); @@ -897,29 +863,43 @@ std::string toStringHuman(const TypePath::Path& path) } else if constexpr (std::is_same_v) { - size_t humanIndex = c.index + 1; - - if (state == State::Initial && !last) - result << "in" << ' '; - else if (state == State::PendingIs) - result << ' ' << "has" << ' '; - else if (state == State::Property) - result << '`' << ' ' << "has" << ' '; + if (FFlag::LuauNewNonStrictBetterCheckedFunctionErrorMessage) + { + if (state == State::Initial && !last) + result << "in" << ' '; + else if (state == State::PendingIs) + result << ' ' << "has" << ' '; + else if (state == State::Property) + result << '`' << ' ' << "has" << ' '; - result << "the " << humanIndex; - switch (humanIndex) + result << "the " << toHumanReadableIndex(c.index); + } + else { - case 1: - result << "st"; - break; - case 2: - result << "nd"; - break; - case 3: - result << "rd"; - break; - default: - result << "th"; + size_t humanIndex = c.index + 1; + + if (state == State::Initial && !last) + result << "in" << ' '; + else if (state == State::PendingIs) + result << ' ' << "has" << ' '; + else if (state == State::Property) + result << '`' << ' ' << "has" << ' '; + + result << "the " << humanIndex; + switch (humanIndex) + { + case 1: + result << "st"; + break; + case 2: + result << "nd"; + break; + case 3: + result << "rd"; + break; + default: + result << "th"; + } } switch (c.variant) @@ -1053,10 +1033,7 @@ std::string toStringHuman(const TypePath::Path& path) state = State::Normal; } else if constexpr (std::is_same_v) - { - LUAU_ASSERT(FFlag::LuauSubtypingGenericPacksDoesntUseVariance); result << "is a generic pack mapped to "; - } else { static_assert(always_false_v, "Unhandled Component variant"); @@ -1113,32 +1090,8 @@ static bool traverse(TraversalState& state, const Path& path) return true; } -std::optional traverse_DEPRECATED(TypeId root, const Path& path, NotNull builtinTypes) -{ - LUAU_ASSERT(!FFlag::LuauSubtypingGenericPacksDoesntUseVariance); - - TraversalState state(follow(root), builtinTypes, nullptr, nullptr); - if (traverse(state, path)) - return state.current; - else - return std::nullopt; -} - -std::optional traverse_DEPRECATED(TypePackId root, const Path& path, NotNull builtinTypes) -{ - LUAU_ASSERT(!FFlag::LuauSubtypingGenericPacksDoesntUseVariance); - - TraversalState state(follow(root), builtinTypes, nullptr, nullptr); - if (traverse(state, path)) - return state.current; - else - return std::nullopt; -} - std::optional traverse(const TypePackId root, const Path& path, const NotNull builtinTypes, const NotNull arena) { - LUAU_ASSERT(FFlag::LuauSubtypingGenericPacksDoesntUseVariance); - TraversalState state(follow(root), builtinTypes, arena); if (traverse(state, path)) return state.current; @@ -1146,44 +1099,8 @@ std::optional traverse(const TypePackId root, const Path& path, cons return std::nullopt; } -std::optional traverse_DEPRECATED( - TypeId root, - const Path& path, - NotNull builtinTypes, - NotNull> mappedGenericPacks, - NotNull arena -) -{ - LUAU_ASSERT(!FFlag::LuauSubtypingGenericPacksDoesntUseVariance); - - TraversalState state(follow(root), builtinTypes, mappedGenericPacks, arena); - if (traverse(state, path)) - return state.current; - else - return std::nullopt; -} - -std::optional traverse_DEPRECATED( - TypePackId root, - const Path& path, - NotNull builtinTypes, - NotNull> mappedGenericPacks, - NotNull arena -) -{ - LUAU_ASSERT(!FFlag::LuauSubtypingGenericPacksDoesntUseVariance); - - TraversalState state(follow(root), builtinTypes, mappedGenericPacks, arena); - if (traverse(state, path)) - return state.current; - else - return std::nullopt; -} - std::optional traverse(const TypeId root, const Path& path, const NotNull builtinTypes, const NotNull arena) { - LUAU_ASSERT(FFlag::LuauSubtypingGenericPacksDoesntUseVariance); - TraversalState state(follow(root), builtinTypes, arena); if (traverse(state, path)) return state.current; @@ -1191,82 +1108,15 @@ std::optional traverse(const TypeId root, const Path& path, const No return std::nullopt; } -std::optional traverseForType_DEPRECATED(TypeId root, const Path& path, NotNull builtinTypes) -{ - LUAU_ASSERT(!FFlag::LuauSubtypingGenericPacksDoesntUseVariance); - - TraversalState state(follow(root), builtinTypes, nullptr, nullptr); - if (traverse(state, path)) - { - auto ty = get(state.current); - return ty ? std::make_optional(*ty) : std::nullopt; - } - else - return std::nullopt; -} - -std::optional traverseForType_DEPRECATED( - TypeId root, - const Path& path, - NotNull builtinTypes, - NotNull> mappedGenericPacks, - NotNull arena -) -{ - LUAU_ASSERT(!FFlag::LuauSubtypingGenericPacksDoesntUseVariance); - - TraversalState state(follow(root), builtinTypes, mappedGenericPacks, arena); - if (traverse(state, path)) - { - auto ty = get(state.current); - return ty ? std::make_optional(*ty) : std::nullopt; - } - else - return std::nullopt; -} - std::optional traverseForType(const TypeId root, const Path& path, const NotNull builtinTypes, const NotNull arena) { - LUAU_ASSERT(FFlag::LuauSubtypingGenericPacksDoesntUseVariance); - TraversalState state(follow(root), builtinTypes, arena); if (traverse(state, path)) { - const TypeId* ty = get(state.current); - return ty ? std::make_optional(*ty) : std::nullopt; - } - else - return std::nullopt; -} - -std::optional traverseForType_DEPRECATED(TypePackId root, const Path& path, NotNull builtinTypes) -{ - LUAU_ASSERT(!FFlag::LuauSubtypingGenericPacksDoesntUseVariance); - - TraversalState state(follow(root), builtinTypes, nullptr, nullptr); - if (traverse(state, path)) - { - auto ty = get(state.current); - return ty ? std::make_optional(*ty) : std::nullopt; - } - else - return std::nullopt; -} - -std::optional traverseForType_DEPRECATED( - TypePackId root, - const Path& path, - NotNull builtinTypes, - NotNull> mappedGenericPacks, - NotNull arena -) -{ - LUAU_ASSERT(!FFlag::LuauSubtypingGenericPacksDoesntUseVariance); + if (FFlag::LuauConsiderErrorSuppressionInTypes && state.encounteredErrorSuppression) + return builtinTypes->errorType; - TraversalState state(follow(root), builtinTypes, mappedGenericPacks, arena); - if (traverse(state, path)) - { - auto ty = get(state.current); + const TypeId* ty = get(state.current); return ty ? std::make_optional(*ty) : std::nullopt; } else @@ -1280,11 +1130,11 @@ std::optional traverseForType( const NotNull arena ) { - LUAU_ASSERT(FFlag::LuauSubtypingGenericPacksDoesntUseVariance); - TraversalState state(follow(root), builtinTypes, arena); if (traverse(state, path)) { + if (FFlag::LuauConsiderErrorSuppressionInTypes && state.encounteredErrorSuppression) + return builtinTypes->errorType; const TypeId* ty = get(state.current); return ty ? std::make_optional(*ty) : std::nullopt; } @@ -1292,40 +1142,6 @@ std::optional traverseForType( return std::nullopt; } -std::optional traverseForPack_DEPRECATED(TypeId root, const Path& path, NotNull builtinTypes) -{ - LUAU_ASSERT(!FFlag::LuauSubtypingGenericPacksDoesntUseVariance); - - TraversalState state(follow(root), builtinTypes, nullptr, nullptr); - if (traverse(state, path)) - { - auto ty = get(state.current); - return ty ? std::make_optional(*ty) : std::nullopt; - } - else - return std::nullopt; -} - -std::optional traverseForPack_DEPRECATED( - TypeId root, - const Path& path, - NotNull builtinTypes, - NotNull> mappedGenericPacks, - NotNull arena -) -{ - LUAU_ASSERT(!FFlag::LuauSubtypingGenericPacksDoesntUseVariance); - - TraversalState state(follow(root), builtinTypes, mappedGenericPacks, arena); - if (traverse(state, path)) - { - auto ty = get(state.current); - return ty ? std::make_optional(*ty) : std::nullopt; - } - else - return std::nullopt; -} - std::optional traverseForPack( const TypeId root, const Path& path, @@ -1333,11 +1149,11 @@ std::optional traverseForPack( const NotNull arena ) { - LUAU_ASSERT(FFlag::LuauSubtypingGenericPacksDoesntUseVariance); - TraversalState state(follow(root), builtinTypes, arena); if (traverse(state, path)) { + if (FFlag::LuauConsiderErrorSuppressionInTypes && state.encounteredErrorSuppression) + return builtinTypes->errorTypePack; const TypePackId* ty = get(state.current); return ty ? std::make_optional(*ty) : std::nullopt; } @@ -1345,40 +1161,6 @@ std::optional traverseForPack( return std::nullopt; } -std::optional traverseForPack_DEPRECATED(TypePackId root, const Path& path, NotNull builtinTypes) -{ - LUAU_ASSERT(!FFlag::LuauSubtypingGenericPacksDoesntUseVariance); - - TraversalState state(follow(root), builtinTypes, nullptr, nullptr); - if (traverse(state, path)) - { - auto ty = get(state.current); - return ty ? std::make_optional(*ty) : std::nullopt; - } - else - return std::nullopt; -} - -std::optional traverseForPack_DEPRECATED( - TypePackId root, - const Path& path, - NotNull builtinTypes, - NotNull> mappedGenericPacks, - NotNull arena -) -{ - LUAU_ASSERT(!FFlag::LuauSubtypingGenericPacksDoesntUseVariance); - - TraversalState state(follow(root), builtinTypes, mappedGenericPacks, arena); - if (traverse(state, path)) - { - auto ty = get(state.current); - return ty ? std::make_optional(*ty) : std::nullopt; - } - else - return std::nullopt; -} - std::optional traverseForPack( const TypePackId root, const Path& path, @@ -1386,11 +1168,11 @@ std::optional traverseForPack( const NotNull arena ) { - LUAU_ASSERT(FFlag::LuauSubtypingGenericPacksDoesntUseVariance); - TraversalState state(follow(root), builtinTypes, arena); if (traverse(state, path)) { + if (FFlag::LuauConsiderErrorSuppressionInTypes && state.encounteredErrorSuppression) + return builtinTypes->errorTypePack; const TypePackId* ty = get(state.current); return ty ? std::make_optional(*ty) : std::nullopt; } @@ -1427,8 +1209,6 @@ std::optional traverseForIndex(const Path& path) TypePack flattenPackWithPath(TypePackId root, const Path& path) { - LUAU_ASSERT(FFlag::LuauSubtypingGenericPacksDoesntUseVariance); - std::vector flattened; std::optional curr = root; @@ -1465,8 +1245,6 @@ TypePack flattenPackWithPath(TypePackId root, const Path& path) TypePack traverseForFlattenedPack(const TypeId root, const Path& path, const NotNull builtinTypes, const NotNull arena) { - LUAU_ASSERT(FFlag::LuauSubtypingGenericPacksDoesntUseVariance); - // Iterate over path's components, and figure out when it turns into Tails and GenericPackMappings // We want to split out the part of the path that contains the generic pack mappings we're interested in, so that we can flatten it // path[splitIndex:] will contain only Tails and GenericPackMappings diff --git a/Analysis/src/TypeUtils.cpp b/Analysis/src/TypeUtils.cpp index 7946c744..64023c2c 100644 --- a/Analysis/src/TypeUtils.cpp +++ b/Analysis/src/TypeUtils.cpp @@ -15,8 +15,7 @@ LUAU_FASTFLAG(LuauSolverV2) LUAU_FASTFLAGVARIABLE(LuauTidyTypeUtils) LUAU_FASTFLAG(LuauEmplaceNotPushBack) -LUAU_FASTFLAGVARIABLE(LuauVariadicAnyPackShouldBeErrorSuppressing) -LUAU_FASTFLAG(LuauPushTypeConstraint) +LUAU_FASTFLAG(LuauPushTypeConstraint2) namespace Luau { @@ -383,7 +382,7 @@ TypePack extendTypePack( return result; } - else if (auto etp = getMutable(pack)) + else if (getMutable(pack)) { while (result.head.size() < length) result.head.push_back(builtinTypes->errorType); @@ -506,13 +505,10 @@ ErrorSuppression shouldSuppressErrors(NotNull normalizer, TypePackId { // Flatten t where t = ...any will produce a type pack [ {}, t] // which trivially fails the tail check below, which is why we need to special case here - if (FFlag::LuauVariadicAnyPackShouldBeErrorSuppressing) + if (auto tpId = get(follow(tp))) { - if (auto tpId = get(follow(tp))) - { - if (get(follow(tpId->ty))) - return ErrorSuppression::Suppress; - } + if (get(follow(tpId->ty))) + return ErrorSuppression::Suppress; } auto [tys, tail] = flatten(tp); @@ -714,7 +710,7 @@ std::optional extractMatchingTableType(std::vector& tables, Type } } - if (FFlag::LuauPushTypeConstraint && fastIsSubtype(propType, expectedType)) + if (FFlag::LuauPushTypeConstraint2 && fastIsSubtype(propType, expectedType)) return ty; } } @@ -747,6 +743,32 @@ AstExpr* unwrapGroup(AstExpr* expr) return expr; } +bool isOptionalType(TypeId ty, NotNull builtinTypes) +{ + ty = follow(ty); + + if (ty == builtinTypes->nilType || ty == builtinTypes->anyType || ty == builtinTypes->unknownType) + return true; + else if (const PrimitiveType* pt = get(ty)) + return pt->type == PrimitiveType::NilType; + else if (const UnionType* ut = get(ty)) + { + for (TypeId option : ut) + { + option = follow(option); + + if (option == builtinTypes->nilType || option == builtinTypes->anyType || option == builtinTypes->unknownType) + return true; + else if (const PrimitiveType* pt = get(option); pt && pt->type == PrimitiveType::NilType) + return true; + } + + return false; + } + + return false; +} + bool isApproximatelyFalsyType(TypeId ty) { ty = follow(ty); @@ -899,7 +921,35 @@ TypeId addUnion(NotNull arena, NotNull builtinTypes, st return ub.build(); } +ContainsAnyGeneric::ContainsAnyGeneric() + : TypeOnceVisitor("ContainsAnyGeneric", /* skipBoundTypes */ true) +{ +} +bool ContainsAnyGeneric::visit(TypeId ty) +{ + found = found || is(ty); + return !found; +} +bool ContainsAnyGeneric::visit(TypePackId ty) +{ + found = found || is(follow(ty)); + return !found; +} + +bool ContainsAnyGeneric::hasAnyGeneric(TypeId ty) +{ + ContainsAnyGeneric cg; + cg.traverse(ty); + return cg.found; +} + +bool ContainsAnyGeneric::hasAnyGeneric(TypePackId tp) +{ + ContainsAnyGeneric cg; + cg.traverse(tp); + return cg.found; +} } // namespace Luau diff --git a/Analysis/src/Unifier.cpp b/Analysis/src/Unifier.cpp index a8da0ef3..41d85940 100644 --- a/Analysis/src/Unifier.cpp +++ b/Analysis/src/Unifier.cpp @@ -22,6 +22,7 @@ LUAU_FASTFLAGVARIABLE(LuauTransitiveSubtyping) LUAU_FASTFLAG(LuauSolverV2) LUAU_FASTFLAGVARIABLE(LuauFixIndexerSubtypingOrdering) LUAU_FASTFLAGVARIABLE(LuauUnifierRecursionOnRestart) +LUAU_FASTFLAG(LuauNormalizerStepwiseFuel) namespace Luau { @@ -1189,13 +1190,26 @@ TypePackId Unifier::tryApplyOverloadedFunction(TypeId function, const Normalized { innerState->log.clear(); innerState->tryUnify_(*result, ftv->retTypes); - 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; + 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; + } } else result = ftv->retTypes; @@ -1452,7 +1466,7 @@ void Unifier::tryUnify_(TypePackId subTp, TypePackId superTp, bool isFunctionCal else if (isBlocked(log, superTp)) blockedTypePacks.push_back(superTp); - if (auto superFree = log.getMutable(superTp)) + if (log.getMutable(superTp)) { if (!occursCheck(superTp, subTp, /* reversed = */ true)) { @@ -1460,7 +1474,7 @@ void Unifier::tryUnify_(TypePackId subTp, TypePackId superTp, bool isFunctionCal log.replace(superTp, Unifiable::Bound(widen(subTp))); } } - else if (auto subFree = log.getMutable(subTp)) + else if (log.getMutable(subTp)) { if (!occursCheck(subTp, superTp, /* reversed = */ false)) { diff --git a/Analysis/src/Unifier2.cpp b/Analysis/src/Unifier2.cpp index b4b03a7a..a6b5fc1b 100644 --- a/Analysis/src/Unifier2.cpp +++ b/Analysis/src/Unifier2.cpp @@ -20,10 +20,11 @@ LUAU_FASTINT(LuauTypeInferIterationLimit) LUAU_FASTINT(LuauTypeInferRecursionLimit) +LUAU_FASTFLAG(LuauIndividualRecursionLimits) +LUAU_DYNAMIC_FASTINTVARIABLE(LuauUnifierRecursionLimit, 100) + LUAU_FASTFLAG(LuauEmplaceNotPushBack) LUAU_FASTFLAGVARIABLE(LuauLimitUnification) -LUAU_FASTFLAGVARIABLE(LuauUnifyShortcircuitSomeIntersectionsAndUnions) -LUAU_FASTFLAGVARIABLE(LuauTryToOptimizeSetTypeUnification) LUAU_FASTFLAGVARIABLE(LuauFixNilRightPad) namespace Luau @@ -111,7 +112,7 @@ Unifier2::Unifier2(NotNull arena, NotNull builtinTypes, , scope(scope) , ice(ice) , limits(TypeCheckLimits{}) // TODO: typecheck limits in unifier2 - , recursionLimit(FInt::LuauTypeInferRecursionLimit) + , recursionLimit(FFlag::LuauIndividualRecursionLimits ? DFInt::LuauUnifierRecursionLimit : FInt::LuauTypeInferRecursionLimit) , uninhabitedTypeFunctions(nullptr) { } @@ -128,7 +129,7 @@ Unifier2::Unifier2( , scope(scope) , ice(ice) , limits(TypeCheckLimits{}) // TODO: typecheck limits in unifier2 - , recursionLimit(FInt::LuauTypeInferRecursionLimit) + , recursionLimit(FFlag::LuauIndividualRecursionLimits ? DFInt::LuauUnifierRecursionLimit : FInt::LuauTypeInferRecursionLimit) , uninhabitedTypeFunctions(uninhabitedTypeFunctions) { } @@ -211,67 +212,19 @@ UnifyResult Unifier2::unify_(TypeId subTy, TypeId superTy) if (subFn && superFn) return unify_(subTy, superFn); - if (FFlag::LuauTryToOptimizeSetTypeUnification) - { - auto subUnion = get(subTy); - auto superUnion = get(superTy); - - auto subIntersection = get(subTy); - auto superIntersection = get(superTy); - - // This is, effectively, arranged to avoid the following: - // - // 'a & T <: U | V => 'a & T <: U and 'a & T <: V - // - - // For T <: U & V and T | U <: V, these two cases are entirely correct. + auto subUnion = get(subTy); + auto superUnion = get(superTy); + if (subUnion) + return unify_(subUnion, superTy); + else if (superUnion) + return unify_(subTy, superUnion); - // We decompose T <: U & V above into T <: U and T <: V ... - if (superIntersection) - return unify_(subTy, superIntersection); - - // ... and T | U <: V into T <: V and U <: V. - if (subUnion) - return unify_(subUnion, superTy); - - // This, T & U <: V, erroneously is decomposed into T <: U and T <: V, - // even though technically we only need one of the above to hold. - // However, this ordering means that we avoid ... - if (subIntersection) - return unify_(subIntersection, superTy); - - // T <: U | V decomposing into T <: U and T <: V is incorrect, and - // can result in some really strange user-visible bugs. Consider: - // - // 'a & ~(false?) <: string | number - // - // Intuitively, this should place a constraint of `string | number` - // on the upper bound of `'a`. But if we hit this case, then we - // end up with something like: - // - // 'a & ~(false?) <: string and 'a & ~(false?) <: number - // - // ... which will result in `'a` having `string & number` as its - // upper bound, and being inferred to `never`. - if (superUnion) - return unify_(subTy, superUnion); - } - else - { - auto subUnion = get(subTy); - auto superUnion = get(superTy); - if (subUnion) - return unify_(subUnion, superTy); - else if (superUnion) - return unify_(subTy, superUnion); - - auto subIntersection = get(subTy); - auto superIntersection = get(superTy); - if (subIntersection) - return unify_(subIntersection, superTy); - else if (superIntersection) - return unify_(subTy, superIntersection); - } + auto subIntersection = get(subTy); + auto superIntersection = get(superTy); + if (subIntersection) + return unify_(subIntersection, superTy); + else if (superIntersection) + return unify_(subTy, superIntersection); auto subNever = get(subTy); auto superNever = get(superTy); @@ -446,15 +399,12 @@ UnifyResult Unifier2::unify_(const UnionType* subUnion, TypeId superTy) UnifyResult Unifier2::unify_(TypeId subTy, const UnionType* superUnion) { - if (FFlag::LuauUnifyShortcircuitSomeIntersectionsAndUnions) + subTy = follow(subTy); + // T <: T | U1 | U2 | ... | Un is trivially true, so we don't gain any information by unifying + for (const auto superOption : superUnion) { - subTy = follow(subTy); - // T <: T | U1 | U2 | ... | Un is trivially true, so we don't gain any information by unifying - for (const auto superOption : superUnion) - { - if (subTy == superOption) - return UnifyResult::Ok; - } + if (subTy == superOption) + return UnifyResult::Ok; } UnifyResult result = UnifyResult::Ok; @@ -471,15 +421,12 @@ UnifyResult Unifier2::unify_(TypeId subTy, const UnionType* superUnion) UnifyResult Unifier2::unify_(const IntersectionType* subIntersection, TypeId superTy) { - if (FFlag::LuauUnifyShortcircuitSomeIntersectionsAndUnions) + superTy = follow(superTy); + // T & I1 & I2 & ... & In <: T is trivially true, so we don't gain any information by unifying + for (const auto subOption : subIntersection) { - superTy = follow(superTy); - // T & I1 & I2 & ... & In <: T is trivially true, so we don't gain any information by unifying - for (const auto subOption : subIntersection) - { - if (superTy == subOption) - return UnifyResult::Ok; - } + if (superTy == subOption) + return UnifyResult::Ok; } UnifyResult result = UnifyResult::Ok; diff --git a/Ast/include/Luau/Cst.h b/Ast/include/Luau/Cst.h index b9dcb7fd..223c3ddd 100644 --- a/Ast/include/Luau/Cst.h +++ b/Ast/include/Luau/Cst.h @@ -1,6 +1,7 @@ // This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details #pragma once +#include "Luau/Ast.h" #include "Luau/Location.h" #include diff --git a/Ast/include/Luau/Parser.h b/Ast/include/Luau/Parser.h index 836708e5..0a31f732 100644 --- a/Ast/include/Luau/Parser.h +++ b/Ast/include/Luau/Parser.h @@ -136,7 +136,6 @@ class Parser const TempVector& attributes, const AstArray& args ); - std::optional validateAttribute_DEPRECATED(const char* attributeName, const TempVector& attributes); // attribute ::= '@' NAME void parseAttribute(TempVector& attribute); diff --git a/Analysis/include/Luau/Transpiler.h b/Ast/include/Luau/PrettyPrinter.h similarity index 63% rename from Analysis/include/Luau/Transpiler.h rename to Ast/include/Luau/PrettyPrinter.h index 586fd92e..6d69bb59 100644 --- a/Analysis/include/Luau/Transpiler.h +++ b/Ast/include/Luau/PrettyPrinter.h @@ -12,7 +12,7 @@ namespace Luau class AstNode; class AstStatBlock; -struct TranspileResult +struct PrettyPrintResult { std::string code; Location errorLocation; @@ -23,11 +23,11 @@ std::string toString(AstNode* node); void dump(AstNode* node); // Never fails on a well-formed AST -std::string transpile(AstStatBlock& ast); -std::string transpileWithTypes(AstStatBlock& block); -std::string transpileWithTypes(AstStatBlock& block, const CstNodeMap& cstNodeMap); +std::string prettyPrint(AstStatBlock& ast); +std::string prettyPrintWithTypes(AstStatBlock& block); +std::string prettyPrintWithTypes(AstStatBlock& block, const CstNodeMap& cstNodeMap); // Only fails when parsing fails -TranspileResult transpile(std::string_view source, ParseOptions options = ParseOptions{}, bool withTypes = false); +PrettyPrintResult prettyPrint(std::string_view source, ParseOptions options = ParseOptions{}, bool withTypes = false); } // namespace Luau diff --git a/Ast/src/Ast.cpp b/Ast/src/Ast.cpp index aecd7171..fc5c4a33 100644 --- a/Ast/src/Ast.cpp +++ b/Ast/src/Ast.cpp @@ -4,8 +4,6 @@ #include "Luau/Common.h" #include "Luau/StringUtils.h" -LUAU_FASTFLAG(LuauParametrizedAttributeSyntax) - namespace Luau { @@ -22,20 +20,7 @@ static AstAttr* findAttributeInArray(const AstArray attributes, AstAtt static bool hasAttributeInArray(const AstArray attributes, AstAttr::Type attributeType) { - if (FFlag::LuauParametrizedAttributeSyntax) - { - return findAttributeInArray(attributes, attributeType) != nullptr; - } - else - { - for (const auto attribute : attributes) - { - if (attribute->type == attributeType) - return true; - } - - return false; - } + return findAttributeInArray(attributes, attributeType) != nullptr; } static void visitTypeList(AstVisitor* visitor, const AstTypeList& list) diff --git a/Ast/src/Lexer.cpp b/Ast/src/Lexer.cpp index 12e35343..dc31a292 100644 --- a/Ast/src/Lexer.cpp +++ b/Ast/src/Lexer.cpp @@ -8,8 +8,6 @@ #include -LUAU_FASTFLAG(LuauParametrizedAttributeSyntax) - namespace Luau { @@ -991,36 +989,28 @@ Lexeme Lexer::readNext() } case '@': { - if (FFlag::LuauParametrizedAttributeSyntax) + if (peekch(1) == '[') { - if (peekch(1) == '[') - { - consume(); - consume(); + consume(); + consume(); + + return Lexeme(Location(start, 2), Lexeme::AttributeOpen); + } + else + { + // consume @ first + consume(); - return Lexeme(Location(start, 2), Lexeme::AttributeOpen); + if (isAlpha(peekch()) || peekch() == '_') + { + std::pair attribute = readName(); + return Lexeme(Location(start, position()), Lexeme::Attribute, attribute.first.value); } else { - // consume @ first - consume(); - - if (isAlpha(peekch()) || peekch() == '_') - { - std::pair attribute = readName(); - return Lexeme(Location(start, position()), Lexeme::Attribute, attribute.first.value); - } - else - { - return Lexeme(Location(start, position()), Lexeme::Attribute, ""); - } + return Lexeme(Location(start, position()), Lexeme::Attribute, ""); } } - else - { - std::pair attribute = readName(); - return Lexeme(Location(start, position()), Lexeme::Attribute, attribute.first.value); - } } default: if (isDigit(peekch())) diff --git a/Ast/src/Parser.cpp b/Ast/src/Parser.cpp index d8ee89b7..cb92bc2a 100644 --- a/Ast/src/Parser.cpp +++ b/Ast/src/Parser.cpp @@ -19,8 +19,6 @@ LUAU_FASTINTVARIABLE(LuauParseErrorLimit, 100) // See docs/SyntaxChanges.md for an explanation. LUAU_FASTFLAGVARIABLE(LuauSolverV2) LUAU_DYNAMIC_FASTFLAGVARIABLE(DebugLuauReportReturnTypeVariadicWithTypeSuffix, false) -LUAU_FASTFLAGVARIABLE(LuauParseIncompleteInterpStringsWithLocation) -LUAU_FASTFLAGVARIABLE(LuauParametrizedAttributeSyntax) LUAU_FASTFLAGVARIABLE(DebugLuauStringSingletonBasedOnQuotes) LUAU_FASTFLAGVARIABLE(LuauAutocompleteAttributes) @@ -869,60 +867,10 @@ std::optional Parser::validateAttribute( return type; } -std::optional Parser::validateAttribute_DEPRECATED(const char* attributeName, const TempVector& attributes) -{ - // check if the attribute name is valid - std::optional type; - - for (int i = 0; kAttributeEntries_DEPRECATED[i].name; ++i) - { - if (strcmp(attributeName, kAttributeEntries_DEPRECATED[i].name) == 0) - { - type = kAttributeEntries_DEPRECATED[i].type; - break; - } - } - - if (!type) - { - if (strlen(attributeName) == 1) - report(lexer.current().location, "Attribute name is missing"); - else - report(lexer.current().location, "Invalid attribute '%s'", attributeName); - } - else - { - // check that attribute is not duplicated - for (const AstAttr* attr : attributes) - { - if (attr->type == *type) - report(lexer.current().location, "Cannot duplicate attribute '%s'", attributeName); - } - } - - return type; -} - // attribute ::= '@' NAME void Parser::parseAttribute(TempVector& attributes) { AstArray empty; - if (!FFlag::LuauParametrizedAttributeSyntax) - { - LUAU_ASSERT(lexer.current().type == Lexeme::Type::Attribute); - - Location loc = lexer.current().location; - - const char* name = lexer.current().name; - std::optional type = validateAttribute_DEPRECATED(name, attributes); - - nextLexeme(); - - if (type) - attributes.push_back(allocator.alloc(loc, *type, empty)); - - return; - } LUAU_ASSERT(lexer.current().type == Lexeme::Type::Attribute || lexer.current().type == Lexeme::Type::AttributeOpen); @@ -1035,7 +983,7 @@ AstArray Parser::parseAttributes() TempVector attributes(scratchAttr); - while (lexer.current().type == Lexeme::Attribute || (FFlag::LuauParametrizedAttributeSyntax && lexer.current().type == Lexeme::AttributeOpen)) + while (lexer.current().type == Lexeme::Attribute || lexer.current().type == Lexeme::AttributeOpen) parseAttribute(attributes); return copy(attributes); @@ -1443,8 +1391,7 @@ AstStat* Parser::parseDeclaration(const Location& start, const AstArray attributes{nullptr, 0}; - if (lexer.current().type == Lexeme::Attribute || - (FFlag::LuauParametrizedAttributeSyntax && lexer.current().type == Lexeme::AttributeOpen)) + if (lexer.current().type == Lexeme::Attribute || lexer.current().type == Lexeme::AttributeOpen) { attributes = Parser::parseAttributes(); @@ -2567,7 +2514,7 @@ AstTypeOrPack Parser::parseSimpleType(bool allowPack, bool inDeclarationContext) AstArray attributes{nullptr, 0}; - if (lexer.current().type == Lexeme::Attribute || (FFlag::LuauParametrizedAttributeSyntax && lexer.current().type == Lexeme::AttributeOpen)) + if (lexer.current().type == Lexeme::Attribute || lexer.current().type == Lexeme::AttributeOpen) { if (!inDeclarationContext) { @@ -3214,7 +3161,7 @@ AstExpr* Parser::parseSimpleExpr() AstArray attributes{nullptr, 0}; - if (lexer.current().type == Lexeme::Attribute || (FFlag::LuauParametrizedAttributeSyntax && lexer.current().type == Lexeme::AttributeOpen)) + if (lexer.current().type == Lexeme::Attribute || lexer.current().type == Lexeme::AttributeOpen) { attributes = parseAttributes(); @@ -4023,33 +3970,27 @@ AstExpr* Parser::parseInterpString() return reportExprError(endLocation, {}, "Double braces are not permitted within interpolated strings; did you mean '\\{'?"); case Lexeme::BrokenString: nextLexeme(); - if (!FFlag::LuauParseIncompleteInterpStringsWithLocation) - return reportExprError(endLocation, {}, "Malformed interpolated string; did you forget to add a '}'?"); LUAU_FALLTHROUGH; case Lexeme::Eof: { - if (FFlag::LuauParseIncompleteInterpStringsWithLocation) + AstArray> stringsArray = copy(strings); + AstArray exprs = copy(expressions); + AstExprInterpString* node = + allocator.alloc(Location{startLocation, lexer.previousLocation()}, stringsArray, exprs); + if (options.storeCstData) + cstNodeMap[node] = allocator.alloc(copy(sourceStrings), copy(stringPositions)); + if (auto top = lexer.peekBraceStackTop()) { - AstArray> stringsArray = copy(strings); - AstArray exprs = copy(expressions); - AstExprInterpString* node = - allocator.alloc(Location{startLocation, lexer.previousLocation()}, stringsArray, exprs); - if (options.storeCstData) - cstNodeMap[node] = allocator.alloc(copy(sourceStrings), copy(stringPositions)); - if (auto top = lexer.peekBraceStackTop()) - { - // We are in a broken interpolated string, the top of the stack is non empty, we are missing '}' - if (*top == Lexer::BraceType::InterpolatedString) - report(lexer.previousLocation(), "Malformed interpolated string; did you forget to add a '}'?"); - } - else - { - // We are in a broken interpolated string, the top of the stack is empty, we are missing '`'. - report(lexer.previousLocation(), "Malformed interpolated string; did you forget to add a '`'?"); - } - return node; + // We are in a broken interpolated string, the top of the stack is non empty, we are missing '}' + if (*top == Lexer::BraceType::InterpolatedString) + report(lexer.previousLocation(), "Malformed interpolated string; did you forget to add a '}'?"); } - LUAU_FALLTHROUGH; + else + { + // We are in a broken interpolated string, the top of the stack is empty, we are missing '`'. + report(lexer.previousLocation(), "Malformed interpolated string; did you forget to add a '`'?"); + } + return node; } default: return reportExprError(endLocation, {}, "Malformed interpolated string, got %s", lexer.current().toString().c_str()); diff --git a/Analysis/src/Transpiler.cpp b/Ast/src/PrettyPrinter.cpp similarity index 98% rename from Analysis/src/Transpiler.cpp rename to Ast/src/PrettyPrinter.cpp index 17ac51dd..7e89f201 100644 --- a/Analysis/src/Transpiler.cpp +++ b/Ast/src/PrettyPrinter.cpp @@ -1,5 +1,5 @@ // This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details -#include "Luau/Transpiler.h" +#include "Luau/PrettyPrinter.h" #include "Luau/Parser.h" #include "Luau/StringUtils.h" @@ -1865,14 +1865,14 @@ void dump(AstNode* node) printf("%s\n", toString(node).c_str()); } -std::string transpile(AstStatBlock& block, const CstNodeMap& cstNodeMap) +std::string prettyPrint(AstStatBlock& block, const CstNodeMap& cstNodeMap) { StringWriter writer; Printer(writer, cstNodeMap).visualizeBlock(block); return writer.str(); } -std::string transpileWithTypes(AstStatBlock& block, const CstNodeMap& cstNodeMap) +std::string prettyPrintWithTypes(AstStatBlock& block, const CstNodeMap& cstNodeMap) { StringWriter writer; Printer printer(writer, cstNodeMap); @@ -1881,12 +1881,12 @@ std::string transpileWithTypes(AstStatBlock& block, const CstNodeMap& cstNodeMap return writer.str(); } -std::string transpileWithTypes(AstStatBlock& block) +std::string prettyPrintWithTypes(AstStatBlock& block) { - return transpileWithTypes(block, CstNodeMap{nullptr}); + return prettyPrintWithTypes(block, CstNodeMap{nullptr}); } -TranspileResult transpile(std::string_view source, ParseOptions options, bool withTypes) +PrettyPrintResult prettyPrint(std::string_view source, ParseOptions options, bool withTypes) { options.storeCstData = true; @@ -1896,20 +1896,20 @@ TranspileResult transpile(std::string_view source, ParseOptions options, bool wi if (!parseResult.errors.empty()) { - // TranspileResult keeps track of only a single error + // PrettyPrintResult keeps track of only a single error const ParseError& error = parseResult.errors.front(); - return TranspileResult{"", error.getLocation(), error.what()}; + return PrettyPrintResult{"", error.getLocation(), error.what()}; } LUAU_ASSERT(parseResult.root); if (!parseResult.root) - return TranspileResult{"", {}, "Internal error: Parser yielded empty parse tree"}; + return PrettyPrintResult{"", {}, "Internal error: Parser yielded empty parse tree"}; if (withTypes) - return TranspileResult{transpileWithTypes(*parseResult.root, parseResult.cstNodeMap)}; + return PrettyPrintResult{prettyPrintWithTypes(*parseResult.root, parseResult.cstNodeMap)}; - return TranspileResult{transpile(*parseResult.root, parseResult.cstNodeMap)}; + return PrettyPrintResult{prettyPrint(*parseResult.root, parseResult.cstNodeMap)}; } } // namespace Luau diff --git a/CLI/include/Luau/AnalyzeRequirer.h b/CLI/include/Luau/AnalyzeRequirer.h index 0e1d5ee8..f9d6ed53 100644 --- a/CLI/include/Luau/AnalyzeRequirer.h +++ b/CLI/include/Luau/AnalyzeRequirer.h @@ -7,6 +7,7 @@ struct FileNavigationContext : Luau::Require::NavigationContext { using NavigateResult = Luau::Require::NavigationContext::NavigateResult; + using ConfigStatus = Luau::Require::NavigationContext::ConfigStatus; FileNavigationContext(std::string requirerPath); @@ -19,7 +20,7 @@ struct FileNavigationContext : Luau::Require::NavigationContext NavigateResult toParent() override; NavigateResult toChild(const std::string& component) override; - bool isConfigPresent() const override; + ConfigStatus getConfigStatus() const override; virtual ConfigBehavior getConfigBehavior() const override; virtual std::optional getAlias(const std::string& alias) const override; virtual std::optional getConfig() const override; diff --git a/CLI/include/Luau/VfsNavigator.h b/CLI/include/Luau/VfsNavigator.h index b781b13d..91988d19 100644 --- a/CLI/include/Luau/VfsNavigator.h +++ b/CLI/include/Luau/VfsNavigator.h @@ -1,6 +1,7 @@ // This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details #pragma once +#include #include enum class NavigationStatus @@ -21,9 +22,21 @@ class VfsNavigator std::string getFilePath() const; std::string getAbsoluteFilePath() const; - std::string getLuaurcPath() const; + + enum class ConfigStatus + { + Absent, + Ambiguous, + PresentJson, + PresentLuau + }; + + ConfigStatus getConfigStatus() const; + std::optional getConfig() const; private: + std::string getConfigPath(const std::string& filename) const; + NavigationStatus updateRealPaths(); std::string realPath; diff --git a/CLI/src/Analyze.cpp b/CLI/src/Analyze.cpp index 1a2cafb4..0a344248 100644 --- a/CLI/src/Analyze.cpp +++ b/CLI/src/Analyze.cpp @@ -1,11 +1,13 @@ // This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details -#include "Luau/Config.h" -#include "Luau/ModuleResolver.h" -#include "Luau/TypeInfer.h" #include "Luau/BuiltinDefinitions.h" +#include "Luau/Config.h" #include "Luau/Frontend.h" +#include "Luau/LuauConfig.h" +#include "Luau/ModuleResolver.h" +#include "Luau/PrettyPrinter.h" +#include "Luau/StringUtils.h" #include "Luau/TypeAttach.h" -#include "Luau/Transpiler.h" +#include "Luau/TypeInfer.h" #include "Luau/AnalyzeRequirer.h" #include "Luau/FileUtils.h" @@ -113,7 +115,7 @@ static bool reportModuleResult(Luau::Frontend& frontend, const Luau::ModuleName& Luau::attachTypeData(*sm, *m); - std::string annotated = Luau::transpileWithTypes(*sm->root); + std::string annotated = Luau::prettyPrintWithTypes(*sm->root); printf("%s", annotated.c_str()); } @@ -174,6 +176,7 @@ struct CliFileResolver : Luau::FileResolver { std::string path{expr->value.data, expr->value.size}; + // TODO (CLI-174536): support interrupt callbacks based on TypeCheckLimits FileNavigationContext navigationContext{context->name}; Luau::Require::ErrorHandler nullErrorHandler{}; @@ -229,20 +232,48 @@ struct CliConfigResolver : Luau::ConfigResolver std::optional parent = getParentPath(path); Luau::Config result = parent ? readConfigRec(*parent) : defaultConfig; - std::string configPath = joinPaths(path, Luau::kConfigName); + std::optional configPath = joinPaths(path, Luau::kConfigName); + if (!isFile(*configPath)) + configPath = std::nullopt; - if (std::optional contents = readFile(configPath)) + std::optional luauConfigPath = joinPaths(path, Luau::kLuauConfigName); + if (!isFile(*luauConfigPath)) + luauConfigPath = std::nullopt; + + if (configPath && luauConfigPath) { - Luau::ConfigOptions::AliasOptions aliasOpts; - aliasOpts.configLocation = configPath; - aliasOpts.overwriteAliases = true; + std::string ambiguousError = Luau::format("Both %s and %s files exist", Luau::kConfigName, Luau::kLuauConfigName); + configErrors.emplace_back(*configPath, std::move(ambiguousError)); + } + else if (configPath) + { + if (std::optional contents = readFile(*configPath)) + { + Luau::ConfigOptions::AliasOptions aliasOpts; + aliasOpts.configLocation = *configPath; + aliasOpts.overwriteAliases = true; - Luau::ConfigOptions opts; - opts.aliasOptions = std::move(aliasOpts); + Luau::ConfigOptions opts; + opts.aliasOptions = std::move(aliasOpts); - std::optional error = Luau::parseConfig(*contents, result, opts); - if (error) - configErrors.push_back({configPath, *error}); + std::optional error = Luau::parseConfig(*contents, result, opts); + if (error) + configErrors.emplace_back(*configPath, *error); + } + } + else if (luauConfigPath) + { + if (std::optional contents = readFile(*luauConfigPath)) + { + Luau::ConfigOptions::AliasOptions aliasOpts; + aliasOpts.configLocation = *configPath; + aliasOpts.overwriteAliases = true; + + // TODO (CLI-174536): support interrupt callbacks based on TypeCheckLimits + std::optional error = Luau::extractLuauConfig(*contents, result, aliasOpts, Luau::InterruptCallbacks{}); + if (error) + configErrors.emplace_back(*luauConfigPath, *error); + } } return configCache[path] = result; @@ -421,9 +452,10 @@ int main(int argc, char** argv) checkedModules = frontend.checkQueuedModules( std::nullopt, - [&](std::function f) + [&](std::vector> tasks) { - scheduler.push(std::move(f)); + for (auto& task : tasks) + scheduler.push(std::move(task)); } ); } diff --git a/CLI/src/AnalyzeRequirer.cpp b/CLI/src/AnalyzeRequirer.cpp index 318a352f..c6543c4a 100644 --- a/CLI/src/AnalyzeRequirer.cpp +++ b/CLI/src/AnalyzeRequirer.cpp @@ -17,6 +17,18 @@ static Luau::Require::NavigationContext::NavigateResult convert(NavigationStatus return Luau::Require::NavigationContext::NavigateResult::NotFound; } +static Luau::Require::NavigationContext::ConfigStatus convert(VfsNavigator::ConfigStatus status) +{ + if (status == VfsNavigator::ConfigStatus::Ambiguous) + return Luau::Require::NavigationContext::ConfigStatus::Ambiguous; + else if (status == VfsNavigator::ConfigStatus::PresentJson) + return Luau::Require::NavigationContext::ConfigStatus::PresentJson; + else if (status == VfsNavigator::ConfigStatus::PresentLuau) + return Luau::Require::NavigationContext::ConfigStatus::PresentLuau; + else + return Luau::Require::NavigationContext::ConfigStatus::Absent; +} + FileNavigationContext::FileNavigationContext(std::string requirerPath) : requirerPath(std::move(requirerPath)) { @@ -63,9 +75,9 @@ std::optional FileNavigationContext::getIdentifier() const return vfs.getAbsoluteFilePath(); } -bool FileNavigationContext::isConfigPresent() const +Luau::Require::NavigationContext::ConfigStatus FileNavigationContext::getConfigStatus() const { - return isFile(vfs.getLuaurcPath()); + return convert(vfs.getConfigStatus()); } Luau::Require::NavigationContext::ConfigBehavior FileNavigationContext::getConfigBehavior() const @@ -80,5 +92,5 @@ std::optional FileNavigationContext::getAlias(const std::string& al std::optional FileNavigationContext::getConfig() const { - return readFile(vfs.getLuaurcPath()); + return vfs.getConfig(); } diff --git a/CLI/src/Reduce.cpp b/CLI/src/Reduce.cpp index 22c72964..49f43164 100644 --- a/CLI/src/Reduce.cpp +++ b/CLI/src/Reduce.cpp @@ -3,7 +3,7 @@ #include "Luau/Ast.h" #include "Luau/Common.h" #include "Luau/Parser.h" -#include "Luau/Transpiler.h" +#include "Luau/PrettyPrinter.h" #include "Luau/FileUtils.h" @@ -85,7 +85,7 @@ struct Reducer void writeTempScript(bool minify = false) { - std::string source = transpileWithTypes(*root, cstNodeMap); + std::string source = prettyPrintWithTypes(*root, cstNodeMap); if (minify) { diff --git a/CLI/src/ReplRequirer.cpp b/CLI/src/ReplRequirer.cpp index 969f0faf..4c46f229 100644 --- a/CLI/src/ReplRequirer.cpp +++ b/CLI/src/ReplRequirer.cpp @@ -42,6 +42,18 @@ static luarequire_NavigateResult convert(NavigationStatus status) return NAVIGATE_NOT_FOUND; } +static luarequire_ConfigStatus convert(VfsNavigator::ConfigStatus status) +{ + if (status == VfsNavigator::ConfigStatus::Ambiguous) + return CONFIG_AMBIGUOUS; + else if (status == VfsNavigator::ConfigStatus::PresentJson) + return CONFIG_PRESENT_JSON; + else if (status == VfsNavigator::ConfigStatus::PresentLuau) + return CONFIG_PRESENT_LUAU; + else + return CONFIG_ABSENT; +} + static bool is_require_allowed(lua_State* L, void* ctx, const char* requirer_chunkname) { std::string_view chunkname = requirer_chunkname; @@ -107,16 +119,16 @@ static luarequire_WriteResult get_cache_key(lua_State* L, void* ctx, char* buffe return write(req->vfs.getAbsoluteFilePath(), buffer, buffer_size, size_out); } -static bool is_config_present(lua_State* L, void* ctx) +static luarequire_ConfigStatus get_config_status(lua_State* L, void* ctx) { ReplRequirer* req = static_cast(ctx); - return isFile(req->vfs.getLuaurcPath()); + return convert(req->vfs.getConfigStatus()); } static luarequire_WriteResult get_config(lua_State* L, void* ctx, char* buffer, size_t buffer_size, size_t* size_out) { ReplRequirer* req = static_cast(ctx); - return write(readFile(req->vfs.getLuaurcPath()), buffer, buffer_size, size_out); + return write(req->vfs.getConfig(), buffer, buffer_size, size_out); } static int load(lua_State* L, void* ctx, const char* path, const char* chunkname, const char* loadname) @@ -204,11 +216,10 @@ void requireConfigInit(luarequire_Configuration* config) config->to_parent = to_parent; config->to_child = to_child; config->is_module_present = is_module_present; - config->is_config_present = is_config_present; + config->get_config_status = get_config_status; config->get_chunkname = get_chunkname; config->get_loadname = get_loadname; config->get_cache_key = get_cache_key; - config->get_alias = nullptr; config->get_config = get_config; config->load = load; } diff --git a/CLI/src/VfsNavigator.cpp b/CLI/src/VfsNavigator.cpp index 5bea9e71..1351c4ac 100644 --- a/CLI/src/VfsNavigator.cpp +++ b/CLI/src/VfsNavigator.cpp @@ -2,7 +2,9 @@ #include "Luau/VfsNavigator.h" #include "Luau/Common.h" +#include "Luau/Config.h" #include "Luau/FileUtils.h" +#include "Luau/LuauConfig.h" #include #include @@ -188,11 +190,16 @@ NavigationStatus VfsNavigator::toParent() modulePath = normalizePath(modulePath + "/.."); absoluteModulePath = normalizePath(absoluteModulePath + "/.."); - return updateRealPaths(); + // There is no ambiguity when navigating up in a tree. + NavigationStatus status = updateRealPaths(); + return status == NavigationStatus::Ambiguous ? NavigationStatus::Success : status; } NavigationStatus VfsNavigator::toChild(const std::string& name) { + if (name == ".config") + return NavigationStatus::NotFound; + modulePath = normalizePath(modulePath + "/" + name); absoluteModulePath = normalizePath(absoluteModulePath + "/" + name); @@ -209,7 +216,7 @@ std::string VfsNavigator::getAbsoluteFilePath() const return absoluteRealPath; } -std::string VfsNavigator::getLuaurcPath() const +std::string VfsNavigator::getConfigPath(const std::string& filename) const { std::string_view directory = realPath; @@ -218,7 +225,7 @@ std::string VfsNavigator::getLuaurcPath() const if (hasSuffix(directory, suffix)) { directory.remove_suffix(suffix.size()); - return std::string(directory) + "/.luaurc"; + return std::string(directory) + '/' + filename; } } for (std::string_view suffix : kSuffixes) @@ -226,9 +233,37 @@ std::string VfsNavigator::getLuaurcPath() const if (hasSuffix(directory, suffix)) { directory.remove_suffix(suffix.size()); - return std::string(directory) + "/.luaurc"; + return std::string(directory) + '/' + filename; } } - return std::string(directory) + "/.luaurc"; + return std::string(directory) + '/' + filename; +} + +VfsNavigator::ConfigStatus VfsNavigator::getConfigStatus() const +{ + bool luaurcExists = isFile(getConfigPath(Luau::kConfigName)); + bool luauConfigExists = isFile(getConfigPath(Luau::kLuauConfigName)); + + if (luaurcExists && luauConfigExists) + return ConfigStatus::Ambiguous; + else if (luauConfigExists) + return ConfigStatus::PresentLuau; + else if (luaurcExists) + return ConfigStatus::PresentJson; + else + return ConfigStatus::Absent; +} + +std::optional VfsNavigator::getConfig() const +{ + ConfigStatus status = getConfigStatus(); + LUAU_ASSERT(status == ConfigStatus::PresentJson || status == ConfigStatus::PresentLuau); + + if (status == ConfigStatus::PresentJson) + return readFile(getConfigPath(Luau::kConfigName)); + else if (status == ConfigStatus::PresentLuau) + return readFile(getConfigPath(Luau::kLuauConfigName)); + + LUAU_UNREACHABLE(); } diff --git a/CLI/src/Web.cpp b/CLI/src/Web.cpp index 81156e88..f3323359 100644 --- a/CLI/src/Web.cpp +++ b/CLI/src/Web.cpp @@ -4,12 +4,72 @@ #include "luacode.h" #include "Luau/Common.h" +#include "Luau/Frontend.h" +#include "Luau/BuiltinDefinitions.h" #include #include #include +// Simple FileResolver for type checking on luau.org/demo +struct DemoFileResolver + : Luau::FileResolver +{ + DemoFileResolver() + : Luau::FileResolver(nullptr) + { + } + + std::optional readSource(const Luau::ModuleName& name) + { + auto it = source.find(name); + if (it == source.end()) + return std::nullopt; + + Luau::SourceCode::Type sourceType = Luau::SourceCode::Module; + auto it2 = sourceTypes.find(name); + if (it2 != sourceTypes.end()) + sourceType = it2->second; + + return Luau::SourceCode{it->second, sourceType}; + } + + std::optional resolveModuleInfo( + const Luau::ModuleName& currentModuleName, const Luau::AstExpr& pathExpr) + { + return std::nullopt; + } + + const Luau::ModulePtr getModule(const Luau::ModuleName& moduleName) const + { + return nullptr; + } + + bool moduleExists(const Luau::ModuleName& moduleName) const + { + return false; + } + + std::optional resolveModule(const Luau::ModuleInfo* context, Luau::AstExpr* expr) + { + return std::nullopt; + } + + std::string getHumanReadableModuleName(const Luau::ModuleName& name) const + { + return name; + } + + std::optional getEnvironmentForModule(const Luau::ModuleName& name) const + { + return std::nullopt; + } + + std::unordered_map source; + std::unordered_map sourceTypes; +}; + static void setupState(lua_State* L) { luaL_openlibs(L); @@ -88,6 +148,43 @@ static std::string runCode(lua_State* L, const std::string& source) } } +extern "C" const char* checkScript(const char* source) +{ + static std::string result; + result.clear(); + + try + { + DemoFileResolver fileResolver; + Luau::NullConfigResolver configResolver; + Luau::FrontendOptions options; + + Luau::Frontend frontend(&fileResolver, &configResolver, options); + // Add Luau builtins + Luau::unfreeze(frontend.globals.globalTypes); + Luau::registerBuiltinGlobals(frontend, frontend.globals); + Luau::freeze(frontend.globals.globalTypes); + + fileResolver.source["main"] = source; + + Luau::CheckResult checkResult = frontend.check("main"); + for (const Luau::TypeError& err : checkResult.errors) + { + if (!result.empty()) + result += "\n"; + result += std::to_string(err.location.begin.line + 1); + result += ": "; + result += Luau::toString(err); + } + } + catch (const std::exception& e) + { + result = e.what(); + } + + return result.empty() ? nullptr : result.c_str(); +} + extern "C" const char* executeScript(const char* source) { // setup flags diff --git a/CMakeLists.txt b/CMakeLists.txt index 9db15a26..afd212c2 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -44,7 +44,6 @@ add_library(Luau.EqSat STATIC) add_library(Luau.CodeGen STATIC) add_library(Luau.VM STATIC) add_library(Luau.Require STATIC) -add_library(Luau.RequireNavigator STATIC) add_library(isocline STATIC) if(LUAU_BUILD_CLI) @@ -96,6 +95,7 @@ target_link_libraries(Luau.Compiler PUBLIC Luau.Ast) target_compile_features(Luau.Config PUBLIC cxx_std_17) target_include_directories(Luau.Config PUBLIC Config/include) target_link_libraries(Luau.Config PUBLIC Luau.Ast) +target_link_libraries(Luau.Config PRIVATE Luau.Compiler Luau.VM) target_compile_features(Luau.Analysis PUBLIC cxx_std_17) target_include_directories(Luau.Analysis PUBLIC Analysis/include) @@ -116,13 +116,8 @@ target_include_directories(Luau.VM PUBLIC VM/include "${PACKAGE_INCLUDE_DIR}") target_link_libraries(Luau.VM PUBLIC Luau.Common) target_compile_features(Luau.Require PUBLIC cxx_std_17) -target_include_directories(Luau.Require PUBLIC Require/Runtime/include) -target_link_libraries(Luau.Require PUBLIC Luau.VM) -target_link_libraries(Luau.Require PRIVATE Luau.RequireNavigator) - -target_compile_features(Luau.RequireNavigator PUBLIC cxx_std_17) -target_include_directories(Luau.RequireNavigator PUBLIC Require/Navigator/include) -target_link_libraries(Luau.RequireNavigator PUBLIC Luau.Config) +target_include_directories(Luau.Require PUBLIC Require/include) +target_link_libraries(Luau.Require PUBLIC Luau.Config Luau.VM) target_include_directories(isocline PUBLIC extern/isocline/include) @@ -258,7 +253,7 @@ if(LUAU_BUILD_CLI) target_link_libraries(Luau.Compile.CLI PRIVATE osthreads) target_link_libraries(Luau.Bytecode.CLI PRIVATE osthreads) - target_link_libraries(Luau.Analyze.CLI PRIVATE Luau.Analysis Luau.CLI.lib Luau.RequireNavigator) + target_link_libraries(Luau.Analyze.CLI PRIVATE Luau.Analysis Luau.CLI.lib Luau.Require) target_link_libraries(Luau.Ast.CLI PRIVATE Luau.Ast Luau.Analysis Luau.CLI.lib) @@ -306,10 +301,10 @@ endif() if(LUAU_BUILD_WEB) target_compile_options(Luau.Web PRIVATE ${LUAU_OPTIONS}) - target_link_libraries(Luau.Web PRIVATE Luau.Compiler Luau.VM) + target_link_libraries(Luau.Web PRIVATE Luau.Compiler Luau.VM Luau.Analysis) # declare exported functions to emscripten - target_link_options(Luau.Web PRIVATE -sEXPORTED_FUNCTIONS=['_executeScript'] -sEXPORTED_RUNTIME_METHODS=['ccall','cwrap']) + target_link_options(Luau.Web PRIVATE -sEXPORTED_FUNCTIONS=["_executeScript","_checkScript"] -sEXPORTED_RUNTIME_METHODS=["ccall","cwrap"]) # add -fexceptions for emscripten to allow exceptions to be caught in C++ target_link_options(Luau.Web PRIVATE -fexceptions) diff --git a/CodeGen/include/Luau/AssemblyBuilderX64.h b/CodeGen/include/Luau/AssemblyBuilderX64.h index 53617a1e..529c4f09 100644 --- a/CodeGen/include/Luau/AssemblyBuilderX64.h +++ b/CodeGen/include/Luau/AssemblyBuilderX64.h @@ -170,8 +170,8 @@ class AssemblyBuilderX64 void vcmpltsd(OperandX64 dst, OperandX64 src1, OperandX64 src2); void vcmpeqps(OperandX64 dst, OperandX64 src1, OperandX64 src2); - void vblendvps(RegisterX64 dst, RegisterX64 src1, RegisterX64 src2, OperandX64 mask); - void vblendvpd(RegisterX64 dst, RegisterX64 src1, RegisterX64 src2, OperandX64 mask); + void vblendvps(RegisterX64 dst, RegisterX64 src1, OperandX64 src2, RegisterX64 mask); + void vblendvpd(RegisterX64 dst, RegisterX64 src1, OperandX64 src2, RegisterX64 mask); void vblendvpd_DEPRECATED(RegisterX64 dst, RegisterX64 src1, OperandX64 mask, RegisterX64 src3); void vpshufps(RegisterX64 dst, RegisterX64 src1, OperandX64 src2, uint8_t shuffle); diff --git a/CodeGen/include/Luau/IrData.h b/CodeGen/include/Luau/IrData.h index c0eadc77..137cf902 100644 --- a/CodeGen/include/Luau/IrData.h +++ b/CodeGen/include/Luau/IrData.h @@ -233,6 +233,18 @@ enum class IrCmd : uint8_t // C: condition CMP_INT, + // Perform a comparison of two tags. Result is an integer register containing 0 or 1 + CMP_TAG, + // A, B: tag + // C: condition (eq/not_eq) + + // Perform tag and value comparison. Result is an integer register containing 0 or 1 + CMP_SPLIT_TVALUE, + // A: tag + // B: tag (constant: boolean/number/string) + // C, D: value + // E: condition (eq/not_eq) + // Unconditional jump // A: block/vmexit/undef JUMP, @@ -462,7 +474,7 @@ enum class IrCmd : uint8_t CHECK_NO_METATABLE, // Guard against executing in unsafe environment, exits to VM on check failure - // A: vmexit/vmexit/undef + // A: block/vmexit/undef // When undef is specified, execution is aborted on check failure CHECK_SAFE_ENV, @@ -653,7 +665,7 @@ enum class IrCmd : uint8_t // C: Kn (prototype) FALLBACK_DUPCLOSURE, - // Prepare loop variables for a generic for loop, jump to the loop backedge unconditionally + // Prepare loop variables for a generic for loop, jump to the loop back edge unconditionally // A: unsigned int (bytecode instruction index) // B: Rn (loop state start, updates Rn Rn+1 Rn+2) // C: block diff --git a/CodeGen/include/Luau/IrUtils.h b/CodeGen/include/Luau/IrUtils.h index 520aec45..b66fd6f5 100644 --- a/CodeGen/include/Luau/IrUtils.h +++ b/CodeGen/include/Luau/IrUtils.h @@ -113,6 +113,8 @@ inline bool hasResult(IrCmd cmd) case IrCmd::NOT_ANY: case IrCmd::CMP_ANY: case IrCmd::CMP_INT: + case IrCmd::CMP_TAG: + case IrCmd::CMP_SPLIT_TVALUE: case IrCmd::TABLE_LEN: case IrCmd::TABLE_SETNUM: case IrCmd::STRING_LEN: diff --git a/CodeGen/include/Luau/IrVisitUseDef.h b/CodeGen/include/Luau/IrVisitUseDef.h index 029507f4..0674641b 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(LuauCodegenDirectCompare2) + namespace Luau { namespace CodeGen @@ -37,10 +39,18 @@ static void visitVmRegDefsUses(T& visitor, IrFunction& function, const IrInst& i visitor.use(inst.a); visitor.use(inst.b); break; + case IrCmd::CMP_TAG: + if (FFlag::LuauCodegenDirectCompare2) + visitor.maybeUse(inst.a); + break; case IrCmd::JUMP_IF_TRUTHY: case IrCmd::JUMP_IF_FALSY: visitor.use(inst.a); break; + case IrCmd::JUMP_EQ_TAG: + if (FFlag::LuauCodegenDirectCompare2) + visitor.maybeUse(inst.a); + break; // A <- B, C case IrCmd::DO_ARITH: visitor.maybeUse(inst.b); // Argument can also be a VmConst diff --git a/CodeGen/src/AssemblyBuilderX64.cpp b/CodeGen/src/AssemblyBuilderX64.cpp index 929bd167..5542fe05 100644 --- a/CodeGen/src/AssemblyBuilderX64.cpp +++ b/CodeGen/src/AssemblyBuilderX64.cpp @@ -942,10 +942,10 @@ void AssemblyBuilderX64::vcmpeqps(OperandX64 dst, OperandX64 src1, OperandX64 sr placeAvx("vcmpeqps", dst, src1, src2, 0x00, 0xc2, false, AVX_0F, AVX_NP); } -void AssemblyBuilderX64::vblendvps(RegisterX64 dst, RegisterX64 src1, RegisterX64 src2, OperandX64 mask) +void AssemblyBuilderX64::vblendvps(RegisterX64 dst, RegisterX64 src1, OperandX64 src2, RegisterX64 mask) { // bits [7:4] of imm8 are used to select register for operand 4 - placeAvx("vblendvpd", dst, src1, mask, src2.index << 4, 0x4a, false, AVX_0F3A, AVX_66); + placeAvx("vblendvps", dst, src1, src2, mask.index << 4, 0x4a, false, AVX_0F3A, AVX_66); } void AssemblyBuilderX64::vblendvpd_DEPRECATED(RegisterX64 dst, RegisterX64 src1, OperandX64 mask, RegisterX64 src3) @@ -954,10 +954,10 @@ void AssemblyBuilderX64::vblendvpd_DEPRECATED(RegisterX64 dst, RegisterX64 src1, placeAvx("vblendvpd", dst, src1, mask, src3.index << 4, 0x4b, false, AVX_0F3A, AVX_66); } -void AssemblyBuilderX64::vblendvpd(RegisterX64 dst, RegisterX64 src1, RegisterX64 src2, OperandX64 mask) +void AssemblyBuilderX64::vblendvpd(RegisterX64 dst, RegisterX64 src1, OperandX64 src2, RegisterX64 mask) { // bits [7:4] of imm8 are used to select register for operand 4 - placeAvx("vblendvpd", dst, src1, mask, src2.index << 4, 0x4b, false, AVX_0F3A, AVX_66); + placeAvx("vblendvpd", dst, src1, src2, mask.index << 4, 0x4b, false, AVX_0F3A, AVX_66); } void AssemblyBuilderX64::vpshufps(RegisterX64 dst, RegisterX64 src1, OperandX64 src2, uint8_t shuffle) diff --git a/CodeGen/src/BytecodeAnalysis.cpp b/CodeGen/src/BytecodeAnalysis.cpp index c0fee693..3ea22f3c 100644 --- a/CodeGen/src/BytecodeAnalysis.cpp +++ b/CodeGen/src/BytecodeAnalysis.cpp @@ -10,8 +10,6 @@ #include -LUAU_FASTFLAGVARIABLE(LuauCodeGenBetterBytecodeAnalysis) - namespace Luau { namespace CodeGen @@ -154,7 +152,7 @@ static BytecodeRegTypeInfo* findRegType(BytecodeTypeInfo& info, uint8_t reg, int auto b = info.regTypes.begin() + info.regTypeOffsets[reg]; auto e = info.regTypes.begin() + info.regTypeOffsets[reg + 1]; - // Doen't have info + // Doesn't have info if (b == e) return nullptr; @@ -636,7 +634,7 @@ void buildBytecodeBlocks(IrFunction& function, const std::vector& jumpT int target = getJumpTarget(*pc, uint32_t(i)); - // Implicit fallthroughs terminate the block and might start a new one + // Implicit fallthrough terminate the block and might start a new one if (target >= 0 && !isFastCall(op)) { bcBlocks.back().finishpc = i; @@ -774,26 +772,16 @@ void analyzeBytecodeTypes(IrFunction& function, const HostIrHooks& hostHooks) } case LOP_GETTABLE: { - if (FFlag::LuauCodeGenBetterBytecodeAnalysis) - { - int ra = LUAU_INSN_A(*pc); - int rb = LUAU_INSN_B(*pc); - int rc = LUAU_INSN_C(*pc); + int ra = LUAU_INSN_A(*pc); + int rb = LUAU_INSN_B(*pc); + int rc = LUAU_INSN_C(*pc); - regTags[ra] = LBC_TYPE_ANY; + regTags[ra] = LBC_TYPE_ANY; - bcType.a = regTags[rb]; - bcType.b = regTags[rc]; + bcType.a = regTags[rb]; + bcType.b = regTags[rc]; - bcType.result = regTags[ra]; - } - else - { - int rb = LUAU_INSN_B(*pc); - int rc = LUAU_INSN_C(*pc); - bcType.a = regTags[rb]; - bcType.b = regTags[rc]; - } + bcType.result = regTags[ra]; break; } case LOP_SETTABLE: @@ -851,25 +839,15 @@ void analyzeBytecodeTypes(IrFunction& function, const HostIrHooks& hostHooks) } case LOP_GETTABLEN: { - if (FFlag::LuauCodeGenBetterBytecodeAnalysis) - { - int ra = LUAU_INSN_A(*pc); - int rb = LUAU_INSN_B(*pc); + int ra = LUAU_INSN_A(*pc); + int rb = LUAU_INSN_B(*pc); - regTags[ra] = LBC_TYPE_ANY; + regTags[ra] = LBC_TYPE_ANY; - bcType.a = regTags[rb]; - bcType.b = LBC_TYPE_NUMBER; + bcType.a = regTags[rb]; + bcType.b = LBC_TYPE_NUMBER; - bcType.result = regTags[ra]; - } - else - { - int rb = LUAU_INSN_B(*pc); - bcType.a = regTags[rb]; - bcType.b = LBC_TYPE_NUMBER; - break; - } + bcType.result = regTags[ra]; break; } case LOP_SETTABLEN: @@ -1157,11 +1135,8 @@ void analyzeBytecodeTypes(IrFunction& function, const HostIrHooks& hostHooks) refineRegType(bcTypeInfo, ra, i, bcType.result); - if (FFlag::LuauCodeGenBetterBytecodeAnalysis) - { - // Fastcall failure fallback is skipped from result propagation - i += skip; - } + // Fastcall failure fallback is skipped from result propagation + i += skip; break; } case LOP_FASTCALL1: @@ -1181,11 +1156,8 @@ void analyzeBytecodeTypes(IrFunction& function, const HostIrHooks& hostHooks) refineRegType(bcTypeInfo, ra, i, bcType.result); - if (FFlag::LuauCodeGenBetterBytecodeAnalysis) - { - // Fastcall failure fallback is skipped from result propagation - i += skip; - } + // Fastcall failure fallback is skipped from result propagation + i += skip; break; } case LOP_FASTCALL2: @@ -1205,11 +1177,8 @@ void analyzeBytecodeTypes(IrFunction& function, const HostIrHooks& hostHooks) refineRegType(bcTypeInfo, ra, i, bcType.result); - if (FFlag::LuauCodeGenBetterBytecodeAnalysis) - { - // Fastcall failure fallback is skipped from result propagation - i += skip; - } + // Fastcall failure fallback is skipped from result propagation + i += skip; break; } case LOP_FASTCALL3: @@ -1225,17 +1194,14 @@ void analyzeBytecodeTypes(IrFunction& function, const HostIrHooks& hostHooks) applyBuiltinCall(LuauBuiltinFunction(bfid), bcType); regTags[LUAU_INSN_B(*pc)] = bcType.a; - regTags[aux & 0xff] = bcType.b; - regTags[(aux >> 8) & 0xff] = bcType.c; + regTags[LUAU_INSN_AUX_A(aux)] = bcType.b; + regTags[LUAU_INSN_AUX_B(aux)] = bcType.c; regTags[ra] = bcType.result; refineRegType(bcTypeInfo, ra, i, bcType.result); - if (FFlag::LuauCodeGenBetterBytecodeAnalysis) - { - // Fastcall failure fallback is skipped from result propagation - i += skip; - } + // Fastcall failure fallback is skipped from result propagation + i += skip; break; } case LOP_FORNPREP: @@ -1347,13 +1313,10 @@ void analyzeBytecodeTypes(IrFunction& function, const HostIrHooks& hostHooks) } case LOP_GETGLOBAL: { - if (FFlag::LuauCodeGenBetterBytecodeAnalysis) - { - int ra = LUAU_INSN_A(*pc); + int ra = LUAU_INSN_A(*pc); - regTags[ra] = LBC_TYPE_ANY; - bcType.result = regTags[ra]; - } + regTags[ra] = LBC_TYPE_ANY; + bcType.result = regTags[ra]; break; } case LOP_SETGLOBAL: @@ -1382,48 +1345,39 @@ void analyzeBytecodeTypes(IrFunction& function, const HostIrHooks& hostHooks) case LOP_AND: case LOP_OR: { - if (FFlag::LuauCodeGenBetterBytecodeAnalysis) - { - int ra = LUAU_INSN_A(*pc); - int rb = LUAU_INSN_B(*pc); - int rc = LUAU_INSN_C(*pc); + int ra = LUAU_INSN_A(*pc); + int rb = LUAU_INSN_B(*pc); + int rc = LUAU_INSN_C(*pc); - bcType.a = regTags[rb]; - bcType.b = regTags[rc]; + bcType.a = regTags[rb]; + bcType.b = regTags[rc]; - regTags[ra] = LBC_TYPE_ANY; - bcType.result = regTags[ra]; - } + regTags[ra] = LBC_TYPE_ANY; + bcType.result = regTags[ra]; break; } case LOP_ANDK: case LOP_ORK: { - if (FFlag::LuauCodeGenBetterBytecodeAnalysis) - { - int ra = LUAU_INSN_A(*pc); - int rb = LUAU_INSN_B(*pc); - int kc = LUAU_INSN_C(*pc); + int ra = LUAU_INSN_A(*pc); + int rb = LUAU_INSN_B(*pc); + int kc = LUAU_INSN_C(*pc); - bcType.a = regTags[rb]; - bcType.b = getBytecodeConstantTag(proto, kc); + bcType.a = regTags[rb]; + bcType.b = getBytecodeConstantTag(proto, kc); - regTags[ra] = LBC_TYPE_ANY; - bcType.result = regTags[ra]; - } + regTags[ra] = LBC_TYPE_ANY; + bcType.result = regTags[ra]; break; } case LOP_COVERAGE: break; case LOP_GETIMPORT: { - if (FFlag::LuauCodeGenBetterBytecodeAnalysis) - { - int ra = LUAU_INSN_A(*pc); + int ra = LUAU_INSN_A(*pc); - regTags[ra] = LBC_TYPE_ANY; - bcType.result = regTags[ra]; - } + regTags[ra] = LBC_TYPE_ANY; + bcType.result = regTags[ra]; break; } case LOP_CAPTURE: diff --git a/CodeGen/src/CodeGen.cpp b/CodeGen/src/CodeGen.cpp index c8e88231..850311e1 100644 --- a/CodeGen/src/CodeGen.cpp +++ b/CodeGen/src/CodeGen.cpp @@ -210,7 +210,7 @@ bool isSupported() #endif // We require AVX1 support for VEX encoded XMM operations - // We also requre SSE4.1 support for ROUNDSD but the AVX check below covers it + // We also require SSE4.1 support for ROUNDSD but the AVX check below covers it // https://en.wikipedia.org/wiki/CPUID#EAX=1:_Processor_Info_and_Feature_Bits if ((cpuinfo[2] & (1 << 28)) == 0) return false; diff --git a/CodeGen/src/CodeGenA64.cpp b/CodeGen/src/CodeGenA64.cpp index 6505f788..f35d4684 100644 --- a/CodeGen/src/CodeGenA64.cpp +++ b/CodeGen/src/CodeGenA64.cpp @@ -117,7 +117,7 @@ static void emitContinueCall(AssemblyBuilderA64& build, ModuleHelpers& helpers) build.mov(rClosure, x0); - CODEGEN_ASSERT(offsetof(Proto, code) == offsetof(Proto, k) + 8); + static_assert(offsetof(Proto, code) == offsetof(Proto, k) + sizeof(Proto::k)); build.ldp(rConstants, rCode, mem(x1, offsetof(Proto, k))); // proto->k, proto->code build.br(x2); @@ -184,12 +184,12 @@ void emitReturn(AssemblyBuilderA64& build, ModuleHelpers& helpers) if (DFFlag::AddReturnExectargetCheck) { // Get new instruction location - CODEGEN_ASSERT(offsetof(Proto, exectarget) == offsetof(Proto, execdata) + 8); + static_assert(offsetof(Proto, exectarget) == offsetof(Proto, execdata) + sizeof(Proto::execdata)); build.ldp(x3, x4, mem(x1, offsetof(Proto, execdata))); build.cbz(x4, helpers.exitContinueVmClearNativeFlag); } - CODEGEN_ASSERT(offsetof(Proto, code) == offsetof(Proto, k) + 8); + static_assert(offsetof(Proto, code) == offsetof(Proto, k) + sizeof(Proto::k)); build.ldp(rConstants, rCode, mem(x1, offsetof(Proto, k))); // proto->k, proto->code // Get instruction index from instruction pointer @@ -201,7 +201,7 @@ void emitReturn(AssemblyBuilderA64& build, ModuleHelpers& helpers) if (!DFFlag::AddReturnExectargetCheck) { // Get new instruction location and jump to it - CODEGEN_ASSERT(offsetof(Proto, exectarget) == offsetof(Proto, execdata) + 8); + static_assert(offsetof(Proto, exectarget) == offsetof(Proto, execdata) + sizeof(Proto::execdata)); build.ldp(x3, x4, mem(x1, offsetof(Proto, execdata))); } build.ldr(w2, mem(x3, x2)); @@ -240,7 +240,7 @@ static EntryLocations buildEntryFunction(AssemblyBuilderA64& build, UnwindBuilde build.ldr(rBase, mem(x0, offsetof(lua_State, base))); // L->base - CODEGEN_ASSERT(offsetof(Proto, code) == offsetof(Proto, k) + 8); + static_assert(offsetof(Proto, code) == offsetof(Proto, k) + sizeof(Proto::k)); build.ldp(rConstants, rCode, mem(x1, offsetof(Proto, k))); // proto->k, proto->code build.ldr(x9, mem(x0, offsetof(lua_State, ci))); // L->ci @@ -300,7 +300,7 @@ bool initHeaderFunctions(BaseCodeGenContext& codeGenContext) return false; } - // Set the offset at the begining so that functions in new blocks will not overlay the locations + // Set the offset at the beginning so that functions in new blocks will not overlay the locations // specified by the unwind information of the entry function unwind.setBeginOffset(build.getLabelOffset(entryLocations.prologueEnd)); diff --git a/CodeGen/src/CodeGenContext.cpp b/CodeGen/src/CodeGenContext.cpp index 678d4f19..75404766 100644 --- a/CodeGen/src/CodeGenContext.cpp +++ b/CodeGen/src/CodeGenContext.cpp @@ -15,7 +15,6 @@ LUAU_FASTINTVARIABLE(LuauCodeGenBlockSize, 4 * 1024 * 1024) LUAU_FASTINTVARIABLE(LuauCodeGenMaxTotalSize, 256 * 1024 * 1024) -LUAU_FASTFLAGVARIABLE(LuauCodeGenUnassignedBcTargetAbort) LUAU_DYNAMIC_FASTFLAGVARIABLE(LuauCodeGenDisableWithNoReentry, false) namespace Luau @@ -447,31 +446,18 @@ void create(lua_State* L, SharedCodeGenContext* codeGenContext) NativeProtoExecDataPtr nativeExecData = createNativeProtoExecData(proto->sizecode); uint32_t instTarget = ir.function.entryLocation; + uint32_t unassignedOffset = ir.function.endLocation - instTarget; - if (FFlag::LuauCodeGenUnassignedBcTargetAbort) + for (int i = 0; i < proto->sizecode; ++i) { - uint32_t unassignedOffset = ir.function.endLocation - instTarget; + const BytecodeMapping& bcMapping = ir.function.bcMapping[i]; - for (int i = 0; i < proto->sizecode; ++i) - { - const BytecodeMapping& bcMapping = ir.function.bcMapping[i]; - - CODEGEN_ASSERT(bcMapping.asmLocation >= instTarget); + CODEGEN_ASSERT(bcMapping.asmLocation >= instTarget); - if (bcMapping.asmLocation != ~0u) - nativeExecData[i] = bcMapping.asmLocation - instTarget; - else - nativeExecData[i] = unassignedOffset; - } - } - else - { - for (int i = 0; i < proto->sizecode; ++i) - { - CODEGEN_ASSERT(ir.function.bcMapping[i].asmLocation >= instTarget); - - nativeExecData[i] = ir.function.bcMapping[i].asmLocation - instTarget; - } + if (bcMapping.asmLocation != ~0u) + nativeExecData[i] = bcMapping.asmLocation - instTarget; + else + nativeExecData[i] = unassignedOffset; } // Set first instruction offset to 0 so that entering this function still diff --git a/CodeGen/src/CodeGenX64.cpp b/CodeGen/src/CodeGenX64.cpp index 0884fe68..233996ef 100644 --- a/CodeGen/src/CodeGenX64.cpp +++ b/CodeGen/src/CodeGenX64.cpp @@ -215,7 +215,7 @@ bool initHeaderFunctions(BaseCodeGenContext& codeGenContext) return false; } - // Set the offset at the begining so that functions in new blocks will not overlay the locations + // Set the offset at the beginning so that functions in new blocks will not overlay the locations // specified by the unwind information of the entry function unwind.setBeginOffset(build.getLabelOffset(entryLocations.prologueEnd)); diff --git a/CodeGen/src/IrBuilder.cpp b/CodeGen/src/IrBuilder.cpp index 3d673f92..69cde9ab 100644 --- a/CodeGen/src/IrBuilder.cpp +++ b/CodeGen/src/IrBuilder.cpp @@ -12,6 +12,8 @@ #include +LUAU_FASTFLAGVARIABLE(LuauCodegenDirectCompare2) + namespace Luau { namespace CodeGen @@ -252,6 +254,31 @@ void IrBuilder::rebuildBytecodeBasicBlocks(Proto* proto) buildBytecodeBlocks(function, jumpTargets); } +static bool isDirectCompare(Proto* proto, const Instruction* pc, int i) +{ + // Matching the compiler sequence for generating 0 or 1 based on a comparison between values: + // LOP_JUMP** Lx + // [aux] + // LOADB Rx, 0 +1 + // Lx: LOADB Rx, 1 + if (i + 3 < proto->sizecode && LUAU_INSN_D(*pc) == 2) + { + const Instruction loadTrue = pc[2]; + const Instruction loadFalse = pc[3]; + + if (LUAU_INSN_OP(loadTrue) == LOP_LOADB && LUAU_INSN_OP(loadFalse) == LOP_LOADB) + { + bool sameTarget = LUAU_INSN_A(loadTrue) == LUAU_INSN_A(loadFalse); + bool zeroAndOne = LUAU_INSN_B(loadTrue) == 0 && LUAU_INSN_B(loadFalse) == 1; + bool correctJumps = LUAU_INSN_C(loadTrue) == 1 && LUAU_INSN_C(loadFalse) == 0; + + return sameTarget && zeroAndOne && correctJumps; + } + } + + return false; +} + void IrBuilder::translateInst(LuauOpcode op, const Instruction* pc, int i) { switch (int(op)) @@ -354,15 +381,55 @@ void IrBuilder::translateInst(LuauOpcode op, const Instruction* pc, int i) translateInstJumpX(*this, pc, i); break; case LOP_JUMPXEQKNIL: + if (FFlag::LuauCodegenDirectCompare2 && isDirectCompare(function.proto, pc, i)) + { + translateInstJumpxEqNilShortcut(*this, pc, i); + + // We complete the current instruction and the first LOADB, but we do not skip the second LOADB + // This is because the second LOADB was a jump target so there is a block prepared to handle it + cmdSkipTarget = i + 3; + break; + } + translateInstJumpxEqNil(*this, pc, i); break; case LOP_JUMPXEQKB: + if (FFlag::LuauCodegenDirectCompare2 && isDirectCompare(function.proto, pc, i)) + { + translateInstJumpxEqBShortcut(*this, pc, i); + + // We complete the current instruction and the first LOADB, but we do not skip the second LOADB + // This is because the second LOADB was a jump target so there is a block prepared to handle it + cmdSkipTarget = i + 3; + break; + } + translateInstJumpxEqB(*this, pc, i); break; case LOP_JUMPXEQKN: + if (FFlag::LuauCodegenDirectCompare2 && isDirectCompare(function.proto, pc, i)) + { + translateInstJumpxEqNShortcut(*this, pc, i); + + // We complete the current instruction and the first LOADB, but we do not skip the second LOADB + // This is because the second LOADB was a jump target so there is a block prepared to handle it + cmdSkipTarget = i + 3; + break; + } + translateInstJumpxEqN(*this, pc, i); break; case LOP_JUMPXEQKS: + if (FFlag::LuauCodegenDirectCompare2 && isDirectCompare(function.proto, pc, i)) + { + translateInstJumpxEqSShortcut(*this, pc, i); + + // We complete the current instruction and the first LOADB, but we do not skip the second LOADB + // This is because the second LOADB was a jump target so there is a block prepared to handle it + cmdSkipTarget = i + 3; + break; + } + translateInstJumpxEqS(*this, pc, i); break; case LOP_ADD: diff --git a/CodeGen/src/IrDump.cpp b/CodeGen/src/IrDump.cpp index 2937a5f4..5217ad10 100644 --- a/CodeGen/src/IrDump.cpp +++ b/CodeGen/src/IrDump.cpp @@ -193,6 +193,10 @@ const char* getCmdName(IrCmd cmd) return "CMP_ANY"; case IrCmd::CMP_INT: return "CMP_INT"; + case IrCmd::CMP_TAG: + return "CMP_TAG"; + case IrCmd::CMP_SPLIT_TVALUE: + return "CMP_SPLIT_TVALUE"; case IrCmd::JUMP: return "JUMP"; case IrCmd::JUMP_IF_TRUTHY: diff --git a/CodeGen/src/IrLoweringA64.cpp b/CodeGen/src/IrLoweringA64.cpp index 3880349b..dbf3ac6b 100644 --- a/CodeGen/src/IrLoweringA64.cpp +++ b/CodeGen/src/IrLoweringA64.cpp @@ -12,9 +12,7 @@ #include "lstate.h" #include "lgc.h" -LUAU_FASTFLAG(LuauCodeGenUnassignedBcTargetAbort) -LUAU_FASTFLAG(LuauCodeGenDirectBtest) -LUAU_FASTFLAG(LuauCodeGenRegAutoSpillA64) +LUAU_FASTFLAG(LuauCodegenDirectCompare2) namespace Luau { @@ -261,7 +259,7 @@ IrLoweringA64::IrLoweringA64(AssemblyBuilderA64& build, ModuleHelpers& helpers, , valueTracker(function) , exitHandlerMap(~0u) { - valueTracker.setRestoreCallack( + valueTracker.setRestoreCallback( this, [](void* context, IrInst& inst) { @@ -273,8 +271,7 @@ IrLoweringA64::IrLoweringA64(AssemblyBuilderA64& build, ModuleHelpers& helpers, void IrLoweringA64::lowerInst(IrInst& inst, uint32_t index, const IrBlock& next) { - if (FFlag::LuauCodeGenRegAutoSpillA64) - regs.currInstIdx = index; + regs.currInstIdx = index; valueTracker.beforeInstLowering(inst); @@ -845,12 +842,12 @@ void IrLoweringA64::lowerInst(IrInst& inst, uint32_t index, const IrBlock& next) } else { - Label notbool, exit; + Label notBool, exit; // use the fact that NIL is the only value less than BOOLEAN to do two tag comparisons at once CODEGEN_ASSERT(LUA_TNIL == 0 && LUA_TBOOLEAN == 1); build.cmp(regOp(inst.a), LUA_TBOOLEAN); - build.b(ConditionA64::NotEqual, notbool); + build.b(ConditionA64::NotEqual, notBool); if (inst.b.kind == IrOpKind::Constant) build.mov(inst.regA64, intOp(inst.b) == 0 ? 1 : 0); @@ -860,7 +857,7 @@ void IrLoweringA64::lowerInst(IrInst& inst, uint32_t index, const IrBlock& next) build.b(exit); // not boolean => result is true iff tag was nil - build.setLabel(notbool); + build.setLabel(notBool); build.cset(inst.regA64, ConditionA64::Less); build.setLabel(exit); @@ -869,8 +866,6 @@ void IrLoweringA64::lowerInst(IrInst& inst, uint32_t index, const IrBlock& next) } case IrCmd::CMP_INT: { - CODEGEN_ASSERT(FFlag::LuauCodeGenDirectBtest); - inst.regA64 = regs.allocReuse(KindA64::w, index, {inst.a, inst.b}); IrCondition cond = conditionOp(inst.c); @@ -916,6 +911,127 @@ void IrLoweringA64::lowerInst(IrInst& inst, uint32_t index, const IrBlock& next) inst.regA64 = regs.takeReg(w0, index); break; } + case IrCmd::CMP_TAG: + { + CODEGEN_ASSERT(FFlag::LuauCodegenDirectCompare2); + inst.regA64 = regs.allocReuse(KindA64::w, index, {inst.a, inst.b}); + + IrCondition cond = conditionOp(inst.c); + CODEGEN_ASSERT(cond == IrCondition::Equal || cond == IrCondition::NotEqual); + RegisterA64 aReg = noreg; + RegisterA64 bReg = noreg; + + if (inst.a.kind == IrOpKind::Inst) + { + aReg = regOp(inst.a); + } + else if (inst.a.kind == IrOpKind::VmReg) + { + aReg = regs.allocTemp(KindA64::w); + AddressA64 addr = tempAddr(inst.a, offsetof(TValue, tt)); + build.ldr(aReg, addr); + } + else + { + CODEGEN_ASSERT(inst.a.kind == IrOpKind::Constant); + } + + if (inst.b.kind == IrOpKind::Inst) + { + bReg = regOp(inst.b); + } + else if (inst.b.kind == IrOpKind::VmReg) + { + bReg = regs.allocTemp(KindA64::w); + AddressA64 addr = tempAddr(inst.b, offsetof(TValue, tt)); + build.ldr(bReg, addr); + } + else + { + CODEGEN_ASSERT(inst.b.kind == IrOpKind::Constant); + } + + if (inst.a.kind == IrOpKind::Constant) + { + build.cmp(bReg, tagOp(inst.a)); + build.cset(inst.regA64, getInverseCondition(getConditionInt(cond))); + } + else if (inst.b.kind == IrOpKind::Constant) + { + build.cmp(aReg, tagOp(inst.b)); + build.cset(inst.regA64, getConditionInt(cond)); + } + else + { + build.cmp(aReg, bReg); + build.cset(inst.regA64, getConditionInt(cond)); + } + break; + } + case IrCmd::CMP_SPLIT_TVALUE: + { + CODEGEN_ASSERT(FFlag::LuauCodegenDirectCompare2); + inst.regA64 = regs.allocReuse(KindA64::w, index, {inst.a, inst.b}); + + // Second operand of this instruction must be a constant + // Without a constant type, we wouldn't know the correct way to compare the values at lowering time + CODEGEN_ASSERT(inst.b.kind == IrOpKind::Constant); + + IrCondition cond = conditionOp(inst.e); + CODEGEN_ASSERT(cond == IrCondition::Equal || cond == IrCondition::NotEqual); + + // Check tag equality first + RegisterA64 temp = regs.allocTemp(KindA64::w); + + if (inst.a.kind != IrOpKind::Constant) + { + build.cmp(regOp(inst.a), tagOp(inst.b)); + build.cset(temp, getConditionInt(cond)); + } + else + { + // Constant folding had to handle different constant tags + CODEGEN_ASSERT(tagOp(inst.a) == tagOp(inst.b)); + } + + if (tagOp(inst.b) == LUA_TBOOLEAN) + { + if (inst.c.kind == IrOpKind::Constant) + build.cmp(regOp(inst.d), intOp(inst.c)); // swapped arguments + else if (inst.d.kind == IrOpKind::Constant) + build.cmp(regOp(inst.c), intOp(inst.d)); + else + build.cmp(regOp(inst.c), regOp(inst.d)); + + build.cset(inst.regA64, getConditionInt(cond)); + } + else if (tagOp(inst.b) == LUA_TSTRING) + { + build.cmp(regOp(inst.c), regOp(inst.d)); + build.cset(inst.regA64, getConditionInt(cond)); + } + else if (tagOp(inst.b) == LUA_TNUMBER) + { + RegisterA64 temp1 = tempDouble(inst.c); + RegisterA64 temp2 = tempDouble(inst.d); + + build.fcmp(temp1, temp2); + build.cset(inst.regA64, getConditionFP(cond)); + } + else + { + CODEGEN_ASSERT(!"unsupported type tag in CMP_SPLIT_TVALUE"); + } + + if (inst.a.kind != IrOpKind::Constant) + { + if (cond == IrCondition::Equal) + build.and_(inst.regA64, inst.regA64, temp); + else + build.orr(inst.regA64, inst.regA64, temp); + } + break; + } case IrCmd::JUMP: if (inst.a.kind == IrOpKind::Undef || inst.a.kind == IrOpKind::VmExit) { @@ -964,18 +1080,67 @@ void IrLoweringA64::lowerInst(IrInst& inst, uint32_t index, const IrBlock& next) { RegisterA64 zr = noreg; - if (inst.a.kind == IrOpKind::Constant && tagOp(inst.a) == 0) - zr = regOp(inst.b); - else if (inst.b.kind == IrOpKind::Constant && tagOp(inst.b) == 0) - zr = regOp(inst.a); - else if (inst.a.kind == IrOpKind::Inst && inst.b.kind == IrOpKind::Constant) - build.cmp(regOp(inst.a), tagOp(inst.b)); - else if (inst.a.kind == IrOpKind::Inst && inst.b.kind == IrOpKind::Inst) - build.cmp(regOp(inst.a), regOp(inst.b)); - else if (inst.a.kind == IrOpKind::Constant && inst.b.kind == IrOpKind::Inst) - build.cmp(regOp(inst.b), tagOp(inst.a)); + if (FFlag::LuauCodegenDirectCompare2) + { + RegisterA64 aReg = noreg; + RegisterA64 bReg = noreg; + + if (inst.a.kind == IrOpKind::Inst) + { + aReg = regOp(inst.a); + } + else if (inst.a.kind == IrOpKind::VmReg) + { + aReg = regs.allocTemp(KindA64::w); + AddressA64 addr = tempAddr(inst.a, offsetof(TValue, tt)); + build.ldr(aReg, addr); + } + else + { + CODEGEN_ASSERT(inst.a.kind == IrOpKind::Constant); + } + + if (inst.b.kind == IrOpKind::Inst) + { + bReg = regOp(inst.b); + } + else if (inst.b.kind == IrOpKind::VmReg) + { + bReg = regs.allocTemp(KindA64::w); + AddressA64 addr = tempAddr(inst.b, offsetof(TValue, tt)); + build.ldr(bReg, addr); + } + else + { + CODEGEN_ASSERT(inst.b.kind == IrOpKind::Constant); + } + + if (inst.a.kind == IrOpKind::Constant && tagOp(inst.a) == 0) + zr = bReg; + else if (inst.b.kind == IrOpKind::Constant && tagOp(inst.b) == 0) + zr = aReg; + else if (inst.b.kind == IrOpKind::Constant) + build.cmp(aReg, tagOp(inst.b)); + else if (inst.a.kind == IrOpKind::Constant) + build.cmp(bReg, tagOp(inst.a)); + else + build.cmp(aReg, bReg); + } else - CODEGEN_ASSERT(!"Unsupported instruction form"); + { + if (inst.a.kind == IrOpKind::Constant && tagOp(inst.a) == 0) + zr = regOp(inst.b); + else if (inst.b.kind == IrOpKind::Constant && tagOp(inst.b) == 0) + zr = regOp(inst.a); + else if (inst.a.kind == IrOpKind::Inst && inst.b.kind == IrOpKind::Constant) + build.cmp(regOp(inst.a), tagOp(inst.b)); + else if (inst.a.kind == IrOpKind::Inst && inst.b.kind == IrOpKind::Inst) + build.cmp(regOp(inst.a), regOp(inst.b)); + else if (inst.a.kind == IrOpKind::Constant && inst.b.kind == IrOpKind::Inst) + build.cmp(regOp(inst.b), tagOp(inst.a)); + else + CODEGEN_ASSERT(!"Unsupported instruction form"); + } if (isFallthroughBlock(blockOp(inst.d), next)) { @@ -1702,7 +1867,7 @@ void IrLoweringA64::lowerInst(IrInst& inst, uint32_t index, const IrBlock& next) RegisterA64 temp1w = castReg(KindA64::w, temp1); RegisterA64 temp2 = regs.allocTemp(KindA64::x); - CODEGEN_ASSERT(offsetof(LuaNode, key.value) == offsetof(LuaNode, key) && kOffsetOfTKeyTagNext >= 8 && kOffsetOfTKeyTagNext < 16); + static_assert(offsetof(LuaNode, key.value) == offsetof(LuaNode, key) && kOffsetOfTKeyTagNext >= 8 && kOffsetOfTKeyTagNext < 16); build.ldp(temp1, temp2, mem(regOp(inst.a), offsetof(LuaNode, key))); // load key.value into temp1 and key.tt (alongside other bits) into temp2 build.ubfx(temp2, temp2, (kOffsetOfTKeyTagNext - 8) * 8, kTKeyTagBits); // .tt is right before .next, and 8 bytes are skipped by ldp build.cmp(temp2, LUA_TSTRING); @@ -1832,7 +1997,7 @@ void IrLoweringA64::lowerInst(IrInst& inst, uint32_t index, const IrBlock& next) RegisterA64 temp1 = regs.allocTemp(KindA64::x); RegisterA64 temp2 = regs.allocTemp(KindA64::x); - CODEGEN_ASSERT(offsetof(global_State, totalbytes) == offsetof(global_State, GCthreshold) + 8); + static_assert(offsetof(global_State, totalbytes) == offsetof(global_State, GCthreshold) + sizeof(global_State::GCthreshold)); Label skip; build.ldp(temp1, temp2, mem(rGlobalState, offsetof(global_State, GCthreshold))); build.cmp(temp1, temp2); @@ -1938,7 +2103,7 @@ void IrLoweringA64::lowerInst(IrInst& inst, uint32_t index, const IrBlock& next) build.ldr(temp1, mem(rState, offsetof(lua_State, openupval))); build.cbz(temp1, skip); - // ra <= L->openuval->v + // ra <= L->openupval->v build.ldr(temp1, mem(temp1, offsetof(UpVal, v))); build.add(temp2, rBase, uint16_t(vmRegOp(inst.a) * sizeof(TValue))); build.cmp(temp2, temp1); @@ -2558,8 +2723,7 @@ void IrLoweringA64::lowerInst(IrInst& inst, uint32_t index, const IrBlock& next) valueTracker.afterInstLowering(inst, index); - if (FFlag::LuauCodeGenRegAutoSpillA64) - regs.currInstIdx = kInvalidInstIdx; + regs.currInstIdx = kInvalidInstIdx; regs.freeLastUseRegs(inst, index); regs.freeTempRegs(); @@ -2604,12 +2768,9 @@ void IrLoweringA64::finishFunction() build.b(helpers.updatePcAndContinueInVm); } - if (FFlag::LuauCodeGenUnassignedBcTargetAbort) - { - // An undefined instruction is placed after the function to be used as an aborting jump offset - function.endLocation = build.setLabel().location; - build.udf(); - } + // An undefined instruction is placed after the function to be used as an aborting jump offset + function.endLocation = build.setLabel().location; + build.udf(); if (stats) { diff --git a/CodeGen/src/IrLoweringX64.cpp b/CodeGen/src/IrLoweringX64.cpp index 5edb180f..f7253193 100644 --- a/CodeGen/src/IrLoweringX64.cpp +++ b/CodeGen/src/IrLoweringX64.cpp @@ -16,10 +16,8 @@ #include "lstate.h" #include "lgc.h" -LUAU_FASTFLAG(LuauCodeGenUnassignedBcTargetAbort) -LUAU_FASTFLAG(LuauCodeGenDirectBtest) LUAU_FASTFLAGVARIABLE(LuauCodeGenVBlendpdReorder) -LUAU_FASTFLAG(LuauCodeGenRegAutoSpillA64) +LUAU_FASTFLAG(LuauCodegenDirectCompare2) namespace Luau { @@ -37,7 +35,7 @@ IrLoweringX64::IrLoweringX64(AssemblyBuilderX64& build, ModuleHelpers& helpers, , valueTracker(function) , exitHandlerMap(~0u) { - valueTracker.setRestoreCallack( + valueTracker.setRestoreCallback( ®s, [](void* context, IrInst& inst) { @@ -680,7 +678,7 @@ void IrLoweringX64::lowerInst(IrInst& inst, uint32_t index, const IrBlock& next) // If arg == 0 then tmp1 is 0 and mask-bit is 0, result is 0 // If arg > 0 then tmp1 is 0 and mask-bit is 1, result is 1 if (FFlag::LuauCodeGenVBlendpdReorder) - build.vblendvpd(inst.regX64, tmp1.reg, inst.regX64, build.f64x2(1, 1)); + build.vblendvpd(inst.regX64, tmp1.reg, build.f64x2(1, 1), inst.regX64); else build.vblendvpd_DEPRECATED(inst.regX64, tmp1.reg, build.f64x2(1, 1), inst.regX64); break; @@ -701,14 +699,14 @@ void IrLoweringX64::lowerInst(IrInst& inst, uint32_t index, const IrBlock& next) if (inst.a.kind == IrOpKind::Inst) if (FFlag::LuauCodeGenVBlendpdReorder) - build.vblendvpd(inst.regX64, regOp(inst.a), tmp.reg, memRegDoubleOp(inst.b)); + build.vblendvpd(inst.regX64, regOp(inst.a), memRegDoubleOp(inst.b), tmp.reg); else build.vblendvpd_DEPRECATED(inst.regX64, regOp(inst.a), memRegDoubleOp(inst.b), tmp.reg); else { build.vmovsd(inst.regX64, memRegDoubleOp(inst.a)); if (FFlag::LuauCodeGenVBlendpdReorder) - build.vblendvpd(inst.regX64, inst.regX64, tmp.reg, memRegDoubleOp(inst.b)); + build.vblendvpd(inst.regX64, inst.regX64, memRegDoubleOp(inst.b), tmp.reg); else build.vblendvpd_DEPRECATED(inst.regX64, inst.regX64, memRegDoubleOp(inst.b), tmp.reg); } @@ -832,7 +830,7 @@ void IrLoweringX64::lowerInst(IrInst& inst, uint32_t index, const IrBlock& next) // TODO: if we have a single user which is a STORE_INT, we are missing the opportunity to write directly to target inst.regX64 = regs.allocRegOrReuse(SizeX64::dword, index, {inst.a, inst.b}); - Label saveone, savezero, exit; + Label saveOne, saveZero, exit; if (inst.a.kind == IrOpKind::Constant) { @@ -842,29 +840,29 @@ void IrLoweringX64::lowerInst(IrInst& inst, uint32_t index, const IrBlock& next) else { build.cmp(regOp(inst.a), LUA_TNIL); - build.jcc(ConditionX64::Equal, saveone); + build.jcc(ConditionX64::Equal, saveOne); build.cmp(regOp(inst.a), LUA_TBOOLEAN); - build.jcc(ConditionX64::NotEqual, savezero); + build.jcc(ConditionX64::NotEqual, saveZero); } if (inst.b.kind == IrOpKind::Constant) { // If value is 1, we fallthrough to storing 0 if (intOp(inst.b) == 0) - build.jmp(saveone); + build.jmp(saveOne); } else { build.cmp(regOp(inst.b), 0); - build.jcc(ConditionX64::Equal, saveone); + build.jcc(ConditionX64::Equal, saveOne); } - build.setLabel(savezero); + build.setLabel(saveZero); build.mov(inst.regX64, 0); build.jmp(exit); - build.setLabel(saveone); + build.setLabel(saveOne); build.mov(inst.regX64, 1); build.setLabel(exit); @@ -872,8 +870,6 @@ void IrLoweringX64::lowerInst(IrInst& inst, uint32_t index, const IrBlock& next) } case IrCmd::CMP_INT: { - CODEGEN_ASSERT(FFlag::LuauCodeGenDirectBtest); - // Cannot reuse operand registers as a target because we have to modify it before the comparison inst.regX64 = regs.allocReg(SizeX64::dword, index); @@ -921,6 +917,112 @@ void IrLoweringX64::lowerInst(IrInst& inst, uint32_t index, const IrBlock& next) inst.regX64 = regs.takeReg(eax, index); break; } + case IrCmd::CMP_TAG: + { + CODEGEN_ASSERT(FFlag::LuauCodegenDirectCompare2); + // Cannot reuse operand registers as a target because we have to modify it before the comparison + inst.regX64 = regs.allocReg(SizeX64::dword, index); + + // We are going to operate on byte register, those do not clear high bits on write + build.xor_(inst.regX64, inst.regX64); + + IrCondition cond = conditionOp(inst.c); + CODEGEN_ASSERT(cond == IrCondition::Equal || cond == IrCondition::NotEqual); + ConditionX64 condX64 = getConditionInt(cond); + + if (tagOp(inst.b) == LUA_TNIL && inst.a.kind == IrOpKind::Inst) + build.test(regOp(inst.a), regOp(inst.a)); + else + build.cmp(memRegTagOp(inst.a), tagOp(inst.b)); + + build.setcc(condX64, byteReg(inst.regX64)); + + break; + } + case IrCmd::CMP_SPLIT_TVALUE: + { + CODEGEN_ASSERT(FFlag::LuauCodegenDirectCompare2); + // Cannot reuse operand registers as a target because we have to modify it before the comparison + inst.regX64 = regs.allocReg(SizeX64::dword, index); + + // Second operand of this instruction must be a constant + // Without a constant type, we wouldn't know the correct way to compare the values at lowering time + CODEGEN_ASSERT(inst.b.kind == IrOpKind::Constant); + + // We are going to operate on byte registers, those do not clear high bits on write + build.xor_(inst.regX64, inst.regX64); + + IrCondition cond = conditionOp(inst.e); + CODEGEN_ASSERT(cond == IrCondition::Equal || cond == IrCondition::NotEqual); + + // Check tag equality first + ScopedRegX64 tmp1{regs, SizeX64::byte}; + + if (inst.a.kind != IrOpKind::Constant) + { + build.cmp(regOp(inst.a), tagOp(inst.b)); + build.setcc(getConditionInt(cond), byteReg(tmp1.reg)); + } + else + { + // Constant folding had to handle different constant tags + CODEGEN_ASSERT(tagOp(inst.a) == tagOp(inst.b)); + } + + if (tagOp(inst.b) == LUA_TBOOLEAN) + { + if (inst.c.kind == IrOpKind::Constant) + build.cmp(regOp(inst.d), intOp(inst.c)); // swapped arguments + else if (inst.d.kind == IrOpKind::Constant) + build.cmp(regOp(inst.c), intOp(inst.d)); + else + build.cmp(regOp(inst.c), regOp(inst.d)); + + build.setcc(getConditionInt(cond), byteReg(inst.regX64)); + } + else if (tagOp(inst.b) == LUA_TSTRING) + { + build.cmp(regOp(inst.c), regOp(inst.d)); + build.setcc(getConditionInt(cond), byteReg(inst.regX64)); + } + else if (tagOp(inst.b) == LUA_TNUMBER) + { + if (inst.c.kind == IrOpKind::Constant) + build.vucomisd(regOp(inst.d), memRegDoubleOp(inst.c)); // swapped arguments + else if (inst.d.kind == IrOpKind::Constant) + build.vucomisd(regOp(inst.c), memRegDoubleOp(inst.d)); + else + build.vucomisd(regOp(inst.c), regOp(inst.d)); + + ScopedRegX64 tmp2{regs, SizeX64::dword}; + + if (cond == IrCondition::Equal) + { + build.mov(tmp2.reg, 0); + build.setcc(ConditionX64::NotParity, byteReg(inst.regX64)); + build.cmov(ConditionX64::NotEqual, inst.regX64, tmp2.reg); + } + else + { + build.mov(tmp2.reg, 1); + build.setcc(ConditionX64::Parity, byteReg(inst.regX64)); + build.cmov(ConditionX64::NotEqual, inst.regX64, tmp2.reg); + } + } + else + { + CODEGEN_ASSERT(!"unsupported type tag in CMP_SPLIT_TVALUE"); + } + + if (inst.a.kind != IrOpKind::Constant) + { + if (cond == IrCondition::Equal) + build.and_(byteReg(inst.regX64), byteReg(tmp1.reg)); + else + build.or_(byteReg(inst.regX64), byteReg(tmp1.reg)); + } + break; + } case IrCmd::JUMP: jumpOrAbortOnUndef(inst.a, next); break; @@ -1688,7 +1790,7 @@ void IrLoweringX64::lowerInst(IrInst& inst, uint32_t index, const IrBlock& next) build.test(tmp1.reg, tmp1.reg); build.jcc(ConditionX64::Zero, next); - // ra <= L->openuval->v + // ra <= L->openupval->v build.lea(tmp2.reg, addr[rBase + vmRegOp(inst.a) * sizeof(TValue)]); build.cmp(tmp2.reg, qword[tmp1.reg + offsetof(UpVal, v)]); build.jcc(ConditionX64::Above, next); @@ -2253,8 +2355,7 @@ void IrLoweringX64::lowerInst(IrInst& inst, uint32_t index, const IrBlock& next) valueTracker.afterInstLowering(inst, index); - if (FFlag::LuauCodeGenRegAutoSpillA64) - regs.currInstIdx = kInvalidInstIdx; + regs.currInstIdx = kInvalidInstIdx; regs.freeLastUseRegs(inst, index); } @@ -2298,12 +2399,9 @@ void IrLoweringX64::finishFunction() build.jmp(helpers.updatePcAndContinueInVm); } - if (FFlag::LuauCodeGenUnassignedBcTargetAbort) - { - // An undefined instruction is placed after the function to be used as an aborting jump offset - function.endLocation = build.setLabel().location; - build.ud2(); - } + // An undefined instruction is placed after the function to be used as an aborting jump offset + function.endLocation = build.setLabel().location; + build.ud2(); if (stats) { diff --git a/CodeGen/src/IrRegAllocA64.cpp b/CodeGen/src/IrRegAllocA64.cpp index ec02fdfb..2ffa90c8 100644 --- a/CodeGen/src/IrRegAllocA64.cpp +++ b/CodeGen/src/IrRegAllocA64.cpp @@ -11,7 +11,6 @@ #include LUAU_FASTFLAGVARIABLE(DebugCodegenChaosA64) -LUAU_FASTFLAGVARIABLE(LuauCodeGenRegAutoSpillA64) namespace Luau { @@ -147,20 +146,11 @@ RegisterA64 IrRegAllocA64::allocReg(KindA64 kind, uint32_t index) if (set.free == 0) { - if (FFlag::LuauCodeGenRegAutoSpillA64) + // Try to find and spill a register that is not used in the current instruction and has the furthest next use + if (uint32_t furthestUseTarget = findInstructionWithFurthestNextUse(set); furthestUseTarget != kInvalidInstIdx) { - // Try to find and spill a register that is not used in the current instruction and has the furthest next use - if (uint32_t furthestUseTarget = findInstructionWithFurthestNextUse(set); furthestUseTarget != kInvalidInstIdx) - { - CODEGEN_ASSERT(currInstIdx == index); - spill(set, index, furthestUseTarget); - CODEGEN_ASSERT(set.free != 0); - } - else - { - error = true; - return RegisterA64{kind, 0}; - } + spill(set, index, furthestUseTarget); + CODEGEN_ASSERT(set.free != 0); } else { @@ -186,19 +176,11 @@ RegisterA64 IrRegAllocA64::allocTemp(KindA64 kind) if (set.free == 0) { - if (FFlag::LuauCodeGenRegAutoSpillA64) + // Try to find and spill a register that is not used in the current instruction and has the furthest next use + if (uint32_t furthestUseTarget = findInstructionWithFurthestNextUse(set); furthestUseTarget != kInvalidInstIdx) { - // Try to find and spill a register that is not used in the current instruction and has the furthest next use - if (uint32_t furthestUseTarget = findInstructionWithFurthestNextUse(set); furthestUseTarget != kInvalidInstIdx) - { - spill(set, currInstIdx, furthestUseTarget); - CODEGEN_ASSERT(set.free != 0); - } - else - { - error = true; - return RegisterA64{kind, 0}; - } + spill(set, currInstIdx, furthestUseTarget); + CODEGEN_ASSERT(set.free != 0); } else { @@ -352,79 +334,16 @@ size_t IrRegAllocA64::spill(uint32_t index, std::initializer_list l while (regs) { - if (FFlag::LuauCodeGenRegAutoSpillA64) - { - int reg = 31 - countlz(regs); - - uint32_t targetInstIdx = set.defs[reg]; - - CODEGEN_ASSERT(targetInstIdx != kInvalidInstIdx); - CODEGEN_ASSERT(function.instructions[targetInstIdx].regA64.index == reg); - - spill(set, index, targetInstIdx); - - regs &= ~(1u << reg); - } - else - { - int reg = 31 - countlz(regs); - - uint32_t inst = set.defs[reg]; - CODEGEN_ASSERT(inst != kInvalidInstIdx); - - IrInst& def = function.instructions[inst]; - CODEGEN_ASSERT(def.regA64.index == reg); - CODEGEN_ASSERT(!def.reusedReg); - CODEGEN_ASSERT(!def.spilled); - CODEGEN_ASSERT(!def.needsReload); - - if (def.lastUse == index) - { - // instead of spilling the register to never reload it, we assume the register is not needed anymore - } - else if (getReloadAddress(function, def, /*limitToCurrentBlock*/ true).base != xzr) - { - // instead of spilling the register to stack, we can reload it from VM stack/constants - // we still need to record the spill for restore(start) to work - Spill s = {inst, def.regA64, -1}; - spills.push_back(s); - - def.needsReload = true; - - if (stats) - stats->spillsToRestore++; - } - else - { - int slot = allocSpill(freeSpillSlots, def.regA64.kind); - if (slot < 0) - { - slot = kInvalidSpill; - error = true; - } - - build.str(def.regA64, mem(sp, sSpillArea.data + slot * 8)); - - Spill s = {inst, def.regA64, int8_t(slot)}; - spills.push_back(s); - - def.spilled = true; - - if (stats) - { - stats->spillsToSlot++; - - if (slot != kInvalidSpill && unsigned(slot + 1) > stats->maxSpillSlotsUsed) - stats->maxSpillSlotsUsed = slot + 1; - } - } - - def.regA64 = noreg; - - regs &= ~(1u << reg); - set.free |= 1u << reg; - set.defs[reg] = kInvalidInstIdx; - } + int reg = 31 - countlz(regs); + + uint32_t targetInstIdx = set.defs[reg]; + + CODEGEN_ASSERT(targetInstIdx != kInvalidInstIdx); + CODEGEN_ASSERT(function.instructions[targetInstIdx].regA64.index == reg); + + spill(set, index, targetInstIdx); + + regs &= ~(1u << reg); } CODEGEN_ASSERT(set.free == set.base); @@ -486,8 +405,6 @@ void IrRegAllocA64::restoreReg(IrInst& inst) void IrRegAllocA64::spill(Set& set, uint32_t index, uint32_t targetInstIdx) { - CODEGEN_ASSERT(FFlag::LuauCodeGenRegAutoSpillA64); - IrInst& def = function.instructions[targetInstIdx]; int reg = def.regA64.index; @@ -544,8 +461,6 @@ void IrRegAllocA64::spill(Set& set, uint32_t index, uint32_t targetInstIdx) uint32_t IrRegAllocA64::findInstructionWithFurthestNextUse(Set& set) const { - CODEGEN_ASSERT(FFlag::LuauCodeGenRegAutoSpillA64); - if (currInstIdx == kInvalidInstIdx) return kInvalidInstIdx; diff --git a/CodeGen/src/IrRegAllocX64.cpp b/CodeGen/src/IrRegAllocX64.cpp index ce9bf19d..2aa65524 100644 --- a/CodeGen/src/IrRegAllocX64.cpp +++ b/CodeGen/src/IrRegAllocX64.cpp @@ -6,8 +6,6 @@ #include "EmitCommonX64.h" -LUAU_FASTFLAG(LuauCodeGenRegAutoSpillA64) - namespace Luau { namespace CodeGen @@ -395,11 +393,8 @@ OperandX64 IrRegAllocX64::getRestoreAddress(const IrInst& inst, IrOp restoreOp) uint32_t IrRegAllocX64::findInstructionWithFurthestNextUse(const std::array& regInstUsers) const { - if (FFlag::LuauCodeGenRegAutoSpillA64) - { - if (currInstIdx == kInvalidInstIdx) - return kInvalidInstIdx; - } + if (currInstIdx == kInvalidInstIdx) + return kInvalidInstIdx; uint32_t furthestUseTarget = kInvalidInstIdx; uint32_t furthestUseLocation = 0; diff --git a/CodeGen/src/IrTranslateBuiltins.cpp b/CodeGen/src/IrTranslateBuiltins.cpp index b4b7b73e..a7de2e99 100644 --- a/CodeGen/src/IrTranslateBuiltins.cpp +++ b/CodeGen/src/IrTranslateBuiltins.cpp @@ -8,8 +8,7 @@ #include -LUAU_FASTFLAGVARIABLE(LuauCodeGenDirectBtest) -LUAU_FASTFLAGVARIABLE(LuauCodeGenVectorLerp) +LUAU_FASTFLAGVARIABLE(LuauCodeGenVectorLerp2) LUAU_FASTFLAGVARIABLE(LuauCodeGenFMA) // TODO: when nresults is less than our actual result count, we can skip computing/writing unused results @@ -287,7 +286,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::LuauCodeGenVectorLerp || nparams < 3 || nresults > 1) + if (!FFlag::LuauCodeGenVectorLerp2 || nparams < 3 || nresults > 1) return {BuiltinImplType::None, -1}; IrOp arg1 = build.vmReg(arg); @@ -460,30 +459,9 @@ static BuiltinImplResult translateBuiltinBit32BinaryOp( if (btest) { - if (FFlag::LuauCodeGenDirectBtest) - { - IrOp value = build.inst(IrCmd::CMP_INT, res, build.constInt(0), build.cond(IrCondition::NotEqual)); - build.inst(IrCmd::STORE_INT, build.vmReg(ra), value); - build.inst(IrCmd::STORE_TAG, build.vmReg(ra), build.constTag(LUA_TBOOLEAN)); - } - else - { - IrOp falsey = build.block(IrBlockKind::Internal); - IrOp truthy = build.block(IrBlockKind::Internal); - IrOp exit = build.block(IrBlockKind::Internal); - build.inst(IrCmd::JUMP_CMP_INT, res, build.constInt(0), build.cond(IrCondition::Equal), falsey, truthy); - - build.beginBlock(falsey); - build.inst(IrCmd::STORE_INT, build.vmReg(ra), build.constInt(0)); - build.inst(IrCmd::JUMP, exit); - - build.beginBlock(truthy); - build.inst(IrCmd::STORE_INT, build.vmReg(ra), build.constInt(1)); - build.inst(IrCmd::JUMP, exit); - - build.beginBlock(exit); - build.inst(IrCmd::STORE_TAG, build.vmReg(ra), build.constTag(LUA_TBOOLEAN)); - } + IrOp value = build.inst(IrCmd::CMP_INT, res, build.constInt(0), build.cond(IrCondition::NotEqual)); + build.inst(IrCmd::STORE_INT, build.vmReg(ra), value); + build.inst(IrCmd::STORE_TAG, build.vmReg(ra), build.constTag(LUA_TBOOLEAN)); } else { diff --git a/CodeGen/src/IrTranslation.cpp b/CodeGen/src/IrTranslation.cpp index 40f584ed..8d4485c5 100644 --- a/CodeGen/src/IrTranslation.cpp +++ b/CodeGen/src/IrTranslation.cpp @@ -12,6 +12,8 @@ #include "lstate.h" #include "ltm.h" +LUAU_FASTFLAG(LuauCodegenDirectCompare2) + namespace Luau { namespace CodeGen @@ -268,11 +270,35 @@ void translateInstJumpxEqNil(IrBuilder& build, const Instruction* pc, int pcpos) build.beginBlock(next); } +void translateInstJumpxEqNilShortcut(IrBuilder& build, const Instruction* pc, int pcpos) +{ + CODEGEN_ASSERT(FFlag::LuauCodegenDirectCompare2); + int rr = LUAU_INSN_A(pc[2]); + + int ra = LUAU_INSN_A(*pc); + uint32_t aux = pc[1]; + bool not_ = LUAU_INSN_AUX_NOT(aux) != 0; + + IrOp next = build.blockAtInst(pcpos + 4); + + IrOp ta = build.inst(IrCmd::LOAD_TAG, build.vmReg(ra)); + + IrOp result = build.inst(IrCmd::CMP_TAG, ta, build.constTag(LUA_TNIL), build.cond(not_ ? IrCondition::NotEqual : IrCondition::Equal)); + + build.inst(IrCmd::STORE_TAG, build.vmReg(rr), build.constTag(LUA_TBOOLEAN)); + build.inst(IrCmd::STORE_INT, build.vmReg(rr), result); + build.inst(IrCmd::JUMP, next); + + // Fallthrough in original bytecode is implicit, so we start next internal block here + if (build.isInternalBlock(next)) + build.beginBlock(next); +} + void translateInstJumpxEqB(IrBuilder& build, const Instruction* pc, int pcpos) { int ra = LUAU_INSN_A(*pc); uint32_t aux = pc[1]; - bool not_ = (aux & 0x80000000) != 0; + bool not_ = LUAU_INSN_AUX_NOT(aux) != 0; IrOp target = build.blockAtInst(pcpos + 1 + LUAU_INSN_D(*pc)); IrOp next = build.blockAtInst(pcpos + 2); @@ -285,7 +311,34 @@ void translateInstJumpxEqB(IrBuilder& build, const Instruction* pc, int pcpos) build.beginBlock(checkValue); IrOp va = build.inst(IrCmd::LOAD_INT, build.vmReg(ra)); - build.inst(IrCmd::JUMP_CMP_INT, va, build.constInt(aux & 0x1), build.cond(IrCondition::Equal), not_ ? next : target, not_ ? target : next); + build.inst(IrCmd::JUMP_CMP_INT, va, build.constInt(LUAU_INSN_AUX_KB(aux)), build.cond(IrCondition::Equal), not_ ? next : target, not_ ? target : next); + + // Fallthrough in original bytecode is implicit, so we start next internal block here + if (build.isInternalBlock(next)) + build.beginBlock(next); +} + +void translateInstJumpxEqBShortcut(IrBuilder& build, const Instruction* pc, int pcpos) +{ + CODEGEN_ASSERT(FFlag::LuauCodegenDirectCompare2); + int rr = LUAU_INSN_A(pc[2]); + + int ra = LUAU_INSN_A(*pc); + uint32_t aux = pc[1]; + bool not_ = LUAU_INSN_AUX_NOT(aux) != 0; + + IrOp next = build.blockAtInst(pcpos + 4); + + IrOp ta = build.inst(IrCmd::LOAD_TAG, build.vmReg(ra)); + IrOp va = build.inst(IrCmd::LOAD_INT, build.vmReg(ra)); + IrOp vb = build.constInt(LUAU_INSN_AUX_KB(aux)); + + IrOp result = + build.inst(IrCmd::CMP_SPLIT_TVALUE, ta, build.constTag(LUA_TBOOLEAN), va, vb, build.cond(not_ ? IrCondition::NotEqual : IrCondition::Equal)); + + build.inst(IrCmd::STORE_TAG, build.vmReg(rr), build.constTag(LUA_TBOOLEAN)); + build.inst(IrCmd::STORE_INT, build.vmReg(rr), result); + build.inst(IrCmd::JUMP, next); // Fallthrough in original bytecode is implicit, so we start next internal block here if (build.isInternalBlock(next)) @@ -296,7 +349,7 @@ void translateInstJumpxEqN(IrBuilder& build, const Instruction* pc, int pcpos) { int ra = LUAU_INSN_A(*pc); uint32_t aux = pc[1]; - bool not_ = (aux & 0x80000000) != 0; + bool not_ = LUAU_INSN_AUX_NOT(aux) != 0; IrOp target = build.blockAtInst(pcpos + 1 + LUAU_INSN_D(*pc)); IrOp next = build.blockAtInst(pcpos + 2); @@ -310,7 +363,7 @@ void translateInstJumpxEqN(IrBuilder& build, const Instruction* pc, int pcpos) IrOp va = build.inst(IrCmd::LOAD_DOUBLE, build.vmReg(ra)); CODEGEN_ASSERT(build.function.proto); - TValue protok = build.function.proto->k[aux & 0xffffff]; + TValue protok = build.function.proto->k[LUAU_INSN_AUX_KV(aux)]; CODEGEN_ASSERT(protok.tt == LUA_TNUMBER); IrOp vb = build.constDouble(protok.value.n); @@ -322,11 +375,43 @@ void translateInstJumpxEqN(IrBuilder& build, const Instruction* pc, int pcpos) build.beginBlock(next); } +void translateInstJumpxEqNShortcut(IrBuilder& build, const Instruction* pc, int pcpos) +{ + CODEGEN_ASSERT(FFlag::LuauCodegenDirectCompare2); + int rr = LUAU_INSN_A(pc[2]); + + int ra = LUAU_INSN_A(*pc); + uint32_t aux = pc[1]; + bool not_ = LUAU_INSN_AUX_NOT(aux) != 0; + + IrOp next = build.blockAtInst(pcpos + 4); + + IrOp ta = build.inst(IrCmd::LOAD_TAG, build.vmReg(ra)); + IrOp va = build.inst(IrCmd::LOAD_DOUBLE, build.vmReg(ra)); + + CODEGEN_ASSERT(build.function.proto); + TValue protok = build.function.proto->k[LUAU_INSN_AUX_KV(aux)]; + + CODEGEN_ASSERT(protok.tt == LUA_TNUMBER); + IrOp vb = build.constDouble(protok.value.n); + + IrOp result = + build.inst(IrCmd::CMP_SPLIT_TVALUE, ta, build.constTag(LUA_TNUMBER), va, vb, build.cond(not_ ? IrCondition::NotEqual : IrCondition::Equal)); + + build.inst(IrCmd::STORE_TAG, build.vmReg(rr), build.constTag(LUA_TBOOLEAN)); + build.inst(IrCmd::STORE_INT, build.vmReg(rr), result); + build.inst(IrCmd::JUMP, next); + + // Fallthrough in original bytecode is implicit, so we start next internal block here + if (build.isInternalBlock(next)) + build.beginBlock(next); +} + void translateInstJumpxEqS(IrBuilder& build, const Instruction* pc, int pcpos) { int ra = LUAU_INSN_A(*pc); uint32_t aux = pc[1]; - bool not_ = (aux & 0x80000000) != 0; + bool not_ = LUAU_INSN_AUX_NOT(aux) != 0; IrOp target = build.blockAtInst(pcpos + 1 + LUAU_INSN_D(*pc)); IrOp next = build.blockAtInst(pcpos + 2); @@ -337,7 +422,7 @@ void translateInstJumpxEqS(IrBuilder& build, const Instruction* pc, int pcpos) build.beginBlock(checkValue); IrOp va = build.inst(IrCmd::LOAD_POINTER, build.vmReg(ra)); - IrOp vb = build.inst(IrCmd::LOAD_POINTER, build.vmConst(aux & 0xffffff)); + IrOp vb = build.inst(IrCmd::LOAD_POINTER, build.vmConst(LUAU_INSN_AUX_KV(aux))); build.inst(IrCmd::JUMP_EQ_POINTER, va, vb, not_ ? next : target, not_ ? target : next); @@ -346,6 +431,33 @@ void translateInstJumpxEqS(IrBuilder& build, const Instruction* pc, int pcpos) build.beginBlock(next); } +void translateInstJumpxEqSShortcut(IrBuilder& build, const Instruction* pc, int pcpos) +{ + CODEGEN_ASSERT(FFlag::LuauCodegenDirectCompare2); + int rr = LUAU_INSN_A(pc[2]); + + int ra = LUAU_INSN_A(*pc); + uint32_t aux = pc[1]; + bool not_ = LUAU_INSN_AUX_NOT(aux) != 0; + + IrOp next = build.blockAtInst(pcpos + 4); + + IrOp ta = build.inst(IrCmd::LOAD_TAG, build.vmReg(ra)); + IrOp va = build.inst(IrCmd::LOAD_POINTER, build.vmReg(ra)); + IrOp vb = build.inst(IrCmd::LOAD_POINTER, build.vmConst(LUAU_INSN_AUX_KV(aux))); + + IrOp result = + build.inst(IrCmd::CMP_SPLIT_TVALUE, ta, build.constTag(LUA_TSTRING), va, vb, build.cond(not_ ? IrCondition::NotEqual : IrCondition::Equal)); + + build.inst(IrCmd::STORE_TAG, build.vmReg(rr), build.constTag(LUA_TBOOLEAN)); + build.inst(IrCmd::STORE_INT, build.vmReg(rr), result); + build.inst(IrCmd::JUMP, next); + + // Fallthrough in original bytecode is implicit, so we start next internal block here + if (build.isInternalBlock(next)) + build.beginBlock(next); +} + static void translateInstBinaryNumeric(IrBuilder& build, int ra, int rb, int rc, IrOp opb, IrOp opc, int pcpos, TMS tm) { BytecodeTypes bcTypes = build.function.getBytecodeTypesAt(pcpos); diff --git a/CodeGen/src/IrTranslation.h b/CodeGen/src/IrTranslation.h index 662799b3..e5492049 100644 --- a/CodeGen/src/IrTranslation.h +++ b/CodeGen/src/IrTranslation.h @@ -30,9 +30,13 @@ void translateInstJumpIfEq(IrBuilder& build, const Instruction* pc, int pcpos, b void translateInstJumpIfCond(IrBuilder& build, const Instruction* pc, int pcpos, IrCondition cond); void translateInstJumpX(IrBuilder& build, const Instruction* pc, int pcpos); void translateInstJumpxEqNil(IrBuilder& build, const Instruction* pc, int pcpos); +void translateInstJumpxEqNilShortcut(IrBuilder& build, const Instruction* pc, int pcpos); void translateInstJumpxEqB(IrBuilder& build, const Instruction* pc, int pcpos); +void translateInstJumpxEqBShortcut(IrBuilder& build, const Instruction* pc, int pcpos); void translateInstJumpxEqN(IrBuilder& build, const Instruction* pc, int pcpos); +void translateInstJumpxEqNShortcut(IrBuilder& build, const Instruction* pc, int pcpos); void translateInstJumpxEqS(IrBuilder& build, const Instruction* pc, int pcpos); +void translateInstJumpxEqSShortcut(IrBuilder& build, const Instruction* pc, int pcpos); void translateInstBinary(IrBuilder& build, const Instruction* pc, int pcpos, TMS tm); void translateInstBinaryK(IrBuilder& build, const Instruction* pc, int pcpos, TMS tm); void translateInstBinaryRK(IrBuilder& build, const Instruction* pc, int pcpos, TMS tm); diff --git a/CodeGen/src/IrUtils.cpp b/CodeGen/src/IrUtils.cpp index d8b194ab..3f22416c 100644 --- a/CodeGen/src/IrUtils.cpp +++ b/CodeGen/src/IrUtils.cpp @@ -16,7 +16,7 @@ #include #include -LUAU_FASTFLAG(LuauCodeGenDirectBtest) +LUAU_FASTFLAG(LuauCodegenDirectCompare2) namespace Luau { @@ -199,6 +199,8 @@ IrValueKind getCmdValueKind(IrCmd cmd) case IrCmd::NOT_ANY: case IrCmd::CMP_ANY: case IrCmd::CMP_INT: + case IrCmd::CMP_TAG: + case IrCmd::CMP_SPLIT_TVALUE: return IrValueKind::Int; case IrCmd::JUMP: case IrCmd::JUMP_IF_TRUTHY: @@ -796,8 +798,6 @@ void foldConstants(IrBuilder& build, IrFunction& function, IrBlock& block, uint3 } break; case IrCmd::CMP_INT: - CODEGEN_ASSERT(FFlag::LuauCodeGenDirectBtest); - if (inst.a.kind == IrOpKind::Constant && inst.b.kind == IrOpKind::Constant) { if (compare(function.intOp(inst.a), function.intOp(inst.b), conditionOp(inst.c))) @@ -806,6 +806,84 @@ void foldConstants(IrBuilder& build, IrFunction& function, IrBlock& block, uint3 substitute(function, inst, build.constInt(0)); } break; + case IrCmd::CMP_TAG: + CODEGEN_ASSERT(FFlag::LuauCodegenDirectCompare2); + + if (inst.a.kind == IrOpKind::Constant && inst.b.kind == IrOpKind::Constant) + { + IrCondition cond = conditionOp(inst.c); + CODEGEN_ASSERT(cond == IrCondition::Equal || cond == IrCondition::NotEqual); + + if (cond == IrCondition::Equal) + substitute(function, inst, build.constInt(function.tagOp(inst.a) == function.tagOp(inst.b) ? 1 : 0)); + else + substitute(function, inst, build.constInt(function.tagOp(inst.a) != function.tagOp(inst.b) ? 1 : 0)); + } + break; + case IrCmd::CMP_SPLIT_TVALUE: + { + CODEGEN_ASSERT(FFlag::LuauCodegenDirectCompare2); + CODEGEN_ASSERT(inst.b.kind == IrOpKind::Constant); + + IrCondition cond = conditionOp(inst.e); + CODEGEN_ASSERT(cond == IrCondition::Equal || cond == IrCondition::NotEqual); + + if (cond == IrCondition::Equal) + { + if (inst.a.kind == IrOpKind::Constant && function.tagOp(inst.a) != function.tagOp(inst.b)) + { + substitute(function, inst, build.constInt(0)); + } + else if (inst.c.kind == IrOpKind::Constant && inst.d.kind == IrOpKind::Constant) + { + // If the tag is a constant, this means previous condition has failed because tags are the same + bool knownSameTag = inst.a.kind == IrOpKind::Constant; + bool sameValue = false; + + if (function.tagOp(inst.b) == LUA_TBOOLEAN) + sameValue = compare(function.intOp(inst.c), function.intOp(inst.d), IrCondition::Equal); + else if (function.tagOp(inst.b) == LUA_TNUMBER) + sameValue = compare(function.doubleOp(inst.c), function.doubleOp(inst.d), IrCondition::Equal); + else + CODEGEN_ASSERT(!"unsupported type"); + + if (knownSameTag && sameValue) + substitute(function, inst, build.constInt(1)); + else if (sameValue) + replace(function, block, index, {IrCmd::CMP_TAG, inst.a, inst.b, inst.e}); + else + substitute(function, inst, build.constInt(0)); + } + } + else + { + if (inst.a.kind == IrOpKind::Constant && function.tagOp(inst.a) != function.tagOp(inst.b)) + { + substitute(function, inst, build.constInt(1)); + } + else if (inst.c.kind == IrOpKind::Constant && inst.d.kind == IrOpKind::Constant) + { + // If the tag is a constant, this means previous condition has failed because tags are the same + bool knownSameTag = inst.a.kind == IrOpKind::Constant; + bool differentValue = false; + + if (function.tagOp(inst.b) == LUA_TBOOLEAN) + differentValue = compare(function.intOp(inst.c), function.intOp(inst.d), IrCondition::NotEqual); + else if (function.tagOp(inst.b) == LUA_TNUMBER) + differentValue = compare(function.doubleOp(inst.c), function.doubleOp(inst.d), IrCondition::NotEqual); + else + CODEGEN_ASSERT(!"unsupported type"); + + if (differentValue) + substitute(function, inst, build.constInt(1)); + else if (knownSameTag) + substitute(function, inst, build.constInt(0)); + else + replace(function, block, index, {IrCmd::CMP_TAG, inst.a, inst.b, inst.e}); + } + } + } + break; case IrCmd::JUMP_EQ_TAG: if (inst.a.kind == IrOpKind::Constant && inst.b.kind == IrOpKind::Constant) { diff --git a/CodeGen/src/IrValueLocationTracking.cpp b/CodeGen/src/IrValueLocationTracking.cpp index 4a4d148f..3c177416 100644 --- a/CodeGen/src/IrValueLocationTracking.cpp +++ b/CodeGen/src/IrValueLocationTracking.cpp @@ -3,8 +3,6 @@ #include "Luau/IrUtils.h" -LUAU_FASTFLAGVARIABLE(LuauCodeGenRestoreFromSplitStore) - namespace Luau { namespace CodeGen @@ -16,7 +14,7 @@ IrValueLocationTracking::IrValueLocationTracking(IrFunction& function) vmRegValue.fill(kInvalidInstIdx); } -void IrValueLocationTracking::setRestoreCallack(void* context, void (*callback)(void* context, IrInst& inst)) +void IrValueLocationTracking::setRestoreCallback(void* context, void (*callback)(void* context, IrInst& inst)) { restoreCallbackCtx = context; restoreCallback = callback; @@ -99,8 +97,10 @@ void IrValueLocationTracking::beforeInstLowering(IrInst& inst) case IrCmd::LOAD_FLOAT: case IrCmd::LOAD_TVALUE: case IrCmd::CMP_ANY: + case IrCmd::CMP_TAG: case IrCmd::JUMP_IF_TRUTHY: case IrCmd::JUMP_IF_FALSY: + case IrCmd::JUMP_EQ_TAG: case IrCmd::SET_TABLE: case IrCmd::SET_UPVALUE: case IrCmd::INTERRUPT: @@ -131,7 +131,6 @@ void IrValueLocationTracking::beforeInstLowering(IrInst& inst) case IrCmd::MOD_NUM: case IrCmd::MIN_NUM: case IrCmd::MAX_NUM: - case IrCmd::JUMP_EQ_TAG: case IrCmd::JUMP_CMP_NUM: case IrCmd::FLOOR_NUM: case IrCmd::CEIL_NUM: @@ -176,12 +175,9 @@ void IrValueLocationTracking::afterInstLowering(IrInst& inst, uint32_t instIdx) recordRestoreOp(inst.b.index, inst.a); break; case IrCmd::STORE_SPLIT_TVALUE: - if (FFlag::LuauCodeGenRestoreFromSplitStore) - { - // If this is not the last use of the stored value, we can restore it from this new location - if (inst.c.kind == IrOpKind::Inst && function.instOp(inst.c).lastUse != instIdx) - recordRestoreOp(inst.c.index, inst.a); - } + // If this is not the last use of the stored value, we can restore it from this new location + if (inst.c.kind == IrOpKind::Inst && function.instOp(inst.c).lastUse != instIdx) + recordRestoreOp(inst.c.index, inst.a); break; default: break; diff --git a/CodeGen/src/IrValueLocationTracking.h b/CodeGen/src/IrValueLocationTracking.h index 5ce9c23f..4210d5df 100644 --- a/CodeGen/src/IrValueLocationTracking.h +++ b/CodeGen/src/IrValueLocationTracking.h @@ -14,7 +14,7 @@ struct IrValueLocationTracking { IrValueLocationTracking(IrFunction& function); - void setRestoreCallack(void* context, void (*callback)(void* context, IrInst& inst)); + void setRestoreCallback(void* context, void (*callback)(void* context, IrInst& inst)); void beforeInstLowering(IrInst& inst); void afterInstLowering(IrInst& inst, uint32_t instIdx); diff --git a/CodeGen/src/OptimizeConstProp.cpp b/CodeGen/src/OptimizeConstProp.cpp index 6901bb73..10f57bbe 100644 --- a/CodeGen/src/OptimizeConstProp.cpp +++ b/CodeGen/src/OptimizeConstProp.cpp @@ -7,6 +7,8 @@ #include "Luau/IrUtils.h" #include "lua.h" +#include "lobject.h" +#include "lstate.h" #include #include @@ -21,7 +23,9 @@ LUAU_FASTINTVARIABLE(LuauCodeGenReuseSlotLimit, 64) LUAU_FASTINTVARIABLE(LuauCodeGenReuseUdataTagLimit, 64) LUAU_FASTINTVARIABLE(LuauCodeGenLiveSlotReuseLimit, 8) LUAU_FASTFLAGVARIABLE(DebugLuauAbortingChecks) -LUAU_FASTFLAG(LuauCodeGenDirectBtest) +LUAU_FASTFLAG(LuauCodegenDirectCompare2) +LUAU_FASTFLAGVARIABLE(LuauCodegenNilStoreInvalidateValue2) +LUAU_FASTFLAGVARIABLE(LuauCodegenStorePriority) namespace Luau { @@ -59,6 +63,42 @@ struct NumberedInstruction uint32_t finishPos = 0; }; +static uint8_t tryGetTagForTypename(std::string_view name, bool forTypeof) +{ + CODEGEN_ASSERT(FFlag::LuauCodegenDirectCompare2); + + if (name == "nil") + return LUA_TNIL; + + if (name == "boolean") + return LUA_TBOOLEAN; + + if (name == "number") + return LUA_TNUMBER; + + // typeof(vector) can be changed by environment + // TODO: support the environment option + if (name == "vector" && !forTypeof) + return LUA_TVECTOR; + + if (name == "string") + return LUA_TSTRING; + + if (name == "table") + return LUA_TTABLE; + + if (name == "function") + return LUA_TFUNCTION; + + if (name == "thread") + return LUA_TTHREAD; + + if (name == "buffer") + return LUA_TBUFFER; + + return 0xff; +} + // Data we know about the current VM state struct ConstPropState { @@ -691,9 +731,19 @@ static void constPropInInst(ConstPropState& state, IrBuilder& build, IrFunction& std::tie(activeLoadCmd, activeLoadValue) = state.getPreviousVersionedLoadForTag(value, source); if (state.tryGetTag(source) == value) + { kill(function, inst); + } else + { state.saveTag(source, value); + + // Storing 'nil' implicitly kills the known value in the register + // This is required for dead store elimination to correctly establish tag+value pairs as it treats 'nil' write as a full TValue + // store + if (FFlag::LuauCodegenNilStoreInvalidateValue2 && value == LUA_TNIL) + state.invalidateValue(source); + } } else { @@ -816,7 +866,7 @@ static void constPropInInst(ConstPropState& state, IrBuilder& build, IrFunction& // If we know the tag, we can try extracting the value from a register used by LOAD_TVALUE // To do that, we have to ensure that the register link of the source value is still valid - if (tag != 0xff && state.tryGetRegLink(inst.b) != nullptr) + if (tag != 0xff && (!FFlag::LuauCodegenStorePriority || value.kind == IrOpKind::None) && state.tryGetRegLink(inst.b) != nullptr) { if (IrInst* arg = function.asInstOp(inst.b); arg && arg->cmd == IrCmd::LOAD_TVALUE && arg->a.kind == IrOpKind::VmReg) { @@ -1332,13 +1382,84 @@ static void constPropInInst(ConstPropState& state, IrBuilder& build, IrFunction& state.substituteOrRecord(inst, index); break; case IrCmd::CMP_INT: - CODEGEN_ASSERT(FFlag::LuauCodeGenDirectBtest); break; case IrCmd::CMP_ANY: state.invalidateUserCall(); break; + case IrCmd::CMP_TAG: + CODEGEN_ASSERT(FFlag::LuauCodegenDirectCompare2); + break; + case IrCmd::CMP_SPLIT_TVALUE: + CODEGEN_ASSERT(FFlag::LuauCodegenDirectCompare2); + + if (function.proto) + { + uint8_t tagA = inst.a.kind == IrOpKind::Constant ? function.tagOp(inst.a) : state.tryGetTag(inst.a); + uint8_t tagB = inst.b.kind == IrOpKind::Constant ? function.tagOp(inst.b) : state.tryGetTag(inst.b); + + // Try to find pattern like type(x) == 'tagname' or typeof(x) == 'tagname' + if (tagA == LUA_TSTRING && tagB == LUA_TSTRING && inst.c.kind == IrOpKind::Inst && inst.d.kind == IrOpKind::Inst) + { + const IrInst& lhs = function.instOp(inst.c); + const IrInst& rhs = function.instOp(inst.d); + + if (rhs.cmd == IrCmd::LOAD_POINTER && rhs.a.kind == IrOpKind::VmConst) + { + TValue name = function.proto->k[vmConstOp(rhs.a)]; + CODEGEN_ASSERT(name.tt == LUA_TSTRING); + std::string_view nameStr{svalue(&name), tsvalue(&name)->len}; + + if (int tag = tryGetTagForTypename(nameStr, lhs.cmd == IrCmd::GET_TYPEOF); tag != 0xff) + { + if (lhs.cmd == IrCmd::GET_TYPE) + { + replace(function, block, index, {IrCmd::CMP_TAG, lhs.a, build.constTag(tag), inst.e}); + foldConstants(build, function, block, index); + } + else if (lhs.cmd == IrCmd::GET_TYPEOF) + { + replace(function, block, index, {IrCmd::CMP_TAG, lhs.a, build.constTag(tag), inst.e}); + foldConstants(build, function, block, index); + } + } + } + } + } + break; case IrCmd::JUMP: + break; case IrCmd::JUMP_EQ_POINTER: + if (FFlag::LuauCodegenDirectCompare2 && function.proto) + { + // Try to find pattern like type(x) == 'tagname' or typeof(x) == 'tagname' + if (inst.a.kind == IrOpKind::Inst && inst.b.kind == IrOpKind::Inst) + { + const IrInst& lhs = function.instOp(inst.a); + const IrInst& rhs = function.instOp(inst.b); + + if (rhs.cmd == IrCmd::LOAD_POINTER && rhs.a.kind == IrOpKind::VmConst) + { + TValue name = function.proto->k[vmConstOp(rhs.a)]; + CODEGEN_ASSERT(name.tt == LUA_TSTRING); + std::string_view nameStr{svalue(&name), tsvalue(&name)->len}; + + if (int tag = tryGetTagForTypename(nameStr, lhs.cmd == IrCmd::GET_TYPEOF); tag != 0xff) + { + if (lhs.cmd == IrCmd::GET_TYPE) + { + replace(function, block, index, {IrCmd::JUMP_EQ_TAG, lhs.a, build.constTag(tag), inst.c, inst.d}); + foldConstants(build, function, block, index); + } + else if (lhs.cmd == IrCmd::GET_TYPEOF) + { + replace(function, block, index, {IrCmd::JUMP_EQ_TAG, lhs.a, build.constTag(tag), inst.c, inst.d}); + foldConstants(build, function, block, index); + } + } + } + } + } + break; case IrCmd::JUMP_SLOT_MATCH: case IrCmd::TABLE_LEN: break; @@ -1779,7 +1900,18 @@ static void tryCreateLinearBlock(IrBuilder& build, std::vector& visited constPropInBlock(build, startingBlock, state); // Verify that target hasn't changed - CODEGEN_ASSERT(function.instructions[startingBlock.finish].a.index == targetBlockIdx); + if (FFlag::LuauCodegenNilStoreInvalidateValue2) + { + if (function.instructions[startingBlock.finish].a.index != targetBlockIdx) + { + CODEGEN_ASSERT(!"Running same optimization pass on the linear chain head block changed the jump target"); + return; + } + } + else + { + CODEGEN_ASSERT(function.instructions[startingBlock.finish].a.index == targetBlockIdx); + } // Note: using startingBlock after this line is unsafe as the reference may be reallocated by build.block() below const uint32_t startingSortKey = startingBlock.sortkey; diff --git a/Common/include/Luau/Bytecode.h b/Common/include/Luau/Bytecode.h index 355bdab4..bc81a8ca 100644 --- a/Common/include/Luau/Bytecode.h +++ b/Common/include/Luau/Bytecode.h @@ -51,6 +51,7 @@ // # Bytecode type information history // Version 1: (from bytecode version 4) Type information for function signature. Currently supported. // Version 2: (from bytecode version 4) Type information for arguments, upvalues, locals and some temporaries. Currently supported. +// Version 3: (from bytecode version 5) Type information for userdata type names and their index mapping. Currently supported. // Bytecode opcode, part of the instruction header enum LuauOpcode @@ -470,6 +471,23 @@ inline bool luau_is_preemptible(unsigned int op) // E encoding: one signed 24-bit value #define LUAU_INSN_E(insn) (int32_t(insn) >> 8) +// Auxiliary AB: two 8-bit values, containing registers or small numbers +// Used in FASTCALL3 +#define LUAU_INSN_AUX_A(aux) ((aux) & 0xff) +#define LUAU_INSN_AUX_B(aux) (((aux) >> 8) & 0xff) + +// Auxiliary KV: unsigned 24-bit constant index +// Used in LOP_JUMPXEQK* instructions +#define LUAU_INSN_AUX_KV(aux) ((aux) & 0xffffff) + +// Auxiliary KB: 1-bit constant value +// Used in LOP_JUMPXEQKB instruction +#define LUAU_INSN_AUX_KB(aux) ((aux) & 0x1) + +// Auxiliary NOT: 1-bit negation flag +// Used in LOP_JUMPXEQK* instructions +#define LUAU_INSN_AUX_NOT(aux) ((aux) >> 31) + // Bytecode tags, used internally for bytecode encoded as a string enum LuauBytecodeTag { diff --git a/Common/include/Luau/ExperimentalFlags.h b/Common/include/Luau/ExperimentalFlags.h index e5f55f5c..307bbb5b 100644 --- a/Common/include/Luau/ExperimentalFlags.h +++ b/Common/include/Luau/ExperimentalFlags.h @@ -14,13 +14,10 @@ inline bool isAnalysisFlagExperimental(const char* flag) static const char* const kList[] = { "LuauInstantiateInSubtyping", // requires some fixes to lua-apps code "LuauFixIndexerSubtypingOrdering", // requires some small fixes to lua-apps code since this fixes a false negative - "StudioReportLuauAny2", // takes telemetry data for usage of any types "LuauTableCloneClonesType3", // requires fixes in lua-apps code, terrifyingly - "LuauNormalizationReorderFreeTypeIntersect", // requires fixes in lua-apps code, also terrifyingly "LuauSolverV2", "UseNewLuauTypeSolverDefaultEnabled", // This can change the default solver used in cli applications, so it also needs to be disabled. Will // require fixes in lua-apps code - "LuauRawGetHandlesNil", // requires fixes in lua-apps code // makes sure we always have at least one entry nullptr, }; diff --git a/Common/include/Luau/ScopedSeenSet.h b/Common/include/Luau/ScopedSeenSet.h new file mode 100644 index 00000000..dcee6af7 --- /dev/null +++ b/Common/include/Luau/ScopedSeenSet.h @@ -0,0 +1,29 @@ +// This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details +#pragma once + +namespace Luau +{ + +template +struct ScopedSeenSet +{ + SetType& seen; + Key key; + + ScopedSeenSet(SetType& seen, Key key) + : seen(seen) + , key(key) + { + seen.insert(key); + } + + ~ScopedSeenSet() + { + seen.erase(key); + } + + ScopedSeenSet(const ScopedSeenSet&) = delete; + ScopedSeenSet& operator=(const ScopedSeenSet&) = delete; +}; + +} // namespace Luau diff --git a/Common/include/Luau/Variant.h b/Common/include/Luau/Variant.h index 14eb8c4e..5d6542dd 100644 --- a/Common/include/Luau/Variant.h +++ b/Common/include/Luau/Variant.h @@ -290,6 +290,14 @@ auto visit(Visitor&& vis, Variant& var) } } +template +struct overloaded : Ts... +{ + using Ts::operator()...; +}; +template +overloaded(Ts...) -> overloaded; + template inline constexpr bool always_false_v = false; diff --git a/Compiler/src/Compiler.cpp b/Compiler/src/Compiler.cpp index 570c00c7..85e008aa 100644 --- a/Compiler/src/Compiler.cpp +++ b/Compiler/src/Compiler.cpp @@ -25,6 +25,7 @@ LUAU_FASTINTVARIABLE(LuauCompileLoopUnrollThresholdMaxBoost, 300) LUAU_FASTINTVARIABLE(LuauCompileInlineThreshold, 25) LUAU_FASTINTVARIABLE(LuauCompileInlineThresholdMaxBoost, 300) LUAU_FASTINTVARIABLE(LuauCompileInlineDepth, 5) +LUAU_FASTFLAG(LuauInterpStringConstFolding) namespace Luau { @@ -40,6 +41,23 @@ static const uint8_t kInvalidReg = 255; static const uint32_t kDefaultAllocPc = ~0u; +void escapeAndAppend(std::string& buffer, const char* str, size_t len) +{ + if (memchr(str, '%', len)) + { + for (size_t characterIndex = 0; characterIndex < len; ++characterIndex) + { + char character = str[characterIndex]; + buffer.push_back(character); + + if (character == '%') + buffer.push_back('%'); + } + } + else + buffer.append(str, len); +} + CompileError::CompileError(const Location& location, std::string message) : location(location) , message(std::move(message)) @@ -1788,31 +1806,68 @@ struct Compiler formatCapacity += string.size + std::count(string.data, string.data + string.size, '%'); } + size_t skippedSubExpr = 0; + if (FFlag::LuauInterpStringConstFolding) + { + 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) + { + formatCapacity += c->stringLength + std::count(c->valueString, c->valueString + c->stringLength, '%'); + skippedSubExpr++; + } + else + formatCapacity += 2; // "%*" + } + } + std::string formatString; formatString.reserve(formatCapacity); - size_t stringsLeft = expr->strings.size; + 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); - for (AstArray string : expr->strings) + 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 { - if (memchr(string.data, '%', string.size)) + size_t stringsLeft = expr->strings.size; + + for (AstArray string : expr->strings) { - for (size_t characterIndex = 0; characterIndex < string.size; ++characterIndex) + if (memchr(string.data, '%', string.size)) { - char character = string.data[characterIndex]; - formatString.push_back(character); + for (size_t characterIndex = 0; characterIndex < string.size; ++characterIndex) + { + char character = string.data[characterIndex]; + formatString.push_back(character); - if (character == '%') - formatString.push_back('%'); + if (character == '%') + formatString.push_back('%'); + } } - } - else - formatString.append(string.data, string.size); + else + formatString.append(string.data, string.size); - stringsLeft--; + stringsLeft--; - if (stringsLeft > 0) - formatString += "%*"; + if (stringsLeft > 0) + formatString += "%*"; + } } size_t formatStringSize = formatString.size(); @@ -1831,12 +1886,26 @@ struct Compiler RegScope rs(this); - uint8_t baseReg = allocReg(expr, unsigned(2 + expr->expressions.size)); + uint8_t baseReg = allocReg(expr, unsigned(2 + expr->expressions.size - skippedSubExpr)); emitLoadK(baseReg, formatStringIndex); - for (size_t index = 0; index < expr->expressions.size; ++index) - compileExprTempTop(expr->expressions.data[index], uint8_t(baseReg + 2 + index)); + if (FFlag::LuauInterpStringConstFolding) + { + 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++; + } + } + 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")); @@ -1846,7 +1915,7 @@ struct Compiler bytecode.emitABC(LOP_NAMECALL, baseReg, baseReg, uint8_t(BytecodeBuilder::getStringHash(formatMethod))); bytecode.emitAux(formatMethodIndex); - bytecode.emitABC(LOP_CALL, baseReg, uint8_t(expr->expressions.size + 2), 2); + bytecode.emitABC(LOP_CALL, baseReg, uint8_t(expr->expressions.size + 2 - skippedSubExpr), 2); bytecode.emitABC(LOP_MOVE, target, baseReg, 0); } @@ -3401,7 +3470,7 @@ struct Compiler resolveAssignConflicts(stat, vars, stat->values); // compute rhs into (mostly) fresh registers - // note that when the lhs assigment is a local, we evaluate directly into that register + // note that when the lhs assignment is a local, we evaluate directly into that register // this is possible because resolveAssignConflicts renamed conflicting locals into temporaries // after this, vars[i].valueReg is set to a register with the value for *all* vars, but some have already been assigned for (size_t i = 0; i < stat->vars.size && i < stat->values.size; ++i) diff --git a/Compiler/src/ConstantFolding.cpp b/Compiler/src/ConstantFolding.cpp index 72f99f47..0a01f24a 100644 --- a/Compiler/src/ConstantFolding.cpp +++ b/Compiler/src/ConstantFolding.cpp @@ -7,7 +7,8 @@ #include #include -LUAU_FASTFLAGVARIABLE(LuauStringConstFolding) +LUAU_FASTFLAGVARIABLE(LuauStringConstFolding2) +LUAU_FASTFLAGVARIABLE(LuauInterpStringConstFolding) namespace Luau { @@ -286,17 +287,24 @@ static void foldBinary(Constant& result, AstExprBinary::Op op, const Constant& l break; case AstExprBinary::Concat: - if (FFlag::LuauStringConstFolding) + if (FFlag::LuauStringConstFolding2) if (la.type == Constant::Type_String && ra.type == Constant::Type_String) { result.type = Constant::Type_String; result.stringLength = la.stringLength + ra.stringLength; - std::string tmp; - tmp.reserve(result.stringLength + 1); - tmp.append(la.valueString, la.stringLength); - tmp.append(ra.valueString, ra.stringLength); - AstName name = names.getOrAdd(tmp.c_str(), result.stringLength); - result.valueString = name.value; + 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 = names.getOrAdd(tmp.c_str(), result.stringLength); + result.valueString = name.value; + } } break; @@ -367,6 +375,48 @@ static void foldBinary(Constant& result, AstExprBinary::Op op, const Constant& l } } +static void foldInterpString(Constant& result, AstExprInterpString* expr, DenseHashMap& constants, AstNameTable& names) +{ + LUAU_ASSERT(expr->strings.size == expr->expressions.size + 1); + size_t resultLength = 0; + for (size_t index = 0; index < expr->strings.size; ++index) + { + resultLength += expr->strings.data[index].size; + if (index < expr->expressions.size) + { + const Constant* c = constants.find(expr->expressions.data[index]); + LUAU_ASSERT(c != nullptr && c->type == Constant::Type::Type_String); + resultLength += c->stringLength; + } + } + result.type = Constant::Type_String; + result.stringLength = resultLength; + + if (resultLength == 0) + { + result.valueString = ""; + return; + } + + std::string tmp; + tmp.reserve(resultLength); + + for (size_t index = 0; index < expr->strings.size; ++index) + { + AstArray string = expr->strings.data[index]; + tmp.append(string.data, string.size); + if (index < expr->expressions.size) + { + const Constant* c = constants.find(expr->expressions.data[index]); + tmp.append(c->valueString, c->stringLength); + } + } + result.type = Constant::Type_String; + result.stringLength = resultLength; + AstName name = names.getOrAdd(tmp.c_str(), resultLength); + result.valueString = name.value; +} + struct ConstantVisitor : AstVisitor { DenseHashMap& constants; @@ -557,8 +607,21 @@ struct ConstantVisitor : AstVisitor } else if (AstExprInterpString* expr = node->as()) { - for (AstExpr* expression : expr->expressions) - analyze(expression); + if (FFlag::LuauInterpStringConstFolding) + { + bool onlyConstantSubExpr = true; + for (AstExpr* expression : expr->expressions) + if (analyze(expression).type != Constant::Type_String) + onlyConstantSubExpr = false; + + if (onlyConstantSubExpr) + foldInterpString(result, expr, constants, names); + } + else + { + for (AstExpr* expression : expr->expressions) + analyze(expression); + } } else { diff --git a/Config/include/Luau/Config.h b/Config/include/Luau/Config.h index 89d018d2..ce5190ca 100644 --- a/Config/include/Luau/Config.h +++ b/Config/include/Luau/Config.h @@ -91,6 +91,13 @@ struct ConfigOptions std::optional aliasOptions = std::nullopt; }; +std::optional parseAlias( + Config& config, + const std::string& aliasKey, + const std::string& aliasValue, + const std::optional& aliasOptions +); + std::optional parseConfig(const std::string& contents, Config& config, const ConfigOptions& options = ConfigOptions{}); } // namespace Luau diff --git a/Config/include/Luau/LuauConfig.h b/Config/include/Luau/LuauConfig.h new file mode 100644 index 00000000..61af99dc --- /dev/null +++ b/Config/include/Luau/LuauConfig.h @@ -0,0 +1,102 @@ +// This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details +#pragma once + +#include "Luau/Common.h" +#include "Luau/Config.h" +#include "Luau/DenseHash.h" +#include "Luau/Variant.h" + +#include + +struct lua_State; + +namespace Luau +{ + +constexpr const char* kLuauConfigName = ".config.luau"; + +struct ConfigTableKey : public Variant +{ + ConfigTableKey() = default; + ConfigTableKey(const std::string& s) + : Variant(s) + { + } + ConfigTableKey(std::string&& s) + : Variant(std::move(s)) + { + } + ConfigTableKey(const char* s) + : Variant(std::string(s)) + { + } + ConfigTableKey(double d) + : Variant(d) + { + } + + std::string toString() const + { + if (const double* number = get_if()) + return std::to_string(*number); + else if (const std::string* str = get_if()) + return *str; + + LUAU_UNREACHABLE(); + } +}; + +template +struct VariantHashDefault +{ + size_t operator()(const VariantType& variant) const + { + size_t result = 0; + visit( + [&result](auto&& value) + { + result = detail::DenseHashDefault>{}(value); + }, + variant + ); + return result; + } +}; + +// Forward declaration +struct ConfigValue; + +struct ConfigTable : public DenseHashMap> +{ + ConfigTable() + : DenseHashMap>({}) + { + } +}; + +struct ConfigValue : public Variant +{ + using Variant::Variant; +}; + +struct InterruptCallbacks +{ + std::function initCallback; + void (*interruptCallback)(lua_State* L, int gc); +}; + +// Executes the contents of `source` in a sandboxed Luau VM and extracts the +// configuration table that it returns. If extraction fails, std::nullopt is +// returned and `error` is populated with the error message, if provided. +std::optional extractConfig(const std::string& source, const InterruptCallbacks& callbacks, std::string* error = nullptr); + +// Extracts a Luau::Config object into `config` from the configuration code in +// `source`, returning an error message if extraction fails. +std::optional extractLuauConfig( + const std::string& source, + Config& config, + std::optional aliasOptions, + InterruptCallbacks callbacks +); + +} // namespace Luau diff --git a/Config/src/Config.cpp b/Config/src/Config.cpp index 44cbe2e5..776cb834 100644 --- a/Config/src/Config.cpp +++ b/Config/src/Config.cpp @@ -185,7 +185,7 @@ bool isValidAlias(const std::string& alias) return true; } -static Error parseAlias( +Error parseAlias( Config& config, const std::string& aliasKey, const std::string& aliasValue, diff --git a/Config/src/LuauConfig.cpp b/Config/src/LuauConfig.cpp new file mode 100644 index 00000000..1efcb688 --- /dev/null +++ b/Config/src/LuauConfig.cpp @@ -0,0 +1,291 @@ +// This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details +#include "Luau/LuauConfig.h" + +#include "Luau/Compiler.h" +#include "Luau/Config.h" + +#include "lua.h" +#include "lualib.h" + +#include +#include +#include +#include + +#define RETURN_WITH_ERROR(msg) \ + do \ + { \ + if (error) \ + *error = msg; \ + return std::nullopt; \ + } while (false) + +namespace Luau +{ + +struct ThreadPopper +{ + explicit ThreadPopper(lua_State* L) + : L(L) + { + LUAU_ASSERT(L); + } + + ThreadPopper(const ThreadPopper&) = delete; + ThreadPopper& operator=(const ThreadPopper&) = delete; + + ~ThreadPopper() + { + lua_pop(L, 1); + } + + lua_State* L = nullptr; +}; + +static std::optional serializeTable(lua_State* L, std::string* error) +{ + ThreadPopper popper(L); // Remove table from stack after processing + ConfigTable table; + + lua_pushnil(L); + while (lua_next(L, -2) != 0) + { + ThreadPopper popper(L); // Remove value from stack after processing + + // Process key + ConfigTableKey key; + switch (lua_type(L, -2)) + { + case LUA_TNUMBER: + key = lua_tonumber(L, -2); + break; + case LUA_TSTRING: + key = std::string{lua_tostring(L, -2)}; + break; + default: + RETURN_WITH_ERROR("configuration table keys must be strings or numbers"); + } + + // Process value + switch (lua_type(L, -1)) + { + case LUA_TNUMBER: + table[key] = lua_tonumber(L, -1); + break; + case LUA_TSTRING: + table[key] = std::string{lua_tostring(L, -1)}; + break; + case LUA_TBOOLEAN: + table[key] = static_cast(lua_toboolean(L, -1)); + break; + case LUA_TTABLE: + { + lua_pushvalue(L, -1); // Copy table for recursive call + if (std::optional nested = serializeTable(L, error)) + table[key] = std::move(*nested); + else + return std::nullopt; // Error already set in recursive call + break; + } + default: + std::string msg = "configuration value for key \"" + key.toString() + "\" must be a string, number, boolean, or nested table"; + RETURN_WITH_ERROR(std::move(msg)); + } + } + + return table; +} + +std::optional extractConfig(const std::string& source, const InterruptCallbacks& callbacks, std::string* error) +{ + // Initialize Luau VM + std::unique_ptr state{luaL_newstate(), lua_close}; + lua_State* L = state.get(); + luaL_openlibs(L); + luaL_sandbox(L); + + // Compile and load source code + std::string bytecode = compile(source); + if (luau_load(L, "=config", bytecode.data(), bytecode.size(), 0) != 0) + RETURN_WITH_ERROR(lua_tostring(L, -1)); + + // Execute configuration + if (callbacks.initCallback) + callbacks.initCallback(L); + lua_callbacks(L)->interrupt = callbacks.interruptCallback; + switch (lua_resume(L, nullptr, 0)) + { + case LUA_OK: + break; + case LUA_BREAK: // debugging not supported, at least for now + case LUA_YIELD: + RETURN_WITH_ERROR("configuration execution cannot yield"); + default: + RETURN_WITH_ERROR(lua_tostring(L, -1)); + } + + // Extract returned table + if (lua_gettop(L) != 1) + RETURN_WITH_ERROR("configuration must return exactly one value"); + + if (lua_type(L, -1) != LUA_TTABLE) + RETURN_WITH_ERROR("configuration did not return a table"); + + return serializeTable(L, error); +} + +static std::optional createLuauConfigFromLuauTable( + Config& config, + const ConfigTable& luauTable, + std::optional aliasOptions +) +{ + for (const auto& [k, v] : luauTable) + { + const std::string* key = k.get_if(); + if (!key) + return "configuration keys in \"luau\" table must be strings"; + + if (*key == "languagemode") + { + const std::string* value = v.get_if(); + if (!value) + return "configuration value for key \"languagemode\" must be a string"; + + if (std::optional errorMessage = parseModeString(config.mode, *value)) + return errorMessage; + } + + if (*key == "lint") + { + const ConfigTable* lint = v.get_if(); + if (!lint) + return "configuration value for key \"lint\" must be a table"; + + // Handle wildcard first to ensure overrides work as expected. + if (const ConfigValue* value = lint->find("*")) + { + const bool* enabled = value->get_if(); + if (!enabled) + return "configuration values in \"lint\" table must be booleans"; + + if (std::optional errorMessage = + parseLintRuleString(config.enabledLint, config.fatalLint, "*", *enabled ? "true" : "false")) + return errorMessage; + } + + for (const auto& [k, v] : *lint) + { + const std::string* warningName = k.get_if(); + if (!warningName) + return "configuration keys in \"lint\" table must be strings"; + + if (*warningName == "*") + continue; // Handled above + + const bool* enabled = v.get_if(); + if (!enabled) + return "configuration values in \"lint\" table must be booleans"; + + if (std::optional errorMessage = + parseLintRuleString(config.enabledLint, config.fatalLint, *warningName, *enabled ? "true" : "false")) + return errorMessage; + } + } + + if (*key == "linterrors") + { + const bool* value = v.get_if(); + if (!value) + return "configuration value for key \"linterrors\" must be a boolean"; + + config.lintErrors = *value; + } + + if (*key == "typeerrors") + { + const bool* value = v.get_if(); + if (!value) + return "configuration value for key \"typeerrors\" must be a boolean"; + + config.typeErrors = *value; + } + + if (*key == "globals") + { + const ConfigTable* globalsTable = v.get_if(); + if (!globalsTable) + return "configuration value for key \"globals\" must be an array of strings"; + + std::vector globals; + globals.resize(globalsTable->size()); + + for (const auto& [k, v] : *globalsTable) + { + const double* key = k.get_if(); + if (!key) + return "configuration array \"globals\" must only have numeric keys"; + + const size_t index = static_cast(*key); + if (index < 1 || globalsTable->size() < index) + return "configuration array \"globals\" contains invalid numeric key"; + + const std::string* global = v.get_if(); + if (!global) + return "configuration value in \"globals\" table must be a string"; + + LUAU_ASSERT(0 <= index - 1 && index - 1 < globalsTable->size()); + globals[index - 1] = *global; + } + + config.globals = std::move(globals); + } + + if (*key == "aliases") + { + const ConfigTable* aliases = v.get_if(); + if (!aliases) + return "configuration value for key \"aliases\" must be a table"; + + for (const auto& [k, v] : *aliases) + { + const std::string* aliasKey = k.get_if(); + if (!aliasKey) + return "configuration keys in \"aliases\" table must be strings"; + + const std::string* aliasValue = v.get_if(); + if (!aliasValue) + return "configuration values in \"aliases\" table must be strings"; + + if (std::optional errorMessage = parseAlias(config, *aliasKey, *aliasValue, aliasOptions)) + return errorMessage; + } + } + } + + return std::nullopt; +} + +std::optional extractLuauConfig( + const std::string& source, + Config& config, + std::optional aliasOptions, + InterruptCallbacks callbacks +) +{ + std::string error; + std::optional configTable = extractConfig(source, callbacks, &error); + if (!configTable) + return error; + + if (!configTable->contains("luau")) + return std::nullopt; + + ConfigTable* luauTable = (*configTable)["luau"].get_if(); + if (!luauTable) + return "configuration value for key \"luau\" must be a table"; + + return createLuauConfigFromLuauTable(config, *luauTable, aliasOptions); +} + +} // namespace Luau diff --git a/Makefile b/Makefile index 4f2df705..1cda6766 100644 --- a/Makefile +++ b/Makefile @@ -44,14 +44,10 @@ CJSON_OBJECTS=$(CJSON_SOURCES:%=$(BUILD)/%.o) APR_SOURCES=$(wildcard VM/src/apr/*.cpp) APR_OBJECTS=$(APR_SOURCES:%=$(BUILD)/%.o) -REQUIRE_SOURCES=$(wildcard Require/Runtime/src/*.cpp) +REQUIRE_SOURCES=$(wildcard Require/src/*.cpp) REQUIRE_OBJECTS=$(REQUIRE_SOURCES:%=$(BUILD)/%.o) REQUIRE_TARGET=$(BUILD)/libluaurequire.a -REQUIRENAVIGATOR_SOURCES=$(wildcard Require/Navigator/src/*.cpp) -REQUIRENAVIGATOR_OBJECTS=$(REQUIRENAVIGATOR_SOURCES:%=$(BUILD)/%.o) -REQUIRENAVIGATOR_TARGET=$(BUILD)/libluaurequirenavigator.a - ISOCLINE_SOURCES=extern/isocline/src/isocline.c ISOCLINE_OBJECTS=$(ISOCLINE_SOURCES:%=$(BUILD)/%.o) ISOCLINE_TARGET=$(BUILD)/libisocline.a @@ -87,7 +83,7 @@ ifneq ($(opt),) TESTS_ARGS+=-O$(opt) endif -OBJECTS=$(AST_OBJECTS) $(COMPILER_OBJECTS) $(CONFIG_OBJECTS) $(ANALYSIS_OBJECTS) $(EQSAT_OBJECTS) $(CODEGEN_OBJECTS) $(VM_OBJECTS) $(CJSON_OBJECTS) $(APR_OBJECTS) $(REQUIRE_OBJECTS) $(REQUIRENAVIGATOR_OBJECTS) $(ISOCLINE_OBJECTS) $(TESTS_OBJECTS) $(REPL_CLI_OBJECTS) $(ANALYZE_CLI_OBJECTS) $(COMPILE_CLI_OBJECTS) $(BYTECODE_CLI_OBJECTS) $(FUZZ_OBJECTS) +OBJECTS=$(AST_OBJECTS) $(COMPILER_OBJECTS) $(CONFIG_OBJECTS) $(ANALYSIS_OBJECTS) $(EQSAT_OBJECTS) $(CODEGEN_OBJECTS) $(VM_OBJECTS) $(CJSON_OBJECTS) $(APR_OBJECTS) $(REQUIRE_OBJECTS) $(ISOCLINE_OBJECTS) $(TESTS_OBJECTS) $(REPL_CLI_OBJECTS) $(ANALYZE_CLI_OBJECTS) $(COMPILE_CLI_OBJECTS) $(BYTECODE_CLI_OBJECTS) $(FUZZ_OBJECTS) EXECUTABLE_ALIASES = luau luau-analyze luau-compile luau-bytecode luau-tests # common flags @@ -159,20 +155,19 @@ endif # target-specific flags $(AST_OBJECTS): CXXFLAGS+=-std=c++17 -ICommon/include -IAst/include $(COMPILER_OBJECTS): CXXFLAGS+=-std=c++17 -ICompiler/include -ICommon/include -IAst/include -Istage/packages/include -$(CONFIG_OBJECTS): CXXFLAGS+=-std=c++17 -IConfig/include -ICommon/include -IAst/include +$(CONFIG_OBJECTS): CXXFLAGS+=-std=c++17 -IConfig/include -ICommon/include -IAst/include -ICompiler/include -IVM/include $(ANALYSIS_OBJECTS): CXXFLAGS+=-std=c++17 -ICommon/include -IAst/include -IAnalysis/include -IEqSat/include -IConfig/include -ICompiler/include -IVM/include $(EQSAT_OBJECTS): CXXFLAGS+=-std=c++17 -ICommon/include -IEqSat/include $(CODEGEN_OBJECTS): CXXFLAGS+=-std=c++17 -ICommon/include -ICodeGen/include -IVM/include -IVM/src # Code generation needs VM internals $(VM_OBJECTS): CXXFLAGS+=-std=c++11 -ICommon/include -IVM/include -Istage/packages/include -I VM/cjson $(APR_OBJECTS): CXXFLAGS+=-std=c++11 -Wno-unused-function -Wno-char-subscripts -IVM/include -ICommon/include $(CJSON_OBJECTS): CXXFLAGS+=-std=c++11 -Wno-unused-function -Wno-char-subscripts -IVM/include -ICommon/include -I VM/cjson -$(REQUIRE_OBJECTS): CXXFLAGS+=-std=c++17 -ICommon/include -IVM/include -IAst/include -IConfig/include -IRequire/Navigator/include -IRequire/Runtime/include -$(REQUIRENAVIGATOR_OBJECTS): CXXFLAGS+=-std=c++17 -ICommon/include -IAst/include -IConfig/include -IRequire/Navigator/include +$(REQUIRE_OBJECTS): CXXFLAGS+=-std=c++17 -ICommon/include -IVM/include -IAst/include -IConfig/include -IRequire/include $(ISOCLINE_OBJECTS): CXXFLAGS+=-Wno-unused-function -Iextern/isocline/include -$(TESTS_OBJECTS): CXXFLAGS+=-std=c++17 -ICommon/include -IAst/include -ICompiler/include -IConfig/include -IAnalysis/include -IEqSat/include -ICodeGen/include -IVM/include -IRequire/Runtime/include -ICLI/include -Iextern -Istage/packages/include -DDOCTEST_CONFIG_DOUBLE_STRINGIFY -$(REPL_CLI_OBJECTS): CXXFLAGS+=-std=c++17 -ICommon/include -IAst/include -ICompiler/include -IVM/include -ICodeGen/include -IRequire/Runtime/include -Iextern -Iextern/isocline/include -ICLI/include -Istage/packages/include -$(ANALYZE_CLI_OBJECTS): CXXFLAGS+=-std=c++17 -ICommon/include -IAst/include -IAnalysis/include -IEqSat/include -IConfig/include -IRequire/Navigator/include -Iextern -ICLI/include -$(COMPILE_CLI_OBJECTS): CXXFLAGS+=-std=c++17 -ICommon/include -IAst/include -ICompiler/include -IVM/include -ICodeGen/include -ICLI/include -Istage/packages/include +$(TESTS_OBJECTS): CXXFLAGS+=-std=c++17 -ICommon/include -IAst/include -ICompiler/include -IConfig/include -IAnalysis/include -IEqSat/include -ICodeGen/include -IVM/include -IRequire/include -ICLI/include -Iextern -Istage/packages/include -DDOCTEST_CONFIG_DOUBLE_STRINGIFY +$(REPL_CLI_OBJECTS): CXXFLAGS+=-std=c++17 -ICommon/include -IAst/include -ICompiler/include -IVM/include -ICodeGen/include -IRequire/include -Iextern -Iextern/isocline/include -ICLI/include +$(ANALYZE_CLI_OBJECTS): CXXFLAGS+=-std=c++17 -ICommon/include -IAst/include -IAnalysis/include -IEqSat/include -IConfig/include -IRequire/include -Iextern -ICLI/include +$(COMPILE_CLI_OBJECTS): CXXFLAGS+=-std=c++17 -ICommon/include -IAst/include -ICompiler/include -IVM/include -ICodeGen/include -ICLI/include -Istage/packages/include $(BYTECODE_CLI_OBJECTS): CXXFLAGS+=-std=c++17 -ICommon/include -IAst/include -ICompiler/include -IVM/include -ICodeGen/include -ICLI/include $(FUZZ_OBJECTS): CXXFLAGS+=-std=c++17 -ICommon/include -IAst/include -ICompiler/include -IAnalysis/include -IEqSat/include -IVM/include -ICodeGen/include -IConfig/include @@ -248,9 +243,9 @@ luau-tests: $(TESTS_TARGET) ln -fs $^ $@ # executable targets -$(TESTS_TARGET): $(TESTS_OBJECTS) $(ANALYSIS_TARGET) $(EQSAT_TARGET) $(COMPILER_TARGET) $(AST_TARGET) $(CODEGEN_TARGET) $(VM_TARGET) $(REQUIRE_TARGET) $(REQUIRENAVIGATOR_TARGET) $(CONFIG_TARGET) $(ISOCLINE_TARGET) -$(REPL_CLI_TARGET): $(REPL_CLI_OBJECTS) $(COMPILER_TARGET) $(AST_TARGET) $(CODEGEN_TARGET) $(VM_TARGET) $(REQUIRE_TARGET) $(REQUIRENAVIGATOR_TARGET) $(CONFIG_TARGET) $(ISOCLINE_TARGET) -$(ANALYZE_CLI_TARGET): $(ANALYZE_CLI_OBJECTS) $(ANALYSIS_TARGET) $(EQSAT_TARGET) $(AST_TARGET) $(COMPILER_TARGET) $(VM_TARGET) $(REQUIRENAVIGATOR_TARGET) $(CONFIG_TARGET) +$(TESTS_TARGET): $(TESTS_OBJECTS) $(ANALYSIS_TARGET) $(EQSAT_TARGET) $(COMPILER_TARGET) $(AST_TARGET) $(CODEGEN_TARGET) $(VM_TARGET) $(REQUIRE_TARGET) $(CONFIG_TARGET) $(ISOCLINE_TARGET) +$(REPL_CLI_TARGET): $(REPL_CLI_OBJECTS) $(COMPILER_TARGET) $(AST_TARGET) $(CODEGEN_TARGET) $(VM_TARGET) $(REQUIRE_TARGET) $(CONFIG_TARGET) $(ISOCLINE_TARGET) +$(ANALYZE_CLI_TARGET): $(ANALYZE_CLI_OBJECTS) $(ANALYSIS_TARGET) $(EQSAT_TARGET) $(AST_TARGET) $(COMPILER_TARGET) $(VM_TARGET) $(REQUIRE_TARGET) $(CONFIG_TARGET) $(COMPILE_CLI_TARGET): $(COMPILE_CLI_OBJECTS) $(COMPILER_TARGET) $(AST_TARGET) $(CODEGEN_TARGET) $(VM_TARGET) $(BYTECODE_CLI_TARGET): $(BYTECODE_CLI_OBJECTS) $(COMPILER_TARGET) $(AST_TARGET) $(CODEGEN_TARGET) $(VM_TARGET) @@ -273,10 +268,9 @@ $(EQSAT_TARGET): $(EQSAT_OBJECTS) $(CODEGEN_TARGET): $(CODEGEN_OBJECTS) $(VM_TARGET): $(VM_OBJECTS) $(CJSON_OBJECTS) $(APR_OBJECTS) $(REQUIRE_TARGET): $(REQUIRE_OBJECTS) -$(REQUIRENAVIGATOR_TARGET): $(REQUIRENAVIGATOR_OBJECTS) $(ISOCLINE_TARGET): $(ISOCLINE_OBJECTS) -$(AST_TARGET) $(COMPILER_TARGET) $(CONFIG_TARGET) $(ANALYSIS_TARGET) $(EQSAT_TARGET) $(CODEGEN_TARGET) $(VM_TARGET) $(REQUIRE_TARGET) $(REQUIRENAVIGATOR_TARGET) $(ISOCLINE_TARGET): +$(AST_TARGET) $(COMPILER_TARGET) $(CONFIG_TARGET) $(ANALYSIS_TARGET) $(EQSAT_TARGET) $(CODEGEN_TARGET) $(VM_TARGET) $(REQUIRE_TARGET) $(ISOCLINE_TARGET): ar rcs $@ $^ # object file targets diff --git a/Require/Runtime/include/Luau/Require.h b/Require/include/Luau/Require.h similarity index 75% rename from Require/Runtime/include/Luau/Require.h rename to Require/include/Luau/Require.h index 386caffc..a4918fff 100644 --- a/Require/Runtime/include/Luau/Require.h +++ b/Require/include/Luau/Require.h @@ -3,6 +3,7 @@ #include "lua.h" +#include #include //////////////////////////////////////////////////////////////////////////////// @@ -19,7 +20,7 @@ // // Without more context, it is impossible to tell which components in a given // path "./foo/bar/baz" are modules and which are directories. To provide this -// context, the require-by-string runtime library must be opened with a +// context, the require-by-string runtime library must be opened with a // luarequire_Configuration object, which defines the navigation behavior of the // context in which Luau is embedded. // @@ -35,35 +36,44 @@ // nestable code unit and organizational unit, respectively. // // Require-by-string's runtime behavior can be additionally be configured in -// configuration files, such as .luaurc files in a filesystem context. The -// presence of a configuration file in the current context is signaled by the -// is_config_present function. Both modules and directories can contain -// configuration files; however, note that a given configuration file's scope is -// limited to the descendants of the module or directory in which it resides. In -// other words, when searching for a relevant configuration file for a given -// module, the search begins at the module's parent context and proceeds up the -// hierarchy from there, resolving to the first configuration file found. +// configuration files, such as .luaurc or .config.luau files in a filesystem +// context. The presence of a configuration file in the current context is +// signaled by the get_config_status function. Both modules and directories can +// contain configuration files; however, note that a given configuration file's +// scope is limited to the descendants of the module or directory in which it +// resides. In other words, when searching for a relevant configuration file for +// a given module, the search begins at the module's parent context and proceeds +// up the hierarchy from there, resolving to the first configuration file found. // //////////////////////////////////////////////////////////////////////////////// -enum luarequire_NavigateResult +typedef enum luarequire_NavigateResult { NAVIGATE_SUCCESS, NAVIGATE_AMBIGUOUS, NAVIGATE_NOT_FOUND -}; +} luarequire_NavigateResult; // Functions returning WRITE_SUCCESS are expected to set their size_out argument // to the number of bytes written to the buffer. If WRITE_BUFFER_TOO_SMALL is // returned, size_out should be set to the required buffer size. -enum luarequire_WriteResult +typedef enum luarequire_WriteResult { WRITE_SUCCESS, WRITE_BUFFER_TOO_SMALL, WRITE_FAILURE -}; +} luarequire_WriteResult; -struct luarequire_Configuration +// Represents whether a configuration file is present, and if so, its syntax. +typedef enum luarequire_ConfigStatus +{ + CONFIG_ABSENT, + CONFIG_AMBIGUOUS, // Signals the presence of multiple configuration files. + CONFIG_PRESENT_JSON, + CONFIG_PRESENT_LUAU, +} luarequire_ConfigStatus; + +typedef struct luarequire_Configuration { // Returns whether requires are permitted from the given chunkname. bool (*is_require_allowed)(lua_State* L, void* ctx, const char* requirer_chunkname); @@ -96,31 +106,40 @@ struct luarequire_Configuration // only called if is_module_present returns true. luarequire_WriteResult (*get_cache_key)(lua_State* L, void* ctx, char* buffer, size_t buffer_size, size_t* size_out); - // Returns whether a configuration file is present in the current context. - // If not, require-by-string will call to_parent until either a - // configuration file is present or NAVIGATE_FAILURE is returned (at root). - bool (*is_config_present)(lua_State* L, void* ctx); + // Returns whether a configuration file is present in the current context, + // and if so, its syntax. If not present, require-by-string will call + // to_parent until either a configuration file is present or + // NAVIGATE_FAILURE is returned (at root). + luarequire_ConfigStatus (*get_config_status)(lua_State* L, void* ctx); // Parses the configuration file in the current context for the given alias // and returns its value or WRITE_FAILURE if not found. This function is - // only called if is_config_present returns true. If this function pointer + // only called if get_config_status returns true. If this function pointer // is set, get_config must not be set. Opting in to this function pointer // disables parsing configuration files internally and can be used for finer // control over the configuration file parsing process. luarequire_WriteResult (*get_alias)(lua_State* L, void* ctx, const char* alias, char* buffer, size_t buffer_size, size_t* size_out); // Provides the contents of the configuration file in the current context. - // This function is only called if is_config_present returns true. If this - // function pointer is set, get_alias must not be set. Opting in to this - // function pointer enables parsing configuration files internally. + // This function is only called if get_config_status does not return + // CONFIG_ABSENT. If this function pointer is set, get_alias must not be + // set. Opting in to this function pointer enables parsing configuration + // files internally. luarequire_WriteResult (*get_config)(lua_State* L, void* ctx, char* buffer, size_t buffer_size, size_t* size_out); + // Returns the maximum number of milliseconds to allow for executing a given + // Luau-syntax configuration file. This function is only called if + // get_config_status returns CONFIG_PRESENT_LUAU and can be left undefined + // if support for Luau-syntax configuration files is not needed. A default + // value of 2000ms is used. Negative values are treated as infinite. + int (*get_luau_config_timeout)(lua_State* L, void* ctx); + // Executes the module and places the result on the stack. Returns the // number of results placed on the stack. Returning -1 directs the requiring // thread to yield. In this case, this thread should be resumed with the // module result pushed onto its stack. int (*load)(lua_State* L, void* ctx, const char* path, const char* chunkname, const char* loadname); -}; +} luarequire_Configuration; // Populates function pointers in the given luarequire_Configuration. typedef void (*luarequire_Configuration_init)(luarequire_Configuration* config); diff --git a/Require/Navigator/include/Luau/RequireNavigator.h b/Require/include/Luau/RequireNavigator.h similarity index 89% rename from Require/Navigator/include/Luau/RequireNavigator.h rename to Require/include/Luau/RequireNavigator.h index af5646a4..698df295 100644 --- a/Require/Navigator/include/Luau/RequireNavigator.h +++ b/Require/include/Luau/RequireNavigator.h @@ -2,11 +2,15 @@ #pragma once #include "Luau/Config.h" +#include "Luau/LuauConfig.h" +#include #include #include #include +struct lua_State; + //////////////////////////////////////////////////////////////////////////////// // // The RequireNavigator library provides a C++ interface for navigating the @@ -65,10 +69,21 @@ class NavigationContext GetConfig }; - virtual bool isConfigPresent() const = 0; + enum class ConfigStatus + { + Absent, + Ambiguous, + PresentJson, + PresentLuau + }; + + virtual ConfigStatus getConfigStatus() const = 0; + + std::function luauConfigInit = nullptr; + void (*luauConfigInterrupt)(lua_State* L, int gc) = nullptr; // The result of getConfigBehavior determines whether getAlias or getConfig - // is called when isConfigPresent returns true. + // is called when getConfigStatus indicates a configuration is present. virtual ConfigBehavior getConfigBehavior() const = 0; virtual std::optional getAlias(const std::string& alias) const = 0; virtual std::optional getConfig() const = 0; diff --git a/Require/Runtime/src/Navigation.cpp b/Require/src/Navigation.cpp similarity index 74% rename from Require/Runtime/src/Navigation.cpp rename to Require/src/Navigation.cpp index ba679866..9438dca9 100644 --- a/Require/Runtime/src/Navigation.cpp +++ b/Require/src/Navigation.cpp @@ -6,6 +6,7 @@ #include "lua.h" #include "lualib.h" +#include #include static constexpr size_t initalFileBufferSize = 1024; @@ -24,12 +25,38 @@ static NavigationContext::NavigateResult convertNavigateResult(luarequire_Naviga return NavigationContext::NavigateResult::NotFound; } +static NavigationContext::ConfigStatus convertConfigStatus(luarequire_ConfigStatus status) +{ + if (status == CONFIG_PRESENT_JSON) + return NavigationContext::ConfigStatus::PresentJson; + if (status == CONFIG_PRESENT_LUAU) + return NavigationContext::ConfigStatus::PresentLuau; + if (status == CONFIG_AMBIGUOUS) + return NavigationContext::ConfigStatus::Ambiguous; + + return NavigationContext::ConfigStatus::Absent; +} + RuntimeNavigationContext::RuntimeNavigationContext(luarequire_Configuration* config, lua_State* L, void* ctx, std::string requirerChunkname) : config(config) , L(L) , ctx(ctx) , requirerChunkname(std::move(requirerChunkname)) { + luauConfigInit = [config, ctx, this](lua_State* L) + { + int timeout = config->get_luau_config_timeout ? config->get_luau_config_timeout(L, ctx) : 2000; + this->timer.start(timeout); + lua_setthreaddata(L, &this->timer); + }; + + luauConfigInterrupt = [](lua_State* L, int gc) + { + RuntimeLuauConfigTimer* timer = static_cast(lua_getthreaddata(L)); + LUAU_ASSERT(timer); + if (timer->isFinished()) + luaL_errorL(L, "configuration execution timed out"); + }; } std::string RuntimeNavigationContext::getRequirerIdentifier() const @@ -77,9 +104,9 @@ std::optional RuntimeNavigationContext::getCacheKey() const return getStringFromCWriter(config->get_cache_key, initalIdentifierBufferSize); } -bool RuntimeNavigationContext::isConfigPresent() const +NavigationContext::ConfigStatus RuntimeNavigationContext::getConfigStatus() const { - return config->is_config_present(L, ctx); + return convertConfigStatus(config->get_config_status(L, ctx)); } NavigationContext::ConfigBehavior RuntimeNavigationContext::getConfigBehavior() const @@ -165,4 +192,20 @@ const std::string& RuntimeErrorHandler::getReportedError() const return errorMessage; } +void RuntimeLuauConfigTimer::start(int timeoutMs) +{ + startTime = std::chrono::steady_clock::now(); + if (timeoutMs < 0) + timeoutDuration = std::nullopt; // Infinite timeout + else + timeoutDuration = std::chrono::milliseconds(timeoutMs); +} + +bool RuntimeLuauConfigTimer::isFinished() const +{ + if (!timeoutDuration) + return false; + return (std::chrono::steady_clock::now() - startTime) >= *timeoutDuration; +} + } // namespace Luau::Require diff --git a/Require/Runtime/src/Navigation.h b/Require/src/Navigation.h similarity index 85% rename from Require/Runtime/src/Navigation.h rename to Require/src/Navigation.h index 36c30846..ecb90668 100644 --- a/Require/Runtime/src/Navigation.h +++ b/Require/src/Navigation.h @@ -4,6 +4,7 @@ #include "Luau/RequireNavigator.h" #include "Luau/Require.h" +#include #include struct lua_State; @@ -12,6 +13,17 @@ struct luarequire_Configuration; namespace Luau::Require { +class RuntimeLuauConfigTimer +{ +public: + void start(int timeoutMs); + bool isFinished() const; + +private: + std::chrono::steady_clock::time_point startTime; + std::optional timeoutDuration; +}; + class RuntimeNavigationContext : public NavigationContext { public: @@ -26,7 +38,7 @@ class RuntimeNavigationContext : public NavigationContext NavigateResult toParent() override; NavigateResult toChild(const std::string& component) override; - bool isConfigPresent() const override; + NavigationContext::ConfigStatus getConfigStatus() const override; NavigationContext::ConfigBehavior getConfigBehavior() const override; std::optional getAlias(const std::string& alias) const override; std::optional getConfig() const override; @@ -52,6 +64,7 @@ class RuntimeNavigationContext : public NavigationContext lua_State* L; void* ctx; std::string requirerChunkname; + RuntimeLuauConfigTimer timer = RuntimeLuauConfigTimer{}; }; // Non-throwing error reporter diff --git a/Require/Navigator/src/PathUtilities.cpp b/Require/src/PathUtilities.cpp similarity index 96% rename from Require/Navigator/src/PathUtilities.cpp rename to Require/src/PathUtilities.cpp index 6de4f7ad..397a1026 100644 --- a/Require/Navigator/src/PathUtilities.cpp +++ b/Require/src/PathUtilities.cpp @@ -1,6 +1,6 @@ // This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details -#include "Luau/PathUtilities.h" +#include "PathUtilities.h" #include diff --git a/Require/Navigator/include/Luau/PathUtilities.h b/Require/src/PathUtilities.h similarity index 100% rename from Require/Navigator/include/Luau/PathUtilities.h rename to Require/src/PathUtilities.h diff --git a/Require/Runtime/src/Require.cpp b/Require/src/Require.cpp similarity index 91% rename from Require/Runtime/src/Require.cpp rename to Require/src/Require.cpp index 62127acf..1f4906e3 100644 --- a/Require/Runtime/src/Require.cpp +++ b/Require/src/Require.cpp @@ -7,6 +7,8 @@ #include "lua.h" #include "lualib.h" +#include + static void validateConfig(lua_State* L, const luarequire_Configuration& config) { if (!config.is_require_allowed) @@ -27,8 +29,8 @@ static void validateConfig(lua_State* L, const luarequire_Configuration& config) luaL_error(L, "require configuration is missing required function pointer: get_loadname"); if (!config.get_cache_key) luaL_error(L, "require configuration is missing required function pointer: get_cache_key"); - if (!config.is_config_present) - luaL_error(L, "require configuration is missing required function pointer: is_config_present"); + if (!config.get_config_status) + luaL_error(L, "require configuration is missing required function pointer: get_config_status"); if (config.get_alias && config.get_config) luaL_error(L, "require configuration cannot define both get_alias and get_config"); if (!config.get_alias && !config.get_config) @@ -45,12 +47,14 @@ static int pushrequireclosureinternal( const char* debugname ) { - luarequire_Configuration* config = static_cast(lua_newuserdata(L, sizeof(luarequire_Configuration))); - if (!config) + void* ud = (lua_newuserdata(L, sizeof(luarequire_Configuration))); + if (!ud) luaL_error(L, "failed to allocate memory for require configuration"); + // ServerLua: fix the userdata in place lua_fixvalue(L, -1); + luarequire_Configuration* config = new (ud) luarequire_Configuration{}; config_init(config); validateConfig(L, *config); diff --git a/Require/Runtime/src/RequireImpl.cpp b/Require/src/RequireImpl.cpp similarity index 100% rename from Require/Runtime/src/RequireImpl.cpp rename to Require/src/RequireImpl.cpp diff --git a/Require/Runtime/src/RequireImpl.h b/Require/src/RequireImpl.h similarity index 100% rename from Require/Runtime/src/RequireImpl.h rename to Require/src/RequireImpl.h diff --git a/Require/Navigator/src/RequireNavigator.cpp b/Require/src/RequireNavigator.cpp similarity index 78% rename from Require/Navigator/src/RequireNavigator.cpp rename to Require/src/RequireNavigator.cpp index e5a713c9..9852039d 100644 --- a/Require/Navigator/src/RequireNavigator.cpp +++ b/Require/src/RequireNavigator.cpp @@ -2,9 +2,10 @@ #include "Luau/RequireNavigator.h" -#include "Luau/PathUtilities.h" +#include "PathUtilities.h" #include "Luau/Config.h" +#include "Luau/LuauConfig.h" #include #include @@ -173,33 +174,56 @@ Error Navigator::navigateToAndPopulateConfig(const std::string& desiredAlias) while (!foundAliasValue) { - if (navigationContext.toParent() != NavigationContext::NavigateResult::Success) - break; - - if (navigationContext.isConfigPresent()) + NavigationContext::NavigateResult result = navigationContext.toParent(); + if (result == NavigationContext::NavigateResult::Ambiguous) + return "could not navigate up the ancestry chain during search for alias \"" + desiredAlias + "\" (ambiguous)"; + if (result == NavigationContext::NavigateResult::NotFound) + break; // Not treated as an error: interpreted as reaching the root. + + NavigationContext::ConfigStatus status = navigationContext.getConfigStatus(); + if (status == NavigationContext::ConfigStatus::Absent) + { + continue; + } + else if (status == NavigationContext::ConfigStatus::Ambiguous) + { + return "could not resolve alias \"" + desiredAlias + "\" (ambiguous configuration file)"; + } + else { if (navigationContext.getConfigBehavior() == NavigationContext::ConfigBehavior::GetAlias) { foundAliasValue = navigationContext.getAlias(desiredAlias); + break; } - else - { - std::optional configContents = navigationContext.getConfig(); - if (!configContents) - return "could not get configuration file contents to resolve alias \"" + desiredAlias + "\""; - Luau::ConfigOptions opts; - Luau::ConfigOptions::AliasOptions aliasOpts; - aliasOpts.configLocation = "unused"; - aliasOpts.overwriteAliases = false; - opts.aliasOptions = std::move(aliasOpts); + std::optional configContents = navigationContext.getConfig(); + if (!configContents) + return "could not get configuration file contents to resolve alias \"" + desiredAlias + "\""; + + Luau::ConfigOptions opts; + Luau::ConfigOptions::AliasOptions aliasOpts; + aliasOpts.configLocation = "unused"; + aliasOpts.overwriteAliases = false; + opts.aliasOptions = std::move(aliasOpts); + if (status == NavigationContext::ConfigStatus::PresentJson) + { if (Error error = Luau::parseConfig(*configContents, config, opts)) return error; + } + else if (status == NavigationContext::ConfigStatus::PresentLuau) + { + InterruptCallbacks callbacks; + callbacks.initCallback = navigationContext.luauConfigInit; + callbacks.interruptCallback = navigationContext.luauConfigInterrupt; - if (config.aliases.contains(desiredAlias)) - foundAliasValue = config.aliases[desiredAlias].value; + if (Error error = Luau::extractLuauConfig(*configContents, config, std::move(opts.aliasOptions), std::move(callbacks))) + return error; } + + if (config.aliases.contains(desiredAlias)) + foundAliasValue = config.aliases[desiredAlias].value; } }; diff --git a/Sources.cmake b/Sources.cmake index f379aa59..2b36fd57 100644 --- a/Sources.cmake +++ b/Sources.cmake @@ -24,6 +24,7 @@ target_sources(Luau.Ast PRIVATE Ast/include/Luau/ParseOptions.h Ast/include/Luau/Parser.h Ast/include/Luau/ParseResult.h + Ast/include/Luau/PrettyPrinter.h Ast/include/Luau/StringUtils.h Ast/include/Luau/TimeTrace.h @@ -34,6 +35,7 @@ target_sources(Luau.Ast PRIVATE Ast/src/Lexer.cpp Ast/src/Location.cpp Ast/src/Parser.cpp + Ast/src/PrettyPrinter.cpp Ast/src/StringUtils.cpp Ast/src/TimeTrace.cpp ) @@ -74,9 +76,11 @@ endif() target_sources(Luau.Config PRIVATE Config/include/Luau/Config.h Config/include/Luau/LinterConfig.h + Config/include/Luau/LuauConfig.h Config/src/Config.cpp Config/src/LinterConfig.cpp + Config/src/LuauConfig.cpp ) # Luau.CodeGen Sources @@ -190,6 +194,7 @@ target_sources(Luau.Analysis PRIVATE Analysis/include/Luau/Constraint.h Analysis/include/Luau/ConstraintGenerator.h Analysis/include/Luau/ConstraintSet.h + Analysis/include/Luau/NativeStackGuard.h Analysis/include/Luau/ConstraintSolver.h Analysis/include/Luau/ControlFlow.h Analysis/include/Luau/DataFlowGraph.h @@ -235,7 +240,6 @@ target_sources(Luau.Analysis PRIVATE Analysis/include/Luau/ToDot.h Analysis/include/Luau/TopoSortStatements.h Analysis/include/Luau/ToString.h - Analysis/include/Luau/Transpiler.h Analysis/include/Luau/TxnLog.h Analysis/include/Luau/Type.h Analysis/include/Luau/TypeArena.h @@ -286,6 +290,7 @@ target_sources(Luau.Analysis PRIVATE Analysis/src/FragmentAutocomplete.cpp Analysis/src/Frontend.cpp Analysis/src/Generalization.cpp + Analysis/src/NativeStackGuard.cpp Analysis/src/GlobalTypes.cpp Analysis/src/InferPolarity.cpp Analysis/src/Instantiation.cpp @@ -299,6 +304,7 @@ target_sources(Luau.Analysis PRIVATE Analysis/src/Normalize.cpp Analysis/src/OverloadResolution.cpp Analysis/src/Quantify.cpp + Analysis/src/RecursionCounter.cpp Analysis/src/Refinement.cpp Analysis/src/RequireTracer.cpp Analysis/src/Scope.cpp @@ -310,7 +316,6 @@ target_sources(Luau.Analysis PRIVATE Analysis/src/ToDot.cpp Analysis/src/TopoSortStatements.cpp Analysis/src/ToString.cpp - Analysis/src/Transpiler.cpp Analysis/src/TxnLog.cpp Analysis/src/Type.cpp Analysis/src/TypeArena.cpp @@ -539,7 +544,9 @@ if(TARGET Luau.UnitTest) tests/NonStrictTypeChecker.test.cpp tests/Normalize.test.cpp tests/NotNull.test.cpp + tests/OverloadResolver.test.cpp tests/Parser.test.cpp + tests/PrettyPrinter.test.cpp tests/RegisterCallbacks.cpp tests/RegisterCallbacks.h tests/RequireTracer.test.cpp @@ -553,7 +560,6 @@ if(TARGET Luau.UnitTest) tests/ToDot.test.cpp tests/TopoSort.test.cpp tests/ToString.test.cpp - tests/Transpiler.test.cpp tests/TxnLog.test.cpp tests/TypeFunction.test.cpp tests/TypeFunction.user.test.cpp @@ -640,24 +646,21 @@ endif() if(TARGET Luau.Require) # Luau.Require Sources target_sources(Luau.Require PRIVATE - Require/Runtime/include/Luau/Require.h - - Require/Runtime/src/Navigation.h - Require/Runtime/src/RequireImpl.h - - Require/Runtime/src/Navigation.cpp - Require/Runtime/src/Require.cpp - Require/Runtime/src/RequireImpl.cpp) -endif() - -if(TARGET Luau.RequireNavigator) - # Luau.Require Sources - target_sources(Luau.RequireNavigator PRIVATE - Require/Navigator/include/Luau/PathUtilities.h - Require/Navigator/include/Luau/RequireNavigator.h - - Require/Navigator/src/PathUtilities.cpp - Require/Navigator/src/RequireNavigator.cpp) + # Public headers + Require/include/Luau/Require.h + Require/include/Luau/RequireNavigator.h + + # Internal headers + Require/src/Navigation.h + Require/src/PathUtilities.h + Require/src/RequireImpl.h + + # Source files + Require/src/Navigation.cpp + Require/src/PathUtilities.cpp + Require/src/Require.cpp + Require/src/RequireImpl.cpp + Require/src/RequireNavigator.cpp) endif() if(TARGET Luau.Web) diff --git a/VM/src/lapi.cpp b/VM/src/lapi.cpp index a943d897..ee804a8c 100644 --- a/VM/src/lapi.cpp +++ b/VM/src/lapi.cpp @@ -18,6 +18,8 @@ #include +LUAU_FASTFLAG(LuauResumeFix) + /* * This file contains most implementations of core Lua APIs from lua.h. * @@ -1143,10 +1145,17 @@ int lua_cpcall(lua_State* L, lua_CFunction func, void* ud) c.func = func; c.ud = ud; - int status = luaD_pcall(L, f_Ccall, &c, savestack(L, L->top), 0); + if (FFlag::LuauResumeFix) + { + return luaD_pcall(L, f_Ccall, &c, savestack(L, L->top), 0); + } + else + { + int status = luaD_pcall(L, f_Ccall, &c, savestack(L, L->top), 0); - adjustresults(L, 0); - return status; + adjustresults(L, 0); + return status; + } } int lua_status(lua_State* L) diff --git a/VM/src/laux.cpp b/VM/src/laux.cpp index 33bfa9b5..76a568f0 100644 --- a/VM/src/laux.cpp +++ b/VM/src/laux.cpp @@ -11,6 +11,8 @@ #include +LUAU_FASTFLAG(LuauStacklessPcall) + // convert a stack index to positive #define abs_index(L, i) ((i) > 0 || (i) <= LUA_REGISTRYINDEX ? (i) : lua_gettop(L) + (i) + 1) @@ -413,8 +415,17 @@ int luaL_callyieldable(lua_State* L, int nargs, int nresults) lua_call(L, nargs, nresults); - if (L->status == LUA_YIELD || L->status == LUA_BREAK) - return -1; // -1 is a marker for yielding from C + if (FFlag::LuauStacklessPcall) + { + // yielding means we need to propagate yield; resume will call continuation function later + if (isyielded(L)) + return C_CALL_YIELD; + } + else + { + if (L->status == LUA_YIELD || L->status == LUA_BREAK) + return -1; // -1 is a marker for yielding from C + } return cl->c.cont(L, LUA_OK); } diff --git a/VM/src/lbaselib.cpp b/VM/src/lbaselib.cpp index 8297ca04..06a1213c 100644 --- a/VM/src/lbaselib.cpp +++ b/VM/src/lbaselib.cpp @@ -12,8 +12,7 @@ #include #include -LUAU_DYNAMIC_FASTFLAGVARIABLE(LuauXpcallContNoYield, false) -LUAU_DYNAMIC_FASTFLAGVARIABLE(LuauXpcallContErrorHandling, false) +LUAU_FASTFLAG(LuauStacklessPcall) static void writestring(const char* s, size_t l) { @@ -286,7 +285,15 @@ static void luaB_pcallrun(lua_State* L, void* ud) { StkId func = (StkId)ud; - luaD_call(L, func, LUA_MULTRET); + if (FFlag::LuauStacklessPcall) + { + // if we can yield, schedule a call setup with postponed reentry + luaD_callint(L, func, LUA_MULTRET, lua_isyieldable(L) != 0); + } + else + { + luaD_call(L, func, LUA_MULTRET); + } } static int luaB_pcally(lua_State* L) @@ -300,12 +307,21 @@ static int luaB_pcally(lua_State* L) int status = luaD_pcall(L, luaB_pcallrun, func, savestack(L, func), 0); - // necessary to accomodate functions that return lots of values + // necessary to accommodate functions that return lots of values expandstacklimit(L, L->top); - // yielding means we need to propagate yield; resume will call continuation function later - if (status == 0 && (L->status == LUA_YIELD || L->status == LUA_BREAK)) - return -1; // -1 is a marker for yielding from C + if (FFlag::LuauStacklessPcall) + { + // yielding means we need to propagate yield; resume will call continuation function later + if (status == 0 && isyielded(L)) + return C_CALL_YIELD; + } + else + { + // yielding means we need to propagate yield; resume will call continuation function later + if (status == 0 && (L->status == LUA_YIELD || L->status == LUA_BREAK)) + return -1; // -1 is a marker for yielding from C + } // immediate return (error or success) lua_rawcheckstack(L, 1); @@ -354,9 +370,18 @@ static int luaB_xpcally(lua_State* L) // necessary to accommodate functions that return lots of values expandstacklimit(L, L->top); - // yielding means we need to propagate yield; resume will call continuation function later - if (status == 0 && (L->status == LUA_YIELD || L->status == LUA_BREAK)) - return -1; // -1 is a marker for yielding from C + if (FFlag::LuauStacklessPcall) + { + // yielding means we need to propagate yield; resume will call continuation function later + if (status == 0 && isyielded(L)) + return C_CALL_YIELD; + } + else + { + // yielding means we need to propagate yield; resume will call continuation function later + if (status == 0 && (L->status == LUA_YIELD || L->status == LUA_BREAK)) + return -1; // -1 is a marker for yielding from C + } // immediate return (error or success) lua_rawcheckstack(L, 1); @@ -369,10 +394,7 @@ static void luaB_xpcallerr(lua_State* L, void* ud) { StkId func = (StkId)ud; - if (DFFlag::LuauXpcallContNoYield) - luaD_callny(L, func, 1); - else - luaD_call(L, func, 1); + luaD_callny(L, func, 1); } static int luaB_xpcallcont(lua_State* L, int status) @@ -391,35 +413,24 @@ static int luaB_xpcallcont(lua_State* L, int status) lua_pushvalue(L, 1); // push error function on top of the stack lua_pushvalue(L, -3); // push error object (that was on top of the stack before) - if (DFFlag::LuauXpcallContErrorHandling) - { - StkId errf = L->top - 2; - ptrdiff_t oldtopoffset = savestack(L, errf); + StkId errf = L->top - 2; + ptrdiff_t oldtopoffset = savestack(L, errf); - int err = luaD_pcall(L, luaB_xpcallerr, errf, oldtopoffset, 0); + int err = luaD_pcall(L, luaB_xpcallerr, errf, oldtopoffset, 0); - if (err != 0) - { - int errstatus = status; - - // in general we preserve the status, except for cases when the error handler fails - // out of memory is treated specially because it's common for it to be cascading, in which case we preserve the code - if (status == LUA_ERRMEM && err == LUA_ERRMEM) - errstatus = LUA_ERRMEM; - else - errstatus = LUA_ERRERR; - - StkId oldtop = restorestack(L, oldtopoffset); - luaD_seterrorobj(L, errstatus, oldtop); - } - } - else + if (err != 0) { - StkId res = L->top - 3; - StkId errf = L->top - 2; + int errstatus = status; + + // in general we preserve the status, except for cases when the error handler fails + // out of memory is treated specially because it's common for it to be cascading, in which case we preserve the code + if (status == LUA_ERRMEM && err == LUA_ERRMEM) + errstatus = LUA_ERRMEM; + else + errstatus = LUA_ERRERR; - // note: we pass res as errfunc as a short cut; if errf generates an error, we'll try to execute res (boolean) and fail - luaD_pcall(L, luaB_xpcallerr, errf, savestack(L, errf), savestack(L, res)); + StkId oldtop = restorestack(L, oldtopoffset); + luaD_seterrorobj(L, errstatus, oldtop); } return 2; diff --git a/VM/src/ldo.cpp b/VM/src/ldo.cpp index 58a7e01a..21ae3b34 100644 --- a/VM/src/ldo.cpp +++ b/VM/src/ldo.cpp @@ -17,7 +17,8 @@ #include -LUAU_DYNAMIC_FASTFLAG(LuauXpcallContErrorHandling) +LUAU_FASTFLAGVARIABLE(LuauStacklessPcall) +LUAU_FASTFLAGVARIABLE(LuauResumeFix) // keep max stack allocation request under 1GB #define MAX_STACK_SIZE (int(1024 / sizeof(TValue)) * 1024 * 1024) @@ -251,7 +252,7 @@ void luaD_checkCstack(lua_State* L) luaD_throw(L, LUA_ERRERR); // error while handling stack error } -static void performcall(lua_State* L, StkId func, int nresults) +static void performcall(lua_State* L, StkId func, int nresults, bool preparereentry) { if (luau_precall(L, func, nresults) == PCRLUA) { // is a Lua function? @@ -261,7 +262,17 @@ static void performcall(lua_State* L, StkId func, int nresults) L->isactive = true; luaC_threadbarrier(L); - luau_execute(L); // call it + if (FFlag::LuauStacklessPcall) + { + if (preparereentry) + L->status = SCHEDULED_REENTRY; + else + luau_execute(L); + } + else + { + luau_execute(L); // call it + } if (!oldactive) L->isactive = false; @@ -274,7 +285,7 @@ static void performcall(lua_State* L, StkId func, int nresults) ** When returns, all the results are on the stack, starting at the original ** function position. */ -void luaD_call(lua_State* L, StkId func, int nresults) +void luaD_callint(lua_State* L, StkId func, int nresults, bool preparereentry) { if (++L->nCcalls >= LUAI_MAXCCALLS) luaD_checkCstack(L); @@ -296,9 +307,9 @@ void luaD_call(lua_State* L, StkId func, int nresults) ptrdiff_t funcoffset = savestack(L, func); ptrdiff_t cioffset = saveci(L, L->ci); - performcall(L, func, nresults); + performcall(L, func, nresults, preparereentry); - bool yielded = L->status == LUA_YIELD || L->status == LUA_BREAK; + bool yielded = FFlag::LuauStacklessPcall ? isyielded(L) : L->status == LUA_YIELD || L->status == LUA_BREAK; if (fromyieldableccall) { @@ -321,6 +332,11 @@ void luaD_call(lua_State* L, StkId func, int nresults) luaC_checkGC(L); } +void luaD_call(lua_State* L, StkId func, int nresults) +{ + luaD_callint(L, func, nresults, /* preparereentry */ false); +} + // Non-yieldable version of luaD_call, used primarily to call an error handler which cannot yield void luaD_callny(lua_State* L, StkId func, int nresults) { @@ -331,9 +347,12 @@ void luaD_callny(lua_State* L, StkId func, int nresults) ptrdiff_t funcoffset = savestack(L, func); - performcall(L, func, nresults); + performcall(L, func, nresults, /* preparereentry */ false); - LUAU_ASSERT(L->status != LUA_YIELD && L->status != LUA_BREAK); + if (FFlag::LuauStacklessPcall) + LUAU_ASSERT(!isyielded(L)); + else + LUAU_ASSERT(L->status != LUA_YIELD && L->status != LUA_BREAK); if (nresults != LUA_MULTRET) L->top = restorestack(L, funcoffset) + nresults; @@ -368,11 +387,14 @@ void luaD_seterrorobj(lua_State* L, int errcode, StkId oldtop) static void resume_continue(lua_State* L) { - // unroll Lua/C combined stack, processing continuations - while (L->status == 0 && L->ci > L->base_ci) + // unroll Luau/C combined stack, processing continuations + while ((FFlag::LuauStacklessPcall ? L->status == LUA_OK || L->status == SCHEDULED_REENTRY : L->status == LUA_OK) && L->ci > L->base_ci) { LUAU_ASSERT(L->baseCcalls == L->nCcalls); + if (FFlag::LuauStacklessPcall) + L->status = LUA_OK; + Closure* cl = curr_func(L); if (cl->isC) @@ -390,7 +412,7 @@ static void resume_continue(lua_State* L) } else { - // Lua continuation; it terminates at the end of the stack or at another C continuation + // Luau continuation; it terminates at the end of the stack or at another C continuation luau_execute(L); } } @@ -400,49 +422,110 @@ static void resume(lua_State* L, void* ud) { StkId firstArg = cast_to(StkId, ud); - if (L->status == 0) + if (FFlag::LuauStacklessPcall) { - // start coroutine - LUAU_ASSERT(L->ci == L->base_ci && firstArg >= L->base); - if (firstArg == L->base) - luaG_runerror(L, "cannot resume dead coroutine"); + if (L->status == LUA_OK) + { + // start coroutine + LUAU_ASSERT(L->ci == L->base_ci && firstArg >= L->base); + if (firstArg == L->base) + luaG_runerror(L, "cannot resume dead coroutine"); - if (luau_precall(L, firstArg - 1, LUA_MULTRET) != PCRLUA) - return; + int precallresult = luau_precall(L, firstArg - 1, LUA_MULTRET); - L->ci->flags |= LUA_CALLINFO_RETURN; - } - else - { - // resume from previous yield or break - LUAU_ASSERT(firstArg >= L->base); - LUAU_ASSERT(L->status == LUA_YIELD || L->status == LUA_BREAK); - L->status = 0; + // on scheduled reentry, we will continue into the yield-continue block below + if (L->status == SCHEDULED_REENTRY) + { + firstArg = L->base; + } + else + { + // C function is either completed or yielded, exit + if (precallresult != PCRLUA) + return; - Closure* cl = curr_func(L); + // mark to not return past the current Luau function frame + L->ci->flags |= LUA_CALLINFO_RETURN; + } + } - if (cl->isC) + // restore from yield or reentry + if (L->status != LUA_OK) { - // if the top stack frame is a C call continuation, resume_continue will handle that case - if (!cl->c.cont) + // resume from previous yield or break + LUAU_ASSERT(firstArg >= L->base); + LUAU_ASSERT(isyielded(L)); + L->status = LUA_OK; + + Closure* cl = curr_func(L); + + if (cl->isC) { - // finish interrupted execution of `OP_CALL' - luau_poscall(L, firstArg); + // if the top stack frame is a C call continuation, resume_continue will handle that case + if (!cl->c.cont) + { + // finish interrupted execution of `OP_CALL' + luau_poscall(L, firstArg); + } + else + { + // restore arguments we have protected for C continuation + L->base = L->ci->base; + } } else { - // restore arguments we have protected for C continuation + // yielded inside a hook: just continue its execution L->base = L->ci->base; } } + } + else + { + if (L->status == 0) + { + // start coroutine + LUAU_ASSERT(L->ci == L->base_ci && firstArg >= L->base); + if (firstArg == L->base) + luaG_runerror(L, "cannot resume dead coroutine"); + + if (luau_precall(L, firstArg - 1, LUA_MULTRET) != PCRLUA) + return; + + L->ci->flags |= LUA_CALLINFO_RETURN; + } else { - // yielded inside a hook: just continue its execution - L->base = L->ci->base; + // resume from previous yield or break + LUAU_ASSERT(firstArg >= L->base); + LUAU_ASSERT(L->status == LUA_YIELD || L->status == LUA_BREAK); + L->status = 0; + + Closure* cl = curr_func(L); + + if (cl->isC) + { + // if the top stack frame is a C call continuation, resume_continue will handle that case + if (!cl->c.cont) + { + // finish interrupted execution of `OP_CALL' + luau_poscall(L, firstArg); + } + else + { + // restore arguments we have protected for C continuation + L->base = L->ci->base; + } + } + else + { + // yielded inside a hook: just continue its execution + L->base = L->ci->base; + } } } - // run continuations from the stack; typically resumes Lua code and pcalls + // run continuations from the stack; typically resumes Luau code and pcalls resume_continue(L); } @@ -487,9 +570,13 @@ static void resume_handle(lua_State* L, void* ud) // make sure we don't run the handler the second time ci->flags &= ~LUA_CALLINFO_HANDLE; - // restore thread status to 0 since we're handling the error + // restore thread status to LUA_OK since we're handling the error int status = L->status; - L->status = 0; + + if (FFlag::LuauResumeFix) + L->status = LUA_OK; + else + L->status = 0; // push error object to stack top if it's not already there if (status != LUA_ERRRUN) @@ -511,8 +598,7 @@ static void resume_handle(lua_State* L, void* ud) // close eventual pending closures; this means it's now safe to restore stack luaF_close(L, L->ci->base); - if (DFFlag::LuauXpcallContErrorHandling) - restore_stack_limit(L); + restore_stack_limit(L); // finish cont call and restore stack to previous ci top luau_poscall(L, L->top - n); @@ -529,7 +615,7 @@ static int resume_error(lua_State* L, const char* msg, int narg) return LUA_ERRRUN; } -static void resume_finish(lua_State* L, int status) +static void resume_finish_DEPRECATED(lua_State* L, int status) { L->nCcalls = L->baseCcalls; L->isactive = false; @@ -546,11 +632,10 @@ static void resume_finish(lua_State* L, int status) } } -int lua_resume(lua_State* L, lua_State* from, int nargs) +static int resume_start(lua_State* L, lua_State* from, int nargs) { api_check(L, L->top - L->base >= nargs); - int status; if (L->status != LUA_YIELD && L->status != LUA_BREAK && (L->status != 0 || L->ci != L->base_ci)) return resume_error(L, "cannot resume non-suspended coroutine", nargs); @@ -563,49 +648,142 @@ int lua_resume(lua_State* L, lua_State* from, int nargs) luaC_threadbarrier(L); - status = luaD_rawrunprotected(L, resume, L->top - nargs); + return LUA_OK; +} +static int resume_finish(lua_State* L, int status) +{ CallInfo* ch = NULL; - while (status != 0 && (ch = resume_findhandler(L)) != NULL) + while (status != LUA_OK && (ch = resume_findhandler(L)) != NULL) { + if (FFlag::LuauStacklessPcall && lua_isyieldable(L) != 0 && L->global->cb.debugprotectederror) + { + L->global->cb.debugprotectederror(L); + + // debug hook is only allowed to break + if (L->status == LUA_BREAK) + { + status = LUA_OK; + break; + } + } + L->status = cast_byte(status); status = luaD_rawrunprotected(L, resume_handle, ch); } - resume_finish(L, status); - --L->nCcalls; + // C call count base was set to an incremented value of C call count in resume, so we decrement here + L->nCcalls = --L->baseCcalls; + + // make execution context non-yieldable as we are leaving the resume + L->baseCcalls = L->nCcalls; + + L->isactive = false; + + if (status != LUA_OK) + { + L->status = cast_byte(status); + luaD_seterrorobj(L, status, L->top); + L->ci->top = L->top; + } + else if (L->status == LUA_OK) + { + expandstacklimit(L, L->top); + } + return L->status; } -int lua_resumeerror(lua_State* L, lua_State* from) +int lua_resume(lua_State* L, lua_State* from, int nargs) { - api_check(L, L->top - L->base >= 1); + if (FFlag::LuauStacklessPcall || FFlag::LuauResumeFix) + { + if (int starterror = resume_start(L, from, nargs)) + return starterror; - int status; - if (L->status != LUA_YIELD && L->status != LUA_BREAK && (L->status != 0 || L->ci != L->base_ci)) - return resume_error(L, "cannot resume non-suspended coroutine", 1); + int status = luaD_rawrunprotected(L, resume, L->top - nargs); - L->nCcalls = from ? from->nCcalls : 0; - if (L->nCcalls >= LUAI_MAXCCALLS) - return resume_error(L, "C stack overflow", 1); + return resume_finish(L, status); + } + else + { + api_check(L, L->top - L->base >= nargs); - L->baseCcalls = ++L->nCcalls; - L->isactive = true; + int status; + if (L->status != LUA_YIELD && L->status != LUA_BREAK && (L->status != 0 || L->ci != L->base_ci)) + return resume_error(L, "cannot resume non-suspended coroutine", nargs); - luaC_threadbarrier(L); + L->nCcalls = from ? from->nCcalls : 0; + if (L->nCcalls >= LUAI_MAXCCALLS) + return resume_error(L, "C stack overflow", nargs); - status = LUA_ERRRUN; + L->baseCcalls = ++L->nCcalls; + L->isactive = true; - CallInfo* ch = NULL; - while (status != 0 && (ch = resume_findhandler(L)) != NULL) + luaC_threadbarrier(L); + + status = luaD_rawrunprotected(L, resume, L->top - nargs); + + CallInfo* ch = NULL; + while (status != 0 && (ch = resume_findhandler(L)) != NULL) + { + L->status = cast_byte(status); + status = luaD_rawrunprotected(L, resume_handle, ch); + } + + resume_finish_DEPRECATED(L, status); + --L->nCcalls; + return L->status; + } +} + +int lua_resumeerror(lua_State* L, lua_State* from) +{ + if (FFlag::LuauStacklessPcall || FFlag::LuauResumeFix) { - L->status = cast_byte(status); - status = luaD_rawrunprotected(L, resume_handle, ch); + if (int starterror = resume_start(L, from, 1)) + return starterror; + + int status = LUA_ERRRUN; + + if (CallInfo* ci = resume_findhandler(L)) + { + L->status = cast_byte(status); + status = luaD_rawrunprotected(L, resume_handle, ci); + } + + return resume_finish(L, status); } + else + { + api_check(L, L->top - L->base >= 1); - resume_finish(L, status); - --L->nCcalls; - return L->status; + int status; + if (L->status != LUA_YIELD && L->status != LUA_BREAK && (L->status != 0 || L->ci != L->base_ci)) + return resume_error(L, "cannot resume non-suspended coroutine", 1); + + L->nCcalls = from ? from->nCcalls : 0; + if (L->nCcalls >= LUAI_MAXCCALLS) + return resume_error(L, "C stack overflow", 1); + + L->baseCcalls = ++L->nCcalls; + L->isactive = true; + + luaC_threadbarrier(L); + + status = LUA_ERRRUN; + + CallInfo* ch = NULL; + while (status != 0 && (ch = resume_findhandler(L)) != NULL) + { + L->status = cast_byte(status); + status = luaD_rawrunprotected(L, resume_handle, ch); + } + + resume_finish_DEPRECATED(L, status); + --L->nCcalls; + return L->status; + } } int lua_yield(lua_State* L, int nresults) diff --git a/VM/src/ldo.h b/VM/src/ldo.h index dce1c64f..23f25ce6 100644 --- a/VM/src/ldo.h +++ b/VM/src/ldo.h @@ -45,16 +45,27 @@ #define saveci(L, p) ((char*)(p) - (char*)L->base_ci) #define restoreci(L, n) ((CallInfo*)((char*)L->base_ci + (n))) +#define isyielded(L) ((L)->status == LUA_YIELD || (L)->status == LUA_BREAK || (L)->status == SCHEDULED_REENTRY) + // results from luaD_precall #define PCRLUA 0 // initiated a call to a Lua function #define PCRC 1 // did a call to a C function #define PCRYIELD 2 // C function yielded +// return value for a yielded C call +#define C_CALL_YIELD -1 + +// luaD_call can 'yield' into an immediate reentry +// reentry will remove extra call frames from C call stack and continue execution +// this lua_State::status code is internal and should not be used by users +#define SCHEDULED_REENTRY 0x7f + // type of protected functions, to be ran by `runprotected' typedef void (*Pfunc)(lua_State* L, void* ud); LUAI_FUNC CallInfo* luaD_growCI(lua_State* L); +LUAI_FUNC void luaD_callint(lua_State* L, StkId func, int nresults, bool forreentry); LUAI_FUNC void luaD_call(lua_State* L, StkId func, int nresults); LUAI_FUNC void luaD_callny(lua_State* L, StkId func, int nresults); LUAI_FUNC int luaD_pcall(lua_State* L, Pfunc func, void* u, ptrdiff_t oldtop, ptrdiff_t ef); diff --git a/VM/src/lobject.h b/VM/src/lobject.h index 738b2edc..26da979f 100644 --- a/VM/src/lobject.h +++ b/VM/src/lobject.h @@ -314,14 +314,12 @@ typedef struct Proto { CommonHeader; - uint8_t nups; // number of upvalues uint8_t numparams; uint8_t is_vararg; uint8_t maxstacksize; uint8_t flags; - TValue* k; // constants used by the function Instruction* code; // function bytecode struct Proto** p; // functions defined inside the function @@ -330,7 +328,6 @@ typedef struct Proto void* execdata; uintptr_t exectarget; - uint8_t* lineinfo; // for each instruction, line number as a delta from baseline int* abslineinfo; // baseline line info, one entry for each 1< GCthreshold, run GC step size_t totalbytes; // number of bytes currently allocated + int gcgoal; // see LUAI_GCGOAL int gcstepmul; // see LUAI_GCSTEPMUL - int gcstepsize; // see LUAI_GCSTEPSIZE + int gcstepsize; // see LUAI_GCSTEPSIZE struct lua_Page* freepages[LUA_SIZECLASSES]; // free page linked list for each size class for non-collectable objects struct lua_Page* freegcopages[LUA_SIZECLASSES]; // free page linked list for each size class for collectable objects @@ -203,12 +200,12 @@ typedef struct global_State size_t memcatbytes[LUA_MEMORY_CATEGORIES]; // total amount of memory used by each memory category - struct lua_State* mainthread; - UpVal uvhead; // head of double-linked list of all open upvalues - struct LuaTable* mt[LUA_T_COUNT]; // metatables for basic types - TString* ttname[LUA_TDEADKEY]; // names for basic types - TString* tmname[TM_N]; // array with tag-method names + UpVal uvhead; // head of double-linked list of all open upvalues + struct LuaTable* mt[LUA_T_COUNT]; // metatables for basic types + // ServerLua: We extend this to include _all_ types + TString* ttname[LUA_TDEADKEY]; // names for basic types + TString* tmname[TM_N]; // array with tag-method names TValue pseudotemp; // storage for temporary values used in pseudo2addr @@ -251,31 +248,26 @@ struct lua_State bool isactive; // thread is currently executing, stack may be mutated without barriers bool singlestep; // call debugstep hook after each instruction - StkId top; // first free slot in the stack StkId base; // base of current function global_State* global; CallInfo* ci; // call info for current function StkId stack_last; // last free slot in the stack - StkId stack; // stack base - + StkId stack; // stack base CallInfo* end_ci; // points after end of ci array - CallInfo* base_ci; // array of CallInfo's - + CallInfo* base_ci; // array of CallInfo's int stacksize; - int size_ci; // size of array `base_ci' - + int size_ci; // size of array `base_ci' unsigned short nCcalls; // number of nested C calls - unsigned short baseCcalls; // nested C calls when resuming coroutine + unsigned short baseCcalls; // nested C calls when resuming coroutine int cachedslot; // when table operations or INDEX/NEWINDEX is invoked from Luau, what is the expected slot for lookup? - LuaTable* gt; // table of globals - UpVal* openupval; // list of open upvalues in this stack + UpVal* openupval; // list of open upvalues in this stack GCObject* gclist; TString* namecall; // when invoked from Luau using NAMECALL, what method do we need to invoke? diff --git a/VM/src/ltm.cpp b/VM/src/ltm.cpp index 58e2102a..7e4cfc18 100644 --- a/VM/src/ltm.cpp +++ b/VM/src/ltm.cpp @@ -35,7 +35,6 @@ const char* const luaT_typenames[] = { const char* const luaT_eventname[] = { // ORDER TM - "__index", "__newindex", "__mode", @@ -46,7 +45,6 @@ const char* const luaT_eventname[] = { "__eq", - "__add", "__sub", "__mul", @@ -56,7 +54,6 @@ const char* const luaT_eventname[] = { "__pow", "__unm", - "__lt", "__le", "__concat", diff --git a/VM/src/ltm.h b/VM/src/ltm.h index f3294b64..169a0b96 100644 --- a/VM/src/ltm.h +++ b/VM/src/ltm.h @@ -11,7 +11,6 @@ // clang-format off typedef enum { - TM_INDEX, TM_NEWINDEX, TM_MODE, @@ -22,7 +21,6 @@ typedef enum TM_EQ, // last tag method with `fast' access - TM_ADD, TM_SUB, TM_MUL, @@ -32,7 +30,6 @@ typedef enum TM_POW, TM_UNM, - TM_LT, TM_LE, TM_CONCAT, diff --git a/VM/src/lvmexecute.cpp b/VM/src/lvmexecute.cpp index 19085705..f44176a5 100644 --- a/VM/src/lvmexecute.cpp +++ b/VM/src/lvmexecute.cpp @@ -151,7 +151,7 @@ LUAU_NOINLINE void luau_callhook(lua_State* L, lua_Hook hook, void* userdata) ptrdiff_t ci_top = savestack(L, L->ci->top); int status = L->status; - // if the hook is called externally on a paused thread, we need to make sure the paused thread can emit Lua calls + // if the hook is called externally on a paused thread, we need to make sure the paused thread can emit Luau calls if (status == LUA_YIELD || status == LUA_BREAK) { L->status = 0; @@ -3237,8 +3237,8 @@ static void luau_execute(lua_State* L) int skip = LUAU_INSN_C(insn) - 1; uint32_t aux = *pc++; TValue* arg1 = VM_REG(LUAU_INSN_B(insn)); - TValue* arg2 = VM_REG(aux & 0xff); - TValue* arg3 = VM_REG((aux >> 8) & 0xff); + TValue* arg2 = VM_REG(LUAU_INSN_AUX_A(aux)); + TValue* arg3 = VM_REG(LUAU_INSN_AUX_B(aux)); LUAU_ASSERT(unsigned(pc - cl->l.p->code + skip) < unsigned(cl->l.p->sizecode)); @@ -3313,7 +3313,7 @@ static void luau_execute(lua_State* L) StkId ra = VM_REG(LUAU_INSN_A(insn)); static_assert(LUA_TNIL == 0, "we expect type-1 to be negative iff type is nil"); - // condition is equivalent to: int(ttisnil(ra)) != (aux >> 31) + // condition is equivalent to: int(ttisnil(ra)) != LUAU_INSN_AUX_NOT(aux) pc += int((ttype(ra) - 1) ^ aux) < 0 ? LUAU_INSN_D(insn) : 1; LUAU_ASSERT(unsigned(pc - cl->l.p->code) < unsigned(cl->l.p->sizecode)); VM_NEXT(); @@ -3325,7 +3325,7 @@ static void luau_execute(lua_State* L) uint32_t aux = *pc; StkId ra = VM_REG(LUAU_INSN_A(insn)); - pc += int(ttisboolean(ra) && bvalue(ra) == int(aux & 1)) != (aux >> 31) ? LUAU_INSN_D(insn) : 1; + pc += int(ttisboolean(ra) && bvalue(ra) == int(LUAU_INSN_AUX_KB(aux))) != LUAU_INSN_AUX_NOT(aux) ? LUAU_INSN_D(insn) : 1; LUAU_ASSERT(unsigned(pc - cl->l.p->code) < unsigned(cl->l.p->sizecode)); VM_NEXT(); } @@ -3335,18 +3335,18 @@ static void luau_execute(lua_State* L) Instruction insn = *pc++; uint32_t aux = *pc; StkId ra = VM_REG(LUAU_INSN_A(insn)); - TValue* kv = VM_KV(aux & 0xffffff); + TValue* kv = VM_KV(LUAU_INSN_AUX_KV(aux)); LUAU_ASSERT(ttisnumber(kv)); #if defined(__aarch64__) // On several ARM chips (Apple M1/M2, Neoverse N1), comparing the result of a floating-point comparison is expensive, and a branch // is much cheaper; on some 32-bit ARM chips (Cortex A53) the performance is about the same so we prefer less branchy variant there - if (aux >> 31) + if (LUAU_INSN_AUX_NOT(aux)) pc += !(ttisnumber(ra) && nvalue(ra) == nvalue(kv)) ? LUAU_INSN_D(insn) : 1; else pc += (ttisnumber(ra) && nvalue(ra) == nvalue(kv)) ? LUAU_INSN_D(insn) : 1; #else - pc += int(ttisnumber(ra) && nvalue(ra) == nvalue(kv)) != (aux >> 31) ? LUAU_INSN_D(insn) : 1; + pc += int(ttisnumber(ra) && nvalue(ra) == nvalue(kv)) != LUAU_INSN_AUX_NOT(aux) ? LUAU_INSN_D(insn) : 1; #endif LUAU_ASSERT(unsigned(pc - cl->l.p->code) < unsigned(cl->l.p->sizecode)); VM_NEXT(); @@ -3357,10 +3357,10 @@ static void luau_execute(lua_State* L) Instruction insn = *pc++; uint32_t aux = *pc; StkId ra = VM_REG(LUAU_INSN_A(insn)); - TValue* kv = VM_KV(aux & 0xffffff); + TValue* kv = VM_KV(LUAU_INSN_AUX_KV(aux)); LUAU_ASSERT(ttisstring(kv)); - pc += int(ttisstring(ra) && gcvalue(ra) == gcvalue(kv)) != (aux >> 31) ? LUAU_INSN_D(insn) : 1; + pc += int(ttisstring(ra) && gcvalue(ra) == gcvalue(kv)) != LUAU_INSN_AUX_NOT(aux) ? LUAU_INSN_D(insn) : 1; LUAU_ASSERT(unsigned(pc - cl->l.p->code) < unsigned(cl->l.p->sizecode)); VM_NEXT(); } diff --git a/fuzz/transpiler.cpp b/fuzz/prettyprinter.cpp similarity index 79% rename from fuzz/transpiler.cpp rename to fuzz/prettyprinter.cpp index 02872b02..467eee9d 100644 --- a/fuzz/transpiler.cpp +++ b/fuzz/prettyprinter.cpp @@ -1,6 +1,6 @@ // This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details -#include "Luau/Transpiler.h" #include "Luau/Common.h" +#include "Luau/PrettyPrinter.h" #include @@ -14,6 +14,6 @@ extern "C" int LLVMFuzzerTestOneInput(const uint8_t* Data, size_t Size) flag->value = true; } - Luau::transpile(std::string_view(reinterpret_cast(Data), Size)); + Luau::prettyPrint(std::string_view(reinterpret_cast(Data), Size)); return 0; } diff --git a/fuzz/proto.cpp b/fuzz/proto.cpp index d6da9d8e..ea864b8a 100644 --- a/fuzz/proto.cpp +++ b/fuzz/proto.cpp @@ -12,8 +12,8 @@ #include "Luau/Linter.h" #include "Luau/ModuleResolver.h" #include "Luau/Parser.h" +#include "Luau/PrettyPrinter.h" #include "Luau/ToString.h" -#include "Luau/Transpiler.h" #include "Luau/TypeInfer.h" #include "lua.h" @@ -36,7 +36,7 @@ const bool kFuzzCompiler = getEnvParam("LUAU_FUZZ_COMPILER", true); const bool kFuzzLinter = getEnvParam("LUAU_FUZZ_LINTER", true); const bool kFuzzTypeck = getEnvParam("LUAU_FUZZ_TYPE_CHECK", true); const bool kFuzzVM = getEnvParam("LUAU_FUZZ_VM", true); -const bool kFuzzTranspile = getEnvParam("LUAU_FUZZ_TRANSPILE", true); +const bool kFuzzPrettyPrint = getEnvParam("LUAU_FUZZ_PRETTY_PRINT", true); const bool kFuzzCodegenVM = getEnvParam("LUAU_FUZZ_CODEGEN_VM", true); const bool kFuzzCodegenAssembly = getEnvParam("LUAU_FUZZ_CODEGEN_ASM", true); const bool kFuzzUseNewSolver = getEnvParam("LUAU_FUZZ_NEW_SOLVER", false); @@ -353,12 +353,12 @@ DEFINE_PROTO_FUZZER(const luau::ModuleSet& message) } } - if (kFuzzTranspile) + if (kFuzzPrettyPrint) { for (Luau::ParseResult& parseResult : parseResults) { if (parseResult.root) - transpileWithTypes(*parseResult.root); + prettyPrintWithTypes(*parseResult.root); } } diff --git a/tests/AssemblyBuilderX64.test.cpp b/tests/AssemblyBuilderX64.test.cpp index 6acb348e..5700307c 100644 --- a/tests/AssemblyBuilderX64.test.cpp +++ b/tests/AssemblyBuilderX64.test.cpp @@ -578,7 +578,7 @@ TEST_CASE_FIXTURE(AssemblyBuilderX64Fixture, "AVXTernaryInstructionForms") vroundsd(xmm8, xmm13, xmmword[r13 + rdx], RoundingModeX64::RoundToPositiveInfinity), 0xc4, 0x43, 0x11, 0x0b, 0x44, 0x15, 0x00, 0x0a ); SINGLE_COMPARE(vroundsd(xmm9, xmm14, xmmword[rcx + r10], RoundingModeX64::RoundToZero), 0xc4, 0x23, 0x09, 0x0b, 0x0c, 0x11, 0x0b); - SINGLE_COMPARE(vblendvpd(xmm7, xmm12, xmm5, xmmword[rcx + r10]), 0xc4, 0xa3, 0x19, 0x4b, 0x3c, 0x11, 0x50); + SINGLE_COMPARE(vblendvpd(xmm7, xmm12, xmmword[rcx + r10], xmm5), 0xc4, 0xa3, 0x19, 0x4b, 0x3c, 0x11, 0x50); 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); diff --git a/tests/Autocomplete.test.cpp b/tests/Autocomplete.test.cpp index 74692ede..2dca8fc4 100644 --- a/tests/Autocomplete.test.cpp +++ b/tests/Autocomplete.test.cpp @@ -16,14 +16,12 @@ #include +LUAU_DYNAMIC_FASTINT(LuauSubtypingRecursionLimit) + LUAU_FASTFLAG(LuauTraceTypesInNonstrictMode2) LUAU_FASTFLAG(LuauSetMetatableDoesNotTimeTravel) LUAU_FASTINT(LuauTypeInferRecursionLimit) -LUAU_FASTFLAG(LuauIncludeBreakContinueStatements) -LUAU_FASTFLAG(LuauSolverAgnosticStringification) -LUAU_FASTFLAG(LuauSuggestHotComments) LUAU_FASTFLAG(LuauUnfinishedRepeatAncestryFix) -LUAU_FASTFLAG(LuauParametrizedAttributeSyntax) LUAU_FASTFLAG(LuauAutocompleteAttributes) using namespace Luau; @@ -1604,9 +1602,6 @@ return target(a.@1 TEST_CASE_FIXTURE(ACFixture, "type_correct_suggestion_in_table") { - if (FFlag::LuauSolverV2) // CLI-116815 Autocomplete cannot suggest keys while autocompleting inside of a table - return; - check(R"( type Foo = { a: number, b: string } local a = { one = 4, two = "hello" } @@ -2203,7 +2198,6 @@ local abc = bar(@1) TEST_CASE_FIXTURE(ACFixture, "type_correct_sealed_table") { - ScopedFastFlag sff{FFlag::LuauSolverAgnosticStringification, true}; check(R"( local function f(a: { x: number, y: number }) return a.x + a.y end @@ -3908,6 +3902,7 @@ TEST_CASE_FIXTURE(ACFixture, "autocomplete_subtyping_recursion_limit") return; ScopedFastInt luauTypeInferRecursionLimit{FInt::LuauTypeInferRecursionLimit, 10}; + ScopedFastInt luauSubtypingRecursionLimit{DFInt::LuauSubtypingRecursionLimit, 10}; const int parts = 100; std::string source; @@ -4723,8 +4718,6 @@ TEST_CASE_FIXTURE(ACBuiltinsFixture, "autocomplete_via_bidirectional_self") TEST_CASE_FIXTURE(ACFixture, "autocomplete_include_break_continue_in_loop") { - ScopedFastFlag sff{FFlag::LuauIncludeBreakContinueStatements, true}; - check(R"(for x in y do @1 if true then @@ -4745,8 +4738,6 @@ TEST_CASE_FIXTURE(ACFixture, "autocomplete_include_break_continue_in_loop") TEST_CASE_FIXTURE(ACFixture, "autocomplete_exclude_break_continue_outside_loop") { - ScopedFastFlag sff{FFlag::LuauIncludeBreakContinueStatements, true}; - check(R"(@1if true then @2 end)"); @@ -4763,8 +4754,6 @@ TEST_CASE_FIXTURE(ACFixture, "autocomplete_exclude_break_continue_outside_loop") TEST_CASE_FIXTURE(ACFixture, "autocomplete_exclude_break_continue_function_boundary") { - ScopedFastFlag sff{FFlag::LuauIncludeBreakContinueStatements, true}; - check(R"(for i = 1, 10 do local function helper() @1 @@ -4779,8 +4768,6 @@ TEST_CASE_FIXTURE(ACFixture, "autocomplete_exclude_break_continue_function_bound TEST_CASE_FIXTURE(ACFixture, "autocomplete_exclude_break_continue_in_param") { - ScopedFastFlag sff{FFlag::LuauIncludeBreakContinueStatements, true}; - check(R"(while @1 do end)"); @@ -4792,8 +4779,6 @@ TEST_CASE_FIXTURE(ACFixture, "autocomplete_exclude_break_continue_in_param") TEST_CASE_FIXTURE(ACFixture, "autocomplete_exclude_break_continue_incomplete_while") { - ScopedFastFlag sff{FFlag::LuauIncludeBreakContinueStatements, true}; - check("while @1"); auto ac = autocomplete('1'); @@ -4804,8 +4789,6 @@ TEST_CASE_FIXTURE(ACFixture, "autocomplete_exclude_break_continue_incomplete_whi TEST_CASE_FIXTURE(ACFixture, "autocomplete_exclude_break_continue_incomplete_for") { - ScopedFastFlag sff{FFlag::LuauIncludeBreakContinueStatements, true}; - check("for @1 in @2 do"); auto ac = autocomplete('1'); @@ -4821,8 +4804,6 @@ TEST_CASE_FIXTURE(ACFixture, "autocomplete_exclude_break_continue_incomplete_for TEST_CASE_FIXTURE(ACFixture, "autocomplete_exclude_break_continue_expr_func") { - ScopedFastFlag sff{FFlag::LuauIncludeBreakContinueStatements, true}; - check(R"(while true do local _ = function () @1 @@ -4837,8 +4818,6 @@ TEST_CASE_FIXTURE(ACFixture, "autocomplete_exclude_break_continue_expr_func") TEST_CASE_FIXTURE(ACFixture, "autocomplete_include_break_continue_in_repeat") { - ScopedFastFlag sff{FFlag::LuauIncludeBreakContinueStatements, true}; - check(R"(repeat @1 until foo())"); @@ -4851,8 +4830,6 @@ TEST_CASE_FIXTURE(ACFixture, "autocomplete_include_break_continue_in_repeat") TEST_CASE_FIXTURE(ACFixture, "autocomplete_include_break_continue_in_nests") { - ScopedFastFlag sff{FFlag::LuauIncludeBreakContinueStatements, true}; - check(R"(while ((function () while true do @1 @@ -4868,8 +4845,6 @@ TEST_CASE_FIXTURE(ACFixture, "autocomplete_include_break_continue_in_nests") TEST_CASE_FIXTURE(ACFixture, "autocomplete_exclude_break_continue_in_incomplete_loop") { - ScopedFastFlag sff{FFlag::LuauIncludeBreakContinueStatements, true}; - check(R"(while foo() do @1)"); @@ -4882,8 +4857,6 @@ TEST_CASE_FIXTURE(ACFixture, "autocomplete_exclude_break_continue_in_incomplete_ TEST_CASE_FIXTURE(ACFixture, "autocomplete_suggest_hot_comments") { - ScopedFastFlag sff{FFlag::LuauSuggestHotComments, true}; - check("--!@1"); auto ac = autocomplete('1'); @@ -4943,7 +4916,7 @@ TEST_CASE_FIXTURE(ACFixture, "autocomplete_method_in_unfinished_while_body") TEST_CASE_FIXTURE(ACBuiltinsFixture, "autocomplete_empty_attribute") { - ScopedFastFlag sff[]{{FFlag::LuauParametrizedAttributeSyntax, true}, {FFlag::LuauAutocompleteAttributes, true}}; + ScopedFastFlag sff[]{{FFlag::LuauAutocompleteAttributes, true}}; check(R"( \@@1 @@ -4958,7 +4931,7 @@ TEST_CASE_FIXTURE(ACBuiltinsFixture, "autocomplete_empty_attribute") TEST_CASE_FIXTURE(ACBuiltinsFixture, "autocomplete_deprecated_attribute") { - ScopedFastFlag sff[]{{FFlag::LuauParametrizedAttributeSyntax, true}, {FFlag::LuauAutocompleteAttributes, true}}; + ScopedFastFlag sff[]{{FFlag::LuauAutocompleteAttributes, true}}; check(R"( \@dep@1 @@ -4973,7 +4946,7 @@ TEST_CASE_FIXTURE(ACBuiltinsFixture, "autocomplete_deprecated_attribute") TEST_CASE_FIXTURE(ACBuiltinsFixture, "autocomplete_empty_braced_attribute") { - ScopedFastFlag sff[]{{FFlag::LuauParametrizedAttributeSyntax, true}, {FFlag::LuauAutocompleteAttributes, true}}; + ScopedFastFlag sff[]{{FFlag::LuauAutocompleteAttributes, true}}; check(R"( \@[@1] @@ -4988,7 +4961,7 @@ TEST_CASE_FIXTURE(ACBuiltinsFixture, "autocomplete_empty_braced_attribute") TEST_CASE_FIXTURE(ACBuiltinsFixture, "autocomplete_deprecated_braced_attribute") { - ScopedFastFlag sff[]{{FFlag::LuauParametrizedAttributeSyntax, true}, {FFlag::LuauAutocompleteAttributes, true}}; + ScopedFastFlag sff[]{{FFlag::LuauAutocompleteAttributes, true}}; check(R"( \@[dep@1] diff --git a/tests/Compiler.test.cpp b/tests/Compiler.test.cpp index 96b750ff..af1dce13 100644 --- a/tests/Compiler.test.cpp +++ b/tests/Compiler.test.cpp @@ -23,8 +23,9 @@ LUAU_FASTINT(LuauCompileInlineThresholdMaxBoost) LUAU_FASTINT(LuauCompileLoopUnrollThreshold) LUAU_FASTINT(LuauCompileLoopUnrollThresholdMaxBoost) LUAU_FASTINT(LuauRecursionLimit) -LUAU_FASTFLAG(LuauStringConstFolding) +LUAU_FASTFLAG(LuauStringConstFolding2) LUAU_FASTFLAG(LuauCompileTypeofFold) +LUAU_FASTFLAG(LuauInterpStringConstFolding) using namespace Luau; @@ -1419,11 +1420,11 @@ TEST_CASE("InterpStringWithNoExpressions") TEST_CASE("InterpStringZeroCost") { CHECK_EQ( - "\n" + compileFunction0(R"(local _ = `hello, {"world"}!`)"), + "\n" + compileFunction0(R"(local _ = `hello, {42}!`)"), R"( LOADK R1 K0 ['hello, %*!'] -LOADK R3 K1 ['world'] -NAMECALL R1 R1 K2 ['format'] +LOADN R3 42 +NAMECALL R1 R1 K1 ['format'] CALL R1 2 1 MOVE R0 R1 RETURN R0 0 @@ -1436,7 +1437,7 @@ TEST_CASE("InterpStringRegisterCleanup") CHECK_EQ( "\n" + compileFunction0(R"( local a, b, c = nil, "um", "uh oh" - a = `foo{"bar"}` + a = `foo{42}` print(a) )"), @@ -1445,11 +1446,11 @@ LOADNIL R0 LOADK R1 K0 ['um'] LOADK R2 K1 ['uh oh'] LOADK R3 K2 ['foo%*'] -LOADK R5 K3 ['bar'] -NAMECALL R3 R3 K4 ['format'] +LOADN R5 42 +NAMECALL R3 R3 K3 ['format'] CALL R3 2 1 MOVE R0 R3 -GETIMPORT R3 6 [print] +GETIMPORT R3 5 [print] MOVE R4 R0 CALL R3 1 0 RETURN R0 0 @@ -1463,6 +1464,51 @@ TEST_CASE("InterpStringRegisterLimit") CHECK_THROWS_AS(compileFunction0(("local a = `" + rep("{1}", 253) + "`").c_str()), std::exception); } +TEST_CASE("InterpStringConstFold") +{ + ScopedFastFlag sff{FFlag::LuauInterpStringConstFolding, true}; + + CHECK_EQ( + "\n" + compileFunction0(R"(local empty = ""; return `{empty}`)"), + R"( +LOADK R0 K0 [''] +RETURN R0 1 +)" + ); + + CHECK_EQ( + "\n" + compileFunction0(R"(local world = "world"; return `hello, {world}!`)"), + R"( +LOADK R0 K0 ['hello, world!'] +RETURN R0 1 +)" + ); + + CHECK_EQ( + "\n" + compileFunction0(R"(local not_string = 42; local world = "world"; return `hello, {world} {not_string}!`)"), + R"( +LOADK R1 K0 ['hello, world %*!'] +LOADN R3 42 +NAMECALL R1 R1 K1 ['format'] +CALL R1 2 1 +MOVE R0 R1 +RETURN R0 1 +)" + ); + + CHECK_EQ( + "\n" + compileFunction0(R"(local not_string = 42; local str = "%s%s%s"; return `hello, {str} {not_string}!`)"), + R"( +LOADK R1 K0 ['hello, %%s%%s%%s %*!'] +LOADN R3 42 +NAMECALL R1 R1 K1 ['format'] +CALL R1 2 1 +MOVE R0 R1 +RETURN R0 1 +)" + ); +} + TEST_CASE("ConstantFoldArith") { CHECK_EQ("\n" + compileFunction0("return 10 + 2"), R"( @@ -9407,7 +9453,32 @@ RETURN R1 7 TEST_CASE("ConstStringFolding") { - ScopedFastFlag sff{FFlag::LuauStringConstFolding, true}; + ScopedFastFlag sff{FFlag::LuauStringConstFolding2, true}; + + CHECK_EQ( + "\n" + compileFunction(R"(return "" .. "")", 0, 2), + R"( +LOADK R0 K0 [''] +RETURN R0 1 +)" + ); + + CHECK_EQ( + "\n" + compileFunction(R"(return "a" .. "")", 0, 2), + R"( +LOADK R0 K0 ['a'] +RETURN R0 1 +)" + ); + + CHECK_EQ( + "\n" + compileFunction(R"(return "" .. "a")", 0, 2), + R"( +LOADK R0 K0 ['a'] +RETURN R0 1 +)" + ); + CHECK_EQ( "\n" + compileFunction(R"(local hello = "hello"; local world = "world"; return hello .. " " .. world)", 0, 2), R"( diff --git a/tests/Config.test.cpp b/tests/Config.test.cpp index 690c4c37..bc755cd3 100644 --- a/tests/Config.test.cpp +++ b/tests/Config.test.cpp @@ -1,6 +1,8 @@ // This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details #include "Luau/Config.h" #include "Luau/Frontend.h" +#include "Luau/LinterConfig.h" +#include "Luau/LuauConfig.h" #include "Fixture.h" #include "ScopedFlags.h" @@ -8,6 +10,11 @@ #include "doctest.h" #include +#include +#include +#include +#include +#include using namespace Luau; @@ -175,3 +182,184 @@ TEST_CASE("lint_rules_compat") } TEST_SUITE_END(); + +TEST_SUITE_BEGIN("ConfigLuauTest"); + +TEST_CASE("extract_configuration") +{ + std::string source = R"( + local config = {} + config.luau = {} + + config.luau.languagemode = "strict" + config.luau.lint = { + ["*"] = true, + LocalUnused = false + } + config.luau.linterrors = true + config.luau.typeerrors = true + config.luau.globals = {"expect"} + config.luau.aliases = { + src = "./src" + } + + return config + )"; + + std::string error; + std::optional configTable = extractConfig(source, InterruptCallbacks{}, &error); + REQUIRE(configTable); + + CHECK(configTable->size() == 1); + REQUIRE(configTable->contains("luau")); + ConfigTable* luau = (*configTable)["luau"].get_if(); + REQUIRE(luau); + CHECK(luau->size() == 6); + + REQUIRE(luau->contains("languagemode")); + std::string* languageMode = (*luau)["languagemode"].get_if(); + REQUIRE(languageMode); + CHECK(*languageMode == "strict"); + + REQUIRE(luau->contains("lint")); + ConfigTable* lint = (*luau)["lint"].get_if(); + REQUIRE(lint); + CHECK(lint->size() == 2); + REQUIRE(lint->contains("*")); + bool* all = (*lint)["*"].get_if(); + REQUIRE(all); + CHECK(*all); + bool* localUnused = (*lint)["LocalUnused"].get_if(); + REQUIRE(localUnused); + CHECK(!(*localUnused)); + + REQUIRE(luau->contains("linterrors")); + bool* lintErrors = (*luau)["linterrors"].get_if(); + REQUIRE(lintErrors); + CHECK(*lintErrors); + + REQUIRE(luau->contains("typeerrors")); + bool* typeErrors = (*luau)["typeerrors"].get_if(); + REQUIRE(typeErrors); + CHECK(*typeErrors); + + REQUIRE(luau->contains("globals")); + ConfigTable* globalsTable = (*luau)["globals"].get_if(); + REQUIRE(globalsTable); + CHECK(globalsTable->size() == 1); + REQUIRE(globalsTable->contains(1)); + std::string* global = (*globalsTable)[1].get_if(); + REQUIRE(global); + CHECK(*global == "expect"); + + REQUIRE(luau->contains("aliases")); + ConfigTable* aliases = (*luau)["aliases"].get_if(); + REQUIRE(aliases); + CHECK(aliases->size() == 1); + REQUIRE(aliases->contains("src")); + std::string* alias = (*aliases)["src"].get_if(); + REQUIRE(alias); + CHECK(*alias == "./src"); +} + +TEST_CASE("extract_luau_configuration") +{ + std::string source = R"( + local config = {} + config.luau = {} + + config.luau.languagemode = "strict" + config.luau.lint = { + ["*"] = true, + LocalUnused = false + } + config.luau.linterrors = true + config.luau.typeerrors = true + config.luau.globals = {"expect"} + config.luau.aliases = { + src = "./src" + } + + return config + )"; + + ConfigOptions::AliasOptions aliasOptions; + aliasOptions.configLocation = "/some/path"; + aliasOptions.overwriteAliases = true; + + Config config; + std::optional error = extractLuauConfig(source, config, std::move(aliasOptions), InterruptCallbacks{}); + REQUIRE(!error); + + CHECK_EQ(config.mode, Mode::Strict); + + for (LintWarning::Code code = static_cast(0); code <= LintWarning::Code::Code__Count; code = LintWarning::Code(int(code) + 1)) + { + if (code == LintWarning::Code_LocalUnused) + CHECK(!config.enabledLint.isEnabled(code)); + else + CHECK(config.enabledLint.isEnabled(code)); + } + + CHECK_EQ(config.lintErrors, true); + CHECK_EQ(config.typeErrors, true); + + CHECK(config.globals.size() == 1); + CHECK(config.globals[0] == "expect"); + + CHECK(config.aliases.size() == 1); + REQUIRE(config.aliases.contains("src")); + CHECK(config.aliases["src"].value == "./src"); +} + +TEST_CASE("yielded_configuration") +{ + std::string source = R"( + coroutine.yield() + )"; + + std::string error; + std::optional configTable = extractConfig(source, InterruptCallbacks{}, &error); + REQUIRE(!configTable); + CHECK(error == "configuration execution cannot yield"); +} + +TEST_CASE("interrupt_execution" * doctest::timeout(2)) +{ + std::string source = R"( + while true do end + )"; + + std::string error; + std::optional configTable = extractConfig( + source, + { + nullptr, + [](lua_State* L, int gc) + { + throw std::runtime_error("interrupted"); + }, + }, + &error + ); + REQUIRE(!configTable); + CHECK(error.find("interrupted") != std::string_view::npos); +} + +TEST_CASE("validate_return_value") +{ + std::vector> testCases; + testCases.emplace_back("", "configuration must return exactly one value"); + testCases.emplace_back("return {}, {}", "configuration must return exactly one value"); + testCases.emplace_back("return 'a string'", "configuration did not return a table"); + + for (const auto& [source, expectedError] : testCases) + { + std::string error; + std::optional configTable = extractConfig(source, InterruptCallbacks{}, &error); + REQUIRE(!configTable); + CHECK(error == expectedError); + } +} + +TEST_SUITE_END(); diff --git a/tests/Conformance.test.cpp b/tests/Conformance.test.cpp index b1e94e2a..3d64124c 100644 --- a/tests/Conformance.test.cpp +++ b/tests/Conformance.test.cpp @@ -34,18 +34,14 @@ void luaC_validate(lua_State* L); // internal functions, declared in lvm.h - not exposed via lua.h void luau_callhook(lua_State* L, lua_Hook hook, void* userdata); -LUAU_DYNAMIC_FASTFLAG(LuauXpcallContErrorHandling) LUAU_FASTFLAG(DebugLuauAbortingChecks) -LUAU_FASTFLAG(LuauCodeGenDirectBtest) LUAU_FASTINT(CodegenHeuristicsInstructionLimit) LUAU_FASTFLAG(LuauVectorLerp) LUAU_FASTFLAG(LuauCompileVectorLerp) -LUAU_FASTFLAG(LuauTypeCheckerVectorLerp) -LUAU_FASTFLAG(LuauCodeGenVectorLerp) -LUAU_DYNAMIC_FASTFLAG(LuauXpcallContNoYield) -LUAU_FASTFLAG(LuauCodeGenBetterBytecodeAnalysis) -LUAU_FASTFLAG(LuauCodeGenRegAutoSpillA64) -LUAU_FASTFLAG(LuauCodeGenRestoreFromSplitStore) +LUAU_FASTFLAG(LuauTypeCheckerVectorLerp2) +LUAU_FASTFLAG(LuauCodeGenVectorLerp2) +LUAU_FASTFLAG(LuauStacklessPcall) +LUAU_FASTFLAG(LuauResumeFix) static lua_CompileOptions defaultOptions() { @@ -196,7 +192,7 @@ using StateRef = std::unique_ptr; static StateRef runConformance( const char* name, void (*setup)(lua_State* L) = nullptr, - void (*yield)(lua_State* L) = nullptr, + bool (*yield)(lua_State* L) = nullptr, lua_State* initialLuaState = nullptr, lua_CompileOptions* options = nullptr, bool skipCodegen = false, @@ -287,31 +283,52 @@ static StateRef runConformance( int result = luau_load(L, chunkname.c_str(), bytecode, bytecodeSize, 0); free(bytecode); - // ServerLua: We can also fixall down here if we never expect to throw away the protos. - // lua_fixallcollectable(L); + Luau::CodeGen::CompilationOptions nativeOpts = codegenOptions ? *codegenOptions : defaultCodegenOptions(); if (result == 0 && codegen && !skipCodegen && luau_codegen_supported()) - { - Luau::CodeGen::CompilationOptions nativeOpts = codegenOptions ? *codegenOptions : defaultCodegenOptions(); - Luau::CodeGen::compile(L, -1, nativeOpts); + // Extra test for lowering on both platforms with assembly generation + if (luau_codegen_supported()) + { + // ServerLua: let Ares access the compilation func eris_set_compile_func([](lua_State *L, int idx) { Luau::CodeGen::compile(L, idx, Luau::CodeGen::CodeGen_ColdFunctions); }); + + Luau::CodeGen::AssemblyOptions assemblyOptions; + assemblyOptions.compilationOptions = nativeOpts; + + assemblyOptions.includeAssembly = true; + assemblyOptions.includeIr = true; + assemblyOptions.includeOutlinedCode = true; + assemblyOptions.includeIrTypes = true; + + assemblyOptions.target = Luau::CodeGen::AssemblyOptions::A64; + std::string a64 = Luau::CodeGen::getAssembly(L, -1, assemblyOptions); + CHECK(!a64.empty()); + + assemblyOptions.target = Luau::CodeGen::AssemblyOptions::X64_SystemV; + std::string x64 = Luau::CodeGen::getAssembly(L, -1, assemblyOptions); + CHECK(!x64.empty()); } int status = (result == 0) ? lua_resume(L, nullptr, 0) : LUA_ERRSYNTAX; while (yield && (status == LUA_YIELD || status == LUA_BREAK)) { - yield(L); + bool resumeError = yield(L); + // ServerLua: Do some GCs to make sure we didn't hose things for (int i=0; i<5; ++i) { lua_gc(L, LUA_GCCOLLECT, 0); } - status = lua_resume(L, nullptr, 0); + + if (resumeError) + status = lua_resumeerror(L, nullptr); + else + status = lua_resume(L, nullptr, 0); } luaC_validate(L); @@ -1063,6 +1080,7 @@ TEST_CASE("Eris Conformance Tests") [](lua_State *L) { CHECK((lua_gettop(L) == 1)); _serializedEris = std::string(lua_tostring(L, -1), lua_strlen(L, -1)); + return false; } ); @@ -1190,6 +1208,8 @@ TEST_CASE("Literals") TEST_CASE("Errors") { + ScopedFastFlag luauStacklessPcall{FFlag::LuauStacklessPcall, true}; + runConformance("errors.luau"); } @@ -1276,8 +1296,6 @@ TEST_CASE("UTF8") TEST_CASE("Coroutine") { - ScopedFastFlag luauXpcallContNoYield{DFFlag::LuauXpcallContNoYield, true}; - runConformance("coroutine.luau"); } @@ -1292,7 +1310,7 @@ static int cxxthrow(lua_State* L) TEST_CASE("PCall") { - ScopedFastFlag luauXpcallContErrorHandling{DFFlag::LuauXpcallContErrorHandling, true}; + ScopedFastFlag luauStacklessPcall{FFlag::LuauStacklessPcall, true}; runConformance( "pcall.luau", @@ -1612,10 +1630,9 @@ TEST_CASE("VectorLibrary") { ScopedFastFlag _[]{ {FFlag::LuauCompileVectorLerp, true}, - {FFlag::LuauTypeCheckerVectorLerp, true}, + {FFlag::LuauTypeCheckerVectorLerp2, true}, {FFlag::LuauVectorLerp, true}, - {FFlag::LuauCodeGenVectorLerp, true}, - {FFlag::LuauCodeGenBetterBytecodeAnalysis, true} + {FFlag::LuauCodeGenVectorLerp2, true} }; lua_CompileOptions copts = defaultOptions(); @@ -1835,7 +1852,7 @@ TEST_CASE("Debugger") ); lua_setglobal(L, "breakpoint"); }, - [](lua_State* L) + [](lua_State* L) -> bool { CHECK(breakhits % 2 == 1); @@ -1933,6 +1950,8 @@ TEST_CASE("Debugger") lua_resume(interruptedthread, nullptr, 0); interruptedthread = nullptr; } + + return false; }, nullptr, &copts, @@ -1969,7 +1988,7 @@ TEST_CASE("InterruptInspection") skipbreak = !skipbreak; }; }, - [](lua_State* L) + [](lua_State* L) -> bool { // Debug info can be retrieved from every location lua_Debug ar = {}; @@ -1984,6 +2003,8 @@ TEST_CASE("InterruptInspection") }, nullptr ); + + return false; }, nullptr, nullptr, @@ -2064,7 +2085,7 @@ TEST_CASE("NDebugGetUpValue") runConformance( "ndebug_upvalues.luau", nullptr, - [](lua_State* L) + [](lua_State* L) -> bool { lua_checkstack(L, LUA_MINSTACK); @@ -2079,6 +2100,8 @@ TEST_CASE("NDebugGetUpValue") CHECK(strcmp(u, "") == 0); CHECK(lua_tointeger(L, -1) == 5); lua_pop(L, 2); + + return false; }, nullptr, &copts, @@ -2310,6 +2333,8 @@ static int cpcallTest(lua_State* L) TEST_CASE("ApiCalls") { + ScopedFastFlag luauResumeFix{FFlag::LuauResumeFix, true}; + StateRef globalState = runConformance("apicalls.luau", nullptr, nullptr, lua_newstate(limitedRealloc, nullptr)); lua_State* L = globalState.get(); @@ -2324,17 +2349,49 @@ TEST_CASE("ApiCalls") lua_pop(L, 1); } + // lua_call variadic + { + lua_getfield(L, LUA_GLOBALSINDEX, "getnresults"); + lua_pushinteger(L, 200); + lua_call(L, 1, LUA_MULTRET); + CHECK(lua_gettop(L) == 200); + lua_pop(L, 200); + } + // lua_pcall { lua_getfield(L, LUA_GLOBALSINDEX, "add"); lua_pushnumber(L, 40); lua_pushnumber(L, 2); - lua_pcall(L, 2, 1, 0); + int status = lua_pcall(L, 2, 1, 0); + CHECK(status == LUA_OK); CHECK(lua_isnumber(L, -1)); CHECK(lua_tonumber(L, -1) == 42); lua_pop(L, 1); } + // lua_pcall variadic + { + lua_getfield(L, LUA_GLOBALSINDEX, "getnresults"); + lua_pushinteger(L, 200); + int status = lua_pcall(L, 1, LUA_MULTRET, 0); + CHECK(status == LUA_OK); + CHECK(lua_gettop(L) == 200); + lua_pop(L, 200); + } + + // baselib pcall variadic + { + lua_getfield(L, LUA_GLOBALSINDEX, "pcall"); + lua_getfield(L, LUA_GLOBALSINDEX, "getnresults"); + lua_pushinteger(L, 200); + lua_call(L, 2, LUA_MULTRET); + CHECK(lua_gettop(L) == 201); + lua_pop(L, 200); + CHECK(lua_toboolean(L, -1) == 1); + lua_pop(L, 1); + } + // lua_cpcall success { bool shouldFail = false; @@ -2378,6 +2435,46 @@ TEST_CASE("ApiCalls") lua_pop(L, LUAI_MAXCSTACK - 1); } + // lua_resume does not make thread yieldable + { + lua_State* L2 = lua_newthread(L); + + lua_pushcclosurek( + L2, + [](lua_State* L) + { + CHECK(lua_isyieldable(L) == 0); + return 0; + }, + nullptr, + 0, + nullptr + ); + lua_call(L2, 0, 0); + + lua_getfield(L2, LUA_GLOBALSINDEX, "getnresults"); + lua_pushinteger(L2, 1); + int status = lua_resume(L2, nullptr, 1); + REQUIRE(status == LUA_OK); + CHECK(lua_gettop(L2) == 1); + lua_pop(L2, 1); + + lua_pushcclosurek( + L2, + [](lua_State* L) + { + CHECK(lua_isyieldable(L) == 0); + return 0; + }, + nullptr, + 0, + nullptr + ); + lua_call(L2, 0, 0); + + lua_pop(L, 1); + } + // lua_equal with a sleeping thread wake up { lua_State* L2 = lua_newthread(L); @@ -2399,6 +2496,8 @@ TEST_CASE("ApiCalls") CHECK(lua_equal(L2, -1, -2) == 1); lua_pop(L2, 2); + + lua_pop(L, 1); } // lua_clonefunction + fenv @@ -2458,6 +2557,7 @@ TEST_CASE("ApiCalls") lua_getfield(L, LUA_GLOBALSINDEX, "largealloc"); int res = lua_pcall(L, 0, 0, 0); CHECK(res == LUA_ERRMEM); + lua_pop(L, 1); } // lua_pcall on OOM with an error handler @@ -2467,7 +2567,7 @@ TEST_CASE("ApiCalls") int res = lua_pcall(L, 0, 1, -2); CHECK(res == LUA_ERRMEM); CHECK((lua_isstring(L, -1) && strcmp(lua_tostring(L, -1), "oops") == 0)); - lua_pop(L, 1); + lua_pop(L, 2); } // lua_pcall on OOM with an error handler that errors @@ -2477,7 +2577,7 @@ TEST_CASE("ApiCalls") int res = lua_pcall(L, 0, 1, -2); CHECK(res == LUA_ERRERR); CHECK((lua_isstring(L, -1) && strcmp(lua_tostring(L, -1), "error in error handling") == 0)); - lua_pop(L, 1); + lua_pop(L, 2); } // lua_pcall on OOM with an error handler that OOMs @@ -2487,7 +2587,7 @@ TEST_CASE("ApiCalls") int res = lua_pcall(L, 0, 1, -2); CHECK(res == LUA_ERRMEM); CHECK((lua_isstring(L, -1) && strcmp(lua_tostring(L, -1), "not enough memory") == 0)); - lua_pop(L, 1); + lua_pop(L, 2); } // lua_pcall on error with an error handler that OOMs @@ -2497,8 +2597,10 @@ TEST_CASE("ApiCalls") int res = lua_pcall(L, 0, 1, -2); CHECK(res == LUA_ERRERR); CHECK((lua_isstring(L, -1) && strcmp(lua_tostring(L, -1), "error in error handling") == 0)); - lua_pop(L, 1); + lua_pop(L, 2); } + + CHECK(lua_gettop(L) == 0); } TEST_CASE("ApiAtoms") @@ -2760,17 +2862,18 @@ TEST_CASE("TagMethodError") // when doLuaBreak is true the test additionally calls lua_break to ensure breaking the debugger doesn't cause the VM to crash for (bool doLuaBreak : {false, true}) { - expectedHits = {22, 32}; + expectedHits = {37, 54, 73}; static int index; static bool luaBreak; index = 0; luaBreak = doLuaBreak; - // 'yieldCallback' doesn't do anything, but providing the callback to runConformance - // ensures that the call to lua_break doesn't cause an error to be generated because - // runConformance doesn't expect the VM to be in the state LUA_BREAK. - auto yieldCallback = [](lua_State* L) {}; + // To restore from a protected error break, we return 'true' so that test runner will use lua_resumeerror + auto yieldCallback = [](lua_State* L) -> bool + { + return true; + }; runConformance( "tmerror.luau", @@ -3605,10 +3708,6 @@ TEST_CASE("SafeEnv") TEST_CASE("Native") { - ScopedFastFlag luauCodeGenDirectBtest{FFlag::LuauCodeGenDirectBtest, true}; - ScopedFastFlag luauCodeGenRegAutoSpillA64{FFlag::LuauCodeGenRegAutoSpillA64, true}; - ScopedFastFlag luauCodeGenRestoreFromSplitStore{FFlag::LuauCodeGenRestoreFromSplitStore, true}; - // This tests requires code to run natively, otherwise all 'is_native' checks will fail if (!codegen || !luau_codegen_supported()) return; diff --git a/tests/ConstraintSolver.test.cpp b/tests/ConstraintSolver.test.cpp index b83fb345..67f54feb 100644 --- a/tests/ConstraintSolver.test.cpp +++ b/tests/ConstraintSolver.test.cpp @@ -5,6 +5,7 @@ #include "doctest.h" LUAU_FASTFLAG(LuauSolverV2); +LUAU_FASTFLAG(LuauScopedSeenSetInLookupTableProp); using namespace Luau; @@ -61,4 +62,31 @@ TEST_CASE_FIXTURE(ConstraintGeneratorFixture, "proper_let_generalization") CHECK("(unknown) -> number" == toString(idType)); } +TEST_CASE_FIXTURE(ConstraintGeneratorFixture, "table_prop_access_diamond") +{ + ScopedFastFlag sff(FFlag::LuauScopedSeenSetInLookupTableProp, true); + + CheckResult result = check(R"( + export type ItemDetails = { Id: number } + + export type AssetDetails = ItemDetails & {} + export type BundleDetails = ItemDetails & {} + + export type CatalogPage = { AssetDetails | BundleDetails } + + local function isRestricted(item: number) end + + -- Clear all item tiles and create new ones for the items in the specified page + local function displayPage(catalogPage: CatalogPage) + for _, itemDetails in catalogPage do + if isRestricted(itemDetails.Id) then + continue + end + end + end + )"); + + LUAU_REQUIRE_NO_ERRORS(result); +} + TEST_SUITE_END(); diff --git a/tests/EqSatSimplification.test.cpp b/tests/EqSatSimplification.test.cpp index 3ac1dd60..c7dbd78b 100644 --- a/tests/EqSatSimplification.test.cpp +++ b/tests/EqSatSimplification.test.cpp @@ -228,7 +228,7 @@ TEST_CASE_FIXTURE(ESFixture, "\"hello\" | \"world\" | \"hello\"") ); } -TEST_CASE_FIXTURE(ESFixture, "nil | boolean | number | string | thread | function | table | class | buffer") +TEST_CASE_FIXTURE(ESFixture, "nil | boolean | number | string | thread | function | table | userdata | buffer") { CHECK( "unknown" == simplifyStr(arena->addType( @@ -267,14 +267,14 @@ TEST_CASE_FIXTURE(ESFixture, "Child | Parent") CHECK("Parent" == simplifyStr(arena->addType(UnionType{{childClass, parentClass}}))); } -TEST_CASE_FIXTURE(ESFixture, "class | Child") +TEST_CASE_FIXTURE(ESFixture, "userdata | Child") { - CHECK("class" == simplifyStr(arena->addType(UnionType{{getBuiltins()->externType, childClass}}))); + CHECK("userdata" == simplifyStr(arena->addType(UnionType{{getBuiltins()->externType, childClass}}))); } -TEST_CASE_FIXTURE(ESFixture, "Parent | class | Child") +TEST_CASE_FIXTURE(ESFixture, "Parent | userdata | Child") { - CHECK("class" == simplifyStr(arena->addType(UnionType{{parentClass, getBuiltins()->externType, childClass}}))); + CHECK("userdata" == simplifyStr(arena->addType(UnionType{{parentClass, getBuiltins()->externType, childClass}}))); } TEST_CASE_FIXTURE(ESFixture, "Parent | Unrelated") @@ -388,8 +388,9 @@ TEST_CASE_FIXTURE(ESFixture, "(number) -> string | (string) -> number") TEST_CASE_FIXTURE(ESFixture, "add") { CHECK( - "number" == - simplifyStr(arena->addType(TypeFunctionInstanceType{builtinTypeFunctions().addFunc, {getBuiltins()->numberType, getBuiltins()->numberType}})) + "number" == simplifyStr(arena->addType( + TypeFunctionInstanceType{getBuiltinTypeFunctions().addFunc, {getBuiltins()->numberType, getBuiltins()->numberType}} + )) ); } @@ -397,7 +398,7 @@ TEST_CASE_FIXTURE(ESFixture, "union") { CHECK( "number" == simplifyStr(arena->addType( - TypeFunctionInstanceType{builtinTypeFunctions().unionFunc, {getBuiltins()->numberType, getBuiltins()->numberType}} + TypeFunctionInstanceType{getBuiltinTypeFunctions().unionFunc, {getBuiltins()->numberType, getBuiltins()->numberType}} )) ); } @@ -667,7 +668,7 @@ TEST_CASE_FIXTURE(ESFixture, "{ tag: \"Part\", x: number? } & { x: string }") TEST_CASE_FIXTURE(ESFixture, "Child & add") { const TypeId u = arena->addType(UnionType{{childClass, anotherChild, getBuiltins()->stringType}}); - const TypeId intersectTf = arena->addType(TypeFunctionInstanceType{builtinTypeFunctions().addFunc, {u, parentClass}, {}}); + const TypeId intersectTf = arena->addType(TypeFunctionInstanceType{getBuiltinTypeFunctions().addFunc, {u, parentClass}, {}}); const TypeId intersection = arena->addType(IntersectionType{{childClass, intersectTf}}); @@ -677,7 +678,7 @@ TEST_CASE_FIXTURE(ESFixture, "Child & add TEST_CASE_FIXTURE(ESFixture, "Child & intersect") { const TypeId u = arena->addType(UnionType{{childClass, anotherChild, getBuiltins()->stringType}}); - const TypeId intersectTf = arena->addType(TypeFunctionInstanceType{builtinTypeFunctions().intersectFunc, {u, parentClass}, {}}); + const TypeId intersectTf = arena->addType(TypeFunctionInstanceType{getBuiltinTypeFunctions().intersectFunc, {u, parentClass}, {}}); const TypeId intersection = arena->addType(IntersectionType{{childClass, intersectTf}}); @@ -695,7 +696,7 @@ TEST_CASE_FIXTURE(ESFixture, "lt == boolean") for (const auto& [lhs, rhs] : cases) { - const TypeId tfun = arena->addType(TypeFunctionInstanceType{builtinTypeFunctions().ltFunc, {lhs, rhs}}); + const TypeId tfun = arena->addType(TypeFunctionInstanceType{getBuiltinTypeFunctions().ltFunc, {lhs, rhs}}); CHECK("boolean" == simplifyStr(tfun)); } } diff --git a/tests/Fixture.cpp b/tests/Fixture.cpp index 247d2982..3dea764f 100644 --- a/tests/Fixture.cpp +++ b/tests/Fixture.cpp @@ -9,10 +9,10 @@ #include "Luau/ModuleResolver.h" #include "Luau/NotNull.h" #include "Luau/Parser.h" +#include "Luau/PrettyPrinter.h" #include "Luau/Type.h" #include "Luau/TypeAttach.h" #include "Luau/TypeInfer.h" -#include "Luau/Transpiler.h" #include "doctest.h" @@ -29,6 +29,8 @@ LUAU_FASTFLAG(LuauSolverV2); LUAU_FASTFLAG(DebugLuauLogSolverToJsonFile) LUAU_FASTFLAGVARIABLE(DebugLuauForceAllNewSolverTests); +LUAU_FASTFLAG(LuauBuiltinTypeFunctionsArentGlobal) +LUAU_FASTINT(LuauStackGuardThreshold) extern std::optional randomSeed; // tests/main.cpp @@ -578,7 +580,7 @@ std::string Fixture::decorateWithTypes(const std::string& code) SourceModule* sourceModule = getFrontend().getSourceModule(mainModuleName); attachTypeData(*sourceModule, *getFrontend().moduleResolver.getModule(mainModuleName)); - return transpileWithTypes(*sourceModule->root); + return prettyPrintWithTypes(*sourceModule->root); } void Fixture::dumpErrors(std::ostream& os, const std::vector& errors) @@ -689,6 +691,11 @@ NotNull Fixture::getBuiltins() return NotNull{builtinTypes}; } +const BuiltinTypeFunctions& Fixture::getBuiltinTypeFunctions() +{ + return FFlag::LuauBuiltinTypeFunctionsArentGlobal ? *getBuiltins()->typeFunctions : builtinTypeFunctions_DEPRECATED(); +} + Frontend& Fixture::getFrontend() { if (frontend) @@ -731,6 +738,17 @@ Frontend& Fixture::getFrontend() return *frontend; } +void Fixture::limitStackSize(size_t size) +{ + // The FInt is designed to trip when the amount of available address + // space goes below some threshold, but for this API, the convenient thing + // is to specify how much the test should be allowed to use. We need to + // do a tiny amount of arithmetic to convert. + + uintptr_t addressSpaceSize = getStackAddressSpaceSize(); + + dynamicScopedInts.emplace_back(FInt::LuauStackGuardThreshold, (int)(addressSpaceSize - size)); +} BuiltinsFixture::BuiltinsFixture(bool prepareAutocomplete) : Fixture(prepareAutocomplete) diff --git a/tests/Fixture.h b/tests/Fixture.h index 4bd2b891..83baaf97 100644 --- a/tests/Fixture.h +++ b/tests/Fixture.h @@ -186,8 +186,14 @@ struct Fixture LoadDefinitionFileResult loadDefinition(const std::string& source, bool forAutocomplete = false); // TODO: test theory about dynamic dispatch NotNull getBuiltins(); + const BuiltinTypeFunctions& getBuiltinTypeFunctions(); virtual Frontend& getFrontend(); + // On platforms that support it, adjust our internal stack guard to + // limit how much address space we should use before we blow up. We + // use this to test the stack guard itself. + void limitStackSize(size_t size); + private: bool hasDumpedErrors = false; @@ -198,6 +204,8 @@ struct Fixture TypeArena simplifierArena; SimplifierPtr simplifier{nullptr, nullptr}; + + std::vector dynamicScopedInts; }; struct BuiltinsFixture : Fixture diff --git a/tests/FragmentAutocomplete.test.cpp b/tests/FragmentAutocomplete.test.cpp index b0f8b650..cb7d5248 100644 --- a/tests/FragmentAutocomplete.test.cpp +++ b/tests/FragmentAutocomplete.test.cpp @@ -27,15 +27,10 @@ using namespace Luau; LUAU_FASTINT(LuauParseErrorLimit) LUAU_FASTFLAG(LuauBetterReverseDependencyTracking) -LUAU_FASTFLAG(LuauSolverAgnosticStringification) LUAU_FASTFLAG(LuauFragmentRequiresCanBeResolvedToAModule) -LUAU_FASTFLAG(LuauPopulateSelfTypesInFragment) -LUAU_FASTFLAG(LuauParseIncompleteInterpStringsWithLocation) -LUAU_FASTFLAG(LuauForInProvidesRecommendations) -LUAU_FASTFLAG(LuauFragmentAutocompleteTakesInnermostRefinement) -LUAU_FASTFLAG(LuauSuggestHotComments) LUAU_FASTFLAG(LuauNumericUnaryOpsDontProduceNegationRefinements) LUAU_FASTFLAG(LuauUnfinishedRepeatAncestryFix) +LUAU_FASTFLAG(LuauForInRangesConsiderInLocation) static std::optional nullCallback(std::string tag, std::optional ptr, std::optional contents) { @@ -65,9 +60,6 @@ struct FragmentAutocompleteFixtureImpl : BaseType { static_assert(std::is_base_of_v, "BaseType must be a descendant of Fixture"); - ScopedFastFlag sffLuauPopulateSelfTypesInFragment{FFlag::LuauPopulateSelfTypesInFragment, true}; - ScopedFastFlag luauParseIncompleteInterpStringsWithLocation{FFlag::LuauParseIncompleteInterpStringsWithLocation, true}; - FragmentAutocompleteFixtureImpl() : BaseType(true) { @@ -766,7 +758,6 @@ end TEST_CASE_FIXTURE(FragmentAutocompleteFixture, "partial_for_numeric_in_condition") { - ScopedFastFlag sff{FFlag::LuauForInProvidesRecommendations, true}; auto region = getAutocompleteRegion( R"( for c = 1,3 @@ -1613,7 +1604,6 @@ TEST_SUITE_BEGIN("FragmentAutocompleteTests"); TEST_CASE_FIXTURE(FragmentAutocompleteFixture, "multiple_fragment_autocomplete") { - ScopedFastFlag sff{FFlag::LuauSolverAgnosticStringification, true}; ToStringOptions opt; opt.exhaustive = true; opt.exhaustive = true; @@ -2990,7 +2980,6 @@ end) TEST_CASE_FIXTURE(FragmentAutocompleteFixture, "fragment_autocomplete_ensures_memory_isolation") { - ScopedFastFlag sff{FFlag::LuauSolverAgnosticStringification, true}; ToStringOptions opt; opt.exhaustive = true; opt.exhaustive = true; @@ -4199,7 +4188,6 @@ local s = `{e.@1 }` TEST_CASE_FIXTURE(FragmentAutocompleteBuiltinsFixture, "string_interpolation_format_provides_results_inside_of_function_call") { - ScopedFastFlag _{FFlag::LuauParseIncompleteInterpStringsWithLocation, true}; const std::string source = R"( type T = {x : number, y : number, z : number} local e = {x = 1, y = 2, z = 3} @@ -4227,7 +4215,6 @@ print(`{e.x} {e.@1}`) TEST_CASE_FIXTURE(FragmentAutocompleteBuiltinsFixture, "for_in_should_rec") { - ScopedFastFlag sff{FFlag::LuauForInProvidesRecommendations, true}; const std::string source = R"( type T = { x : {[number] : number}, y: number} local x : T = ({} :: T) @@ -4249,7 +4236,6 @@ end TEST_CASE_FIXTURE(FragmentAutocompleteBuiltinsFixture, "for_expr_in_should_rec_no_do") { - ScopedFastFlag sff{FFlag::LuauForInProvidesRecommendations, true}; const std::string source = R"( type T = { x : {[number] : number}, y: number, z: number} local x : T = ({} :: T) @@ -4278,7 +4264,6 @@ end TEST_CASE_FIXTURE(FragmentAutocompleteBuiltinsFixture, "for_expr_in_should_rec_with_do_in_step") { - ScopedFastFlag sff{FFlag::LuauForInProvidesRecommendations, true}; const std::string source = R"( type T = { x : {[number] : number}, y: number, z: number} local x : T = ({} :: T) @@ -4307,7 +4292,6 @@ end TEST_CASE_FIXTURE(FragmentAutocompleteBuiltinsFixture, "for_expr_in_should_rec_with_do_in_max_delete") { - ScopedFastFlag sff{FFlag::LuauForInProvidesRecommendations, true}; const std::string source = R"( type T = { x : {[number] : number}, y: number, z: number} local x : T = ({} :: T) @@ -4336,7 +4320,6 @@ end TEST_CASE_FIXTURE(FragmentAutocompleteBuiltinsFixture, "for_expr_in_should_rec_with_do_in_max_add") { - ScopedFastFlag sff{FFlag::LuauForInProvidesRecommendations, true}; const std::string source = R"( type T = { x : {[number] : number}, y: number, z: number} local x : T = ({} :: T) @@ -4365,8 +4348,6 @@ end TEST_CASE_FIXTURE(FragmentAutocompleteBuiltinsFixture, "correctly_grab_innermost_refinement") { - ScopedFastFlag _{FFlag::LuauFragmentAutocompleteTakesInnermostRefinement, true}; - const std::string source = R"( --!strict type Type1 = { Type: "Type1", CommonKey: string, Type1Key: string } @@ -4409,7 +4390,6 @@ end TEST_CASE_FIXTURE(FragmentAutocompleteBuiltinsFixture, "hot_comment_should_rec") { - ScopedFastFlag sff{FFlag::LuauSuggestHotComments, true}; const std::string source = R"(--!@1)"; autocompleteFragmentInBothSolvers( source, @@ -4549,6 +4529,95 @@ local function whatever() end ); } +TEST_CASE_FIXTURE(FragmentAutocompleteBuiltinsFixture, "in_place_edit_of_for_loop_before_in_keyword_returns_fragment_starting_from_for") +{ + ScopedFastFlag sff{FFlag::LuauForInRangesConsiderInLocation, true}; + std::string source = R"( +local x = {} +for i, value in x do + print(i) +end +)"; + + std::string dest = R"( +local x = {} +for @1, value in x do + print(i) +end +)"; + autocompleteFragmentInBothSolvers( + source, + dest, + '1', + [](auto& result) + { + CHECK(!result.result->acResults.entryMap.empty()); + } + ); +} + +TEST_CASE_FIXTURE(FragmentAutocompleteFixture, "bidirectionally_inferred_table_member") +{ + std::string source = R"( +type Foo = { foo1: string, bar1: number } +type Bar = { foo2: boolean, bar2: string } +type Baz = { foo3: number, bar3: boolean } + +local X: Foo & Bar & Baz = {} +)"; + + std::string dest = R"( +type Foo = { foo1: string, bar1: number } +type Bar = { foo2: boolean, bar2: string } +type Baz = { foo3: number, bar3: boolean } + +local X: Foo & Bar & Baz = { f@1 } + +)"; + autocompleteFragmentInBothSolvers( + source, + dest, + '1', + [](auto& result) + { + CHECK(!result.result->acResults.entryMap.empty()); + CHECK(result.result->acResults.entryMap.count("foo1") > 0); + CHECK(result.result->acResults.entryMap.count("foo2") > 0); + CHECK(result.result->acResults.entryMap.count("foo3") > 0); + } + ); +} + +TEST_CASE_FIXTURE(FragmentAutocompleteFixture, "oss_1850") +{ + std::string source = R"( +type t = { name: "t", } | { name: "ts", person: "dog" } + +local t:t +if t.name == "ts" then +end + )"; + std::string dest = R"( +type t = { name: "t", } | { name: "ts", person: "dog" } + +local t:t +if t.name == "ts" then + t.@1 +end + )"; + + autocompleteFragmentInBothSolvers( + source, + dest, + '1', + [](auto& result) + { + CHECK(!result.result->acResults.entryMap.empty()); + CHECK(result.result->acResults.entryMap.count("name") > 0); + CHECK(result.result->acResults.entryMap.count("person") > 0); + } + ); +} // NOLINTEND(bugprone-unchecked-optional-access) diff --git a/tests/Frontend.test.cpp b/tests/Frontend.test.cpp index 83d493e5..01e93de3 100644 --- a/tests/Frontend.test.cpp +++ b/tests/Frontend.test.cpp @@ -16,7 +16,7 @@ using namespace Luau; LUAU_FASTFLAG(LuauSolverV2); LUAU_FASTFLAG(DebugLuauFreezeArena) LUAU_FASTFLAG(DebugLuauMagicTypes) -LUAU_FASTFLAG(LuauSolverAgnosticStringification) +LUAU_FASTFLAG(LuauBatchedExecuteTask) namespace { @@ -127,7 +127,6 @@ TEST_CASE_FIXTURE(FrontendFixture, "real_source") TEST_CASE_FIXTURE(FrontendFixture, "automatically_check_dependent_scripts") { - ScopedFastFlag sff{FFlag::LuauSolverAgnosticStringification, true}; fileResolver.source["game/Gui/Modules/A"] = "return {hello=5, world=true}"; fileResolver.source["game/Gui/Modules/B"] = R"( local Modules = game:GetService('Gui').Modules @@ -260,7 +259,6 @@ TEST_CASE_FIXTURE(FrontendFixture, "cycle_detection_between_check_and_nocheck") TEST_CASE_FIXTURE(FrontendFixture, "nocheck_cycle_used_by_checked") { - ScopedFastFlag sff{FFlag::LuauSolverAgnosticStringification, true}; fileResolver.source["game/Gui/Modules/A"] = R"( --!nocheck local Modules = game:GetService('Gui').Modules @@ -374,7 +372,6 @@ TEST_CASE_FIXTURE(FrontendFixture, "cycle_error_paths") TEST_CASE_FIXTURE(FrontendFixture, "cycle_incremental_type_surface") { - ScopedFastFlag sff{FFlag::LuauSolverAgnosticStringification, true}; fileResolver.source["game/A"] = R"( return {hello = 2} )"; @@ -435,7 +432,6 @@ TEST_CASE_FIXTURE(FrontendFixture, "cycle_incremental_type_surface_longer") TEST_CASE_FIXTURE(FrontendFixture, "cycle_incremental_type_surface_exports") { - ScopedFastFlag sff{FFlag::LuauSolverAgnosticStringification, true}; fileResolver.source["game/A"] = R"( local b = require(game.B) export type atype = { x: b.btype } @@ -528,7 +524,6 @@ TEST_CASE_FIXTURE(FrontendFixture, "dont_recheck_script_that_hasnt_been_marked_d TEST_CASE_FIXTURE(FrontendFixture, "recheck_if_dependent_script_is_dirty") { - ScopedFastFlag sff{FFlag::LuauSolverAgnosticStringification, true}; fileResolver.source["game/Gui/Modules/A"] = "return {hello=5, world=true}"; fileResolver.source["game/Gui/Modules/B"] = R"( local Modules = game:GetService('Gui').Modules @@ -868,7 +863,6 @@ TEST_CASE_FIXTURE(FrontendFixture, "discard_type_graphs") TEST_CASE_FIXTURE(FrontendFixture, "it_should_be_safe_to_stringify_errors_when_full_type_graph_is_discarded") { - ScopedFastFlag sff{FFlag::LuauSolverAgnosticStringification, true}; Frontend fe{&fileResolver, &configResolver, {false}}; fileResolver.source["Module/A"] = R"( @@ -1741,6 +1735,8 @@ TEST_CASE_FIXTURE(FrontendFixture, "test_invalid_dependency_tracking_per_module_ TEST_CASE_FIXTURE(FrontendFixture, "queue_check_simple") { + ScopedFastFlag luauBatchedExecuteTask{FFlag::LuauBatchedExecuteTask, true}; + fileResolver.source["game/Gui/Modules/A"] = R"( --!strict return {hello=5, world=true} @@ -1762,6 +1758,8 @@ TEST_CASE_FIXTURE(FrontendFixture, "queue_check_simple") TEST_CASE_FIXTURE(FrontendFixture, "queue_check_cycle_instant") { + ScopedFastFlag luauBatchedExecuteTask{FFlag::LuauBatchedExecuteTask, true}; + fileResolver.source["game/Gui/Modules/A"] = R"( --!strict local Modules = game:GetService('Gui').Modules @@ -1787,6 +1785,8 @@ TEST_CASE_FIXTURE(FrontendFixture, "queue_check_cycle_instant") TEST_CASE_FIXTURE(FrontendFixture, "queue_check_cycle_delayed") { + ScopedFastFlag luauBatchedExecuteTask{FFlag::LuauBatchedExecuteTask, true}; + fileResolver.source["game/Gui/Modules/C"] = R"( --!strict return {c_value = 5} @@ -1818,6 +1818,7 @@ TEST_CASE_FIXTURE(FrontendFixture, "queue_check_cycle_delayed") TEST_CASE_FIXTURE(FrontendFixture, "queue_check_propagates_ice") { + ScopedFastFlag luauBatchedExecuteTask{FFlag::LuauBatchedExecuteTask, true}; ScopedFastFlag sffs{FFlag::DebugLuauMagicTypes, true}; ModuleName mm = fromString("MainModule"); diff --git a/tests/Generalization.test.cpp b/tests/Generalization.test.cpp index c435f0ee..40ca347e 100644 --- a/tests/Generalization.test.cpp +++ b/tests/Generalization.test.cpp @@ -18,8 +18,6 @@ LUAU_FASTFLAG(LuauSolverV2) LUAU_FASTFLAG(DebugLuauForbidInternalTypes) LUAU_FASTFLAG(LuauSubtypingReportGenericBoundMismatches2) -LUAU_FASTFLAG(LuauSubtypingGenericsDoesntUseVariance) - TEST_SUITE_BEGIN("Generalization"); struct GeneralizationFixture @@ -394,9 +392,7 @@ TEST_CASE_FIXTURE(Fixture, "generics_dont_leak_into_callback") TEST_CASE_FIXTURE(Fixture, "generics_dont_leak_into_callback_2") { - ScopedFastFlag sffs[] = { - {FFlag::LuauSolverV2, true}, {FFlag::LuauSubtypingReportGenericBoundMismatches2, true}, {FFlag::LuauSubtypingGenericsDoesntUseVariance, true} - }; + ScopedFastFlag sffs[] = {{FFlag::LuauSolverV2, true}, {FFlag::LuauSubtypingReportGenericBoundMismatches2, true}}; CheckResult result = check(R"( local func: (T, (T) -> ()) -> () = nil :: any diff --git a/tests/IrBuilder.test.cpp b/tests/IrBuilder.test.cpp index 70be8994..d6af8efe 100644 --- a/tests/IrBuilder.test.cpp +++ b/tests/IrBuilder.test.cpp @@ -13,6 +13,9 @@ #include LUAU_FASTFLAG(DebugLuauAbortingChecks) +LUAU_FASTFLAG(LuauCodegenDirectCompare2) +LUAU_FASTFLAG(LuauCodegenNilStoreInvalidateValue2) +LUAU_FASTFLAG(LuauCodegenStorePriority) using namespace Luau::CodeGen; @@ -1770,6 +1773,124 @@ TEST_CASE_FIXTURE(IrBuilderFixture, "NumericSimplifications") )"); } +TEST_CASE_FIXTURE(IrBuilderFixture, "CmpTagSimplification") +{ + ScopedFastFlag luauCodegenDirectCompare{FFlag::LuauCodegenDirectCompare2, true}; + + IrOp block = build.block(IrBlockKind::Internal); + + build.beginBlock(block); + + IrOp eq = build.cond(IrCondition::Equal); + IrOp neq = build.cond(IrCondition::NotEqual); + + build.inst(IrCmd::STORE_INT, build.vmReg(0), build.inst(IrCmd::CMP_TAG, build.constTag(tnil), build.constTag(tnumber), eq)); + build.inst(IrCmd::STORE_INT, build.vmReg(1), build.inst(IrCmd::CMP_TAG, build.constTag(tnumber), build.constTag(tnumber), eq)); + build.inst(IrCmd::STORE_INT, build.vmReg(2), build.inst(IrCmd::CMP_TAG, build.constTag(tnil), build.constTag(tnumber), neq)); + build.inst(IrCmd::STORE_INT, build.vmReg(3), build.inst(IrCmd::CMP_TAG, build.constTag(tnumber), build.constTag(tnumber), neq)); + + build.inst(IrCmd::RETURN, build.vmReg(0), build.constInt(4)); + + updateUseCounts(build.function); + constPropInBlockChains(build); + + CHECK("\n" + toString(build.function, IncludeUseInfo::No) == R"( +bb_0: + STORE_INT R0, 0i + STORE_INT R1, 1i + STORE_INT R2, 1i + STORE_INT R3, 0i + RETURN R0, 4i + +)"); +} + +TEST_CASE_FIXTURE(IrBuilderFixture, "CmpSplitTagValueSimplification") +{ + ScopedFastFlag luauCodegenDirectCompare{FFlag::LuauCodegenDirectCompare2, true}; + + IrOp block = build.block(IrBlockKind::Internal); + + build.beginBlock(block); + IrOp valueBoolean = build.inst(IrCmd::LOAD_INT, build.vmReg(1)); + IrOp valueNan = build.inst(IrCmd::DIV_NUM, build.constDouble(0.0), build.constDouble(0.0)); + + IrOp opTag = build.inst(IrCmd::LOAD_TAG, build.vmReg(0)); + IrOp opNil = build.constTag(tnil); + IrOp opBool = build.constTag(tboolean); + IrOp opNum = build.constTag(tnumber); + + IrOp eq = build.cond(IrCondition::Equal); + IrOp neq = build.cond(IrCondition::NotEqual); + + build.inst(IrCmd::STORE_INT, build.vmReg(2), build.inst(IrCmd::CMP_SPLIT_TVALUE, opNil, opBool, build.constInt(0), valueBoolean, eq)); + build.inst(IrCmd::STORE_INT, build.vmReg(3), build.inst(IrCmd::CMP_SPLIT_TVALUE, opBool, opBool, build.constInt(0), build.constInt(0), eq)); + build.inst(IrCmd::STORE_INT, build.vmReg(4), build.inst(IrCmd::CMP_SPLIT_TVALUE, opBool, opBool, build.constInt(0), build.constInt(1), eq)); + build.inst(IrCmd::STORE_INT, build.vmReg(5), build.inst(IrCmd::CMP_SPLIT_TVALUE, opNum, opNum, build.constDouble(0), build.constDouble(0), eq)); + build.inst(IrCmd::STORE_INT, build.vmReg(6), build.inst(IrCmd::CMP_SPLIT_TVALUE, opNum, opNum, build.constDouble(0), build.constDouble(1), eq)); + + build.inst(IrCmd::STORE_INT, build.vmReg(7), build.inst(IrCmd::CMP_SPLIT_TVALUE, opNil, opBool, build.constInt(0), valueBoolean, neq)); + build.inst(IrCmd::STORE_INT, build.vmReg(8), build.inst(IrCmd::CMP_SPLIT_TVALUE, opBool, opBool, build.constInt(0), build.constInt(0), neq)); + build.inst(IrCmd::STORE_INT, build.vmReg(9), build.inst(IrCmd::CMP_SPLIT_TVALUE, opBool, opBool, build.constInt(0), build.constInt(1), neq)); + build.inst(IrCmd::STORE_INT, build.vmReg(10), build.inst(IrCmd::CMP_SPLIT_TVALUE, opNum, opNum, build.constDouble(0), build.constDouble(0), neq)); + build.inst(IrCmd::STORE_INT, build.vmReg(11), build.inst(IrCmd::CMP_SPLIT_TVALUE, opNum, opNum, build.constDouble(0), build.constDouble(1), neq)); + + // When we compare known values, but unknown tags, cases can either be fully folded or simplified to a tag check + build.inst(IrCmd::STORE_INT, build.vmReg(12), build.inst(IrCmd::CMP_SPLIT_TVALUE, opTag, opBool, build.constInt(0), build.constInt(0), eq)); + build.inst(IrCmd::STORE_INT, build.vmReg(13), build.inst(IrCmd::CMP_SPLIT_TVALUE, opTag, opBool, build.constInt(0), build.constInt(1), eq)); + build.inst(IrCmd::STORE_INT, build.vmReg(14), build.inst(IrCmd::CMP_SPLIT_TVALUE, opTag, opNum, build.constDouble(0), build.constDouble(0), eq)); + build.inst(IrCmd::STORE_INT, build.vmReg(15), build.inst(IrCmd::CMP_SPLIT_TVALUE, opTag, opNum, build.constDouble(0), build.constDouble(1), eq)); + + build.inst(IrCmd::STORE_INT, build.vmReg(16), build.inst(IrCmd::CMP_SPLIT_TVALUE, opTag, opBool, build.constInt(0), build.constInt(0), neq)); + build.inst(IrCmd::STORE_INT, build.vmReg(17), build.inst(IrCmd::CMP_SPLIT_TVALUE, opTag, opBool, build.constInt(0), build.constInt(1), neq)); + build.inst(IrCmd::STORE_INT, build.vmReg(18), build.inst(IrCmd::CMP_SPLIT_TVALUE, opTag, opNum, build.constDouble(0), build.constDouble(0), neq)); + build.inst(IrCmd::STORE_INT, build.vmReg(19), build.inst(IrCmd::CMP_SPLIT_TVALUE, opTag, opNum, build.constDouble(0), build.constDouble(1), neq)); + + // NaN is always fun to consider + build.inst(IrCmd::STORE_INT, build.vmReg(20), build.inst(IrCmd::CMP_SPLIT_TVALUE, opNum, opNum, valueNan, valueNan, eq)); + build.inst(IrCmd::STORE_INT, build.vmReg(21), build.inst(IrCmd::CMP_SPLIT_TVALUE, opNum, opNum, valueNan, valueNan, neq)); + build.inst(IrCmd::STORE_INT, build.vmReg(22), build.inst(IrCmd::CMP_SPLIT_TVALUE, opTag, opNum, valueNan, valueNan, eq)); + build.inst(IrCmd::STORE_INT, build.vmReg(23), build.inst(IrCmd::CMP_SPLIT_TVALUE, opTag, opNum, valueNan, valueNan, neq)); + + build.inst(IrCmd::RETURN, build.vmReg(2), build.constInt(21)); + + updateUseCounts(build.function); + constPropInBlockChains(build); + + CHECK("\n" + toString(build.function, IncludeUseInfo::No) == R"( +bb_0: + %2 = LOAD_TAG R0 + STORE_INT R2, 0i + STORE_INT R3, 1i + STORE_INT R4, 0i + STORE_INT R5, 1i + STORE_INT R6, 0i + STORE_INT R7, 1i + STORE_INT R8, 0i + STORE_INT R9, 1i + STORE_INT R10, 0i + STORE_INT R11, 1i + %23 = CMP_TAG %2, tboolean, eq + STORE_INT R12, %23 + STORE_INT R13, 0i + %27 = CMP_TAG %2, tnumber, eq + STORE_INT R14, %27 + STORE_INT R15, 0i + %31 = CMP_TAG %2, tboolean, not_eq + STORE_INT R16, %31 + STORE_INT R17, 1i + %35 = CMP_TAG %2, tnumber, not_eq + STORE_INT R18, %35 + STORE_INT R19, 1i + STORE_INT R20, 0i + STORE_INT R21, 1i + STORE_INT R22, 0i + STORE_INT R23, 1i + RETURN R2, 21i + +)"); +} + TEST_SUITE_END(); TEST_SUITE_BEGIN("LinearExecutionFlowExtraction"); @@ -2797,6 +2918,49 @@ TEST_CASE_FIXTURE(IrBuilderFixture, "DoNotProduceInvalidSplitStore2") )"); } +TEST_CASE_FIXTURE(IrBuilderFixture, "DoNotProduceInvalidSplitStore3") +{ + ScopedFastFlag luauCodegenStorePriority{FFlag::LuauCodegenStorePriority, true}; + + IrOp entry = build.block(IrBlockKind::Internal); + + build.beginBlock(entry); + + // Obscure the R0 state by only storing the value (tag was established in a previous block not visible here) + build.inst(IrCmd::STORE_INT, build.vmReg(0), build.constInt(2)); + + // In the future, STORE_INT might imply that the tag is LUA_TBOOLEAN, but today it is used for other integer stores too + build.inst(IrCmd::CHECK_TAG, build.inst(IrCmd::LOAD_TAG, build.vmReg(0)), build.constTag(tnumber), build.vmExit(1)); + + // Secondary load for store propagation + build.inst(IrCmd::STORE_TAG, build.vmReg(2), build.constTag(tnumber)); + build.inst(IrCmd::STORE_DOUBLE, build.vmReg(2), build.inst(IrCmd::LOAD_DOUBLE, build.vmReg(0))); + + build.inst(IrCmd::STORE_TVALUE, build.vmReg(1), build.inst(IrCmd::LOAD_TVALUE, build.vmReg(0))); + build.inst(IrCmd::STORE_INT, build.vmReg(1), build.constInt(2)); + build.inst(IrCmd::STORE_TAG, build.vmReg(1), build.constTag(tboolean)); + build.inst(IrCmd::RETURN, build.vmReg(1), build.constInt(1)); + + updateUseCounts(build.function); + computeCfgInfo(build.function); + constPropInBlockChains(build); + + CHECK("\n" + toString(build.function, IncludeUseInfo::No) == R"( +bb_0: + STORE_INT R0, 2i + %1 = LOAD_TAG R0 + CHECK_TAG %1, tnumber, exit(1) + STORE_TAG R2, tnumber + %4 = LOAD_DOUBLE R0 + STORE_DOUBLE R2, %4 + %6 = LOAD_TVALUE R0 + STORE_TVALUE R1, %6 + STORE_TAG R1, tboolean + RETURN R1, 1i + +)"); +} + TEST_SUITE_END(); TEST_SUITE_BEGIN("Analysis"); @@ -4654,6 +4818,63 @@ TEST_CASE_FIXTURE(IrBuilderFixture, "VectorOverCombinedNumber") )"); } +TEST_CASE_FIXTURE(IrBuilderFixture, "NilStoreImplicitValueClear1") +{ + ScopedFastFlag luauCodegenNilStoreInvalidateValue2{FFlag::LuauCodegenNilStoreInvalidateValue2, true}; + + IrOp entry = build.block(IrBlockKind::Internal); + + build.beginBlock(entry); + build.inst(IrCmd::STORE_INT, build.vmReg(0), build.constInt(1)); + build.inst(IrCmd::STORE_TAG, build.vmReg(0), build.constTag(tboolean)); + build.inst(IrCmd::STORE_TAG, build.vmReg(0), build.constTag(tnil)); + build.inst(IrCmd::STORE_INT, build.vmReg(0), build.constInt(1)); + build.inst(IrCmd::STORE_TAG, build.vmReg(0), build.constTag(tboolean)); + build.inst(IrCmd::RETURN, build.vmReg(0), build.constInt(1)); + + updateUseCounts(build.function); + computeCfgInfo(build.function); + constPropInBlockChains(build); + markDeadStoresInBlockChains(build); + + CHECK("\n" + toString(build.function, IncludeUseInfo::No) == R"( +bb_0: + STORE_TAG R0, tnil + STORE_INT R0, 1i + STORE_TAG R0, tboolean + RETURN R0, 1i + +)"); +} + +TEST_CASE_FIXTURE(IrBuilderFixture, "NilStoreImplicitValueClear2") +{ + ScopedFastFlag luauCodegenNilStoreInvalidateValue2{FFlag::LuauCodegenNilStoreInvalidateValue2, true}; + + IrOp entry = build.block(IrBlockKind::Internal); + + build.beginBlock(entry); + build.inst(IrCmd::STORE_TAG, build.vmReg(0), build.constTag(tnil)); + IrOp value = build.inst(IrCmd::LOAD_TVALUE, build.vmReg(0)); + build.inst(IrCmd::STORE_TAG, build.vmReg(0), build.constTag(tnil)); + build.inst(IrCmd::STORE_TVALUE, build.vmReg(1), value); + IrOp tag = build.inst(IrCmd::LOAD_TAG, build.vmReg(1)); + build.inst(IrCmd::CHECK_TAG, tag, build.constTag(tnil), build.vmExit(1)); + build.inst(IrCmd::RETURN, build.vmReg(0), build.constInt(1)); + + updateUseCounts(build.function); + computeCfgInfo(build.function); + constPropInBlockChains(build); + markDeadStoresInBlockChains(build); + + CHECK("\n" + toString(build.function, IncludeUseInfo::No) == R"( +bb_0: + STORE_TAG R0, tnil + RETURN R0, 1i + +)"); +} + TEST_SUITE_END(); TEST_SUITE_BEGIN("Dump"); diff --git a/tests/IrLowering.test.cpp b/tests/IrLowering.test.cpp index f5250222..104626f5 100644 --- a/tests/IrLowering.test.cpp +++ b/tests/IrLowering.test.cpp @@ -16,12 +16,12 @@ #include #include -LUAU_FASTFLAG(LuauCodeGenDirectBtest) LUAU_FASTFLAG(LuauVectorLerp) LUAU_FASTFLAG(LuauCompileVectorLerp) -LUAU_FASTFLAG(LuauTypeCheckerVectorLerp) -LUAU_FASTFLAG(LuauCodeGenVectorLerp) +LUAU_FASTFLAG(LuauTypeCheckerVectorLerp2) +LUAU_FASTFLAG(LuauCodeGenVectorLerp2) LUAU_FASTFLAG(LuauCodeGenFMA) +LUAU_FASTFLAG(LuauCodegenDirectCompare2) static void luauLibraryConstantLookup(const char* library, const char* member, Luau::CompileConstant* constant) { @@ -477,19 +477,19 @@ TEST_CASE("VectorLerp") { ScopedFastFlag _[]{ {FFlag::LuauCompileVectorLerp, true}, - {FFlag::LuauTypeCheckerVectorLerp, true}, + {FFlag::LuauTypeCheckerVectorLerp2, true}, {FFlag::LuauVectorLerp, true}, - {FFlag::LuauCodeGenVectorLerp, true} + {FFlag::LuauCodeGenVectorLerp2, true} }; if (FFlag::LuauCodeGenFMA) { - CHECK_EQ( - "\n" + getCodegenAssembly(R"( + CHECK_EQ( + "\n" + getCodegenAssembly(R"( local function vec3lerp(a: vector, b: vector, t: number) return vector.lerp(a, b, t) end )"), - R"( + R"( ; function vec3lerp($arg0, $arg1, $arg2) line 2 bb_0: CHECK_TAG R0, tvector, exit(entry) @@ -517,13 +517,13 @@ end } else { - CHECK_EQ( - "\n" + getCodegenAssembly(R"( + CHECK_EQ( + "\n" + getCodegenAssembly(R"( local function vec3lerp(a: vector, b: vector, t: number) return vector.lerp(a, b, t) end )"), - R"( + R"( ; function vec3lerp($arg0, $arg1, $arg2) line 2 bb_0: CHECK_TAG R0, tvector, exit(entry) @@ -645,6 +645,372 @@ end ); } +TEST_CASE("StringCompare") +{ + ScopedFastFlag luauCodegenDirectCompare{FFlag::LuauCodegenDirectCompare2, true}; + + CHECK_EQ( + "\n" + getCodegenAssembly( + R"( +local function foo(a) + return a == "test" +end +)" + ), + R"( +; function foo($arg0) line 2 +bb_bytecode_0: + %0 = LOAD_TAG R0 + %1 = LOAD_POINTER R0 + %2 = LOAD_POINTER K0 ('test') + %3 = CMP_SPLIT_TVALUE %0, tstring, %1, %2, eq + STORE_TAG R1, tboolean + STORE_INT R1, %3 + JUMP bb_bytecode_2 +bb_bytecode_2: + INTERRUPT 4u + RETURN R1, 1i +)" + ); +} + +TEST_CASE("StringCompareAnnotated") +{ + ScopedFastFlag luauCodegenDirectCompare{FFlag::LuauCodegenDirectCompare2, true}; + + CHECK_EQ( + "\n" + getCodegenAssembly( + R"( +local function foo(a: string) + return a == "test" +end +)" + ), + R"( +; function foo($arg0) line 2 +bb_0: + CHECK_TAG R0, tstring, exit(entry) + JUMP bb_4 +bb_4: + JUMP bb_bytecode_1 +bb_bytecode_1: + %5 = LOAD_POINTER R0 + %6 = LOAD_POINTER K0 ('test') + %7 = CMP_SPLIT_TVALUE tstring, tstring, %5, %6, eq + STORE_TAG R1, tboolean + STORE_INT R1, %7 + JUMP bb_bytecode_3 +bb_bytecode_3: + INTERRUPT 4u + RETURN R1, 1i +)" + ); +} + +TEST_CASE("NilCompare") +{ + ScopedFastFlag luauCodegenDirectCompare{FFlag::LuauCodegenDirectCompare2, true}; + + CHECK_EQ( + "\n" + getCodegenAssembly( + R"( +local function foo(a) + return a == nil +end +)" + ), + R"( +; function foo($arg0) line 2 +bb_bytecode_0: + %0 = LOAD_TAG R0 + %1 = CMP_TAG %0, tnil, eq + STORE_TAG R1, tboolean + STORE_INT R1, %1 + JUMP bb_bytecode_2 +bb_bytecode_2: + INTERRUPT 4u + RETURN R1, 1i +)" + ); +} + +TEST_CASE("BooleanCompare") +{ + ScopedFastFlag luauCodegenDirectCompare{FFlag::LuauCodegenDirectCompare2, true}; + + CHECK_EQ( + "\n" + getCodegenAssembly( + R"( +local function foo(a) + return { a == true, a == false, a ~= true, a ~= false } +end +)" + ), + R"( +; function foo($arg0) line 2 +bb_bytecode_0: + SET_SAVEDPC 1u + %1 = NEW_TABLE 4u, 0u + STORE_POINTER R1, %1 + STORE_TAG R1, ttable + CHECK_GC + %5 = LOAD_TAG R0 + %6 = LOAD_INT R0 + %7 = CMP_SPLIT_TVALUE %5, tboolean, %6, 1i, eq + STORE_TAG R2, tboolean + STORE_INT R2, %7 + JUMP bb_bytecode_2 +bb_bytecode_2: + %14 = LOAD_TAG R0 + %15 = LOAD_INT R0 + %16 = CMP_SPLIT_TVALUE %14, tboolean, %15, 0i, eq + STORE_TAG R3, tboolean + STORE_INT R3, %16 + JUMP bb_bytecode_4 +bb_bytecode_4: + %23 = LOAD_TAG R0 + %24 = LOAD_INT R0 + %25 = CMP_SPLIT_TVALUE %23, tboolean, %24, 1i, not_eq + STORE_TAG R4, tboolean + STORE_INT R4, %25 + JUMP bb_bytecode_6 +bb_bytecode_6: + %32 = LOAD_TAG R0 + %33 = LOAD_INT R0 + %34 = CMP_SPLIT_TVALUE %32, tboolean, %33, 0i, not_eq + STORE_TAG R5, tboolean + STORE_INT R5, %34 + JUMP bb_bytecode_8 +bb_bytecode_8: + SETLIST 18u, R1, R2, 4i, 1u, 4u + INTERRUPT 20u + RETURN R1, 1i +)" + ); +} + +TEST_CASE("NumberCompare") +{ + ScopedFastFlag luauCodegenDirectCompare{FFlag::LuauCodegenDirectCompare2, true}; + + CHECK_EQ( + "\n" + getCodegenAssembly( + R"( +local function foo(a) + return { a == 4.0, a ~= 3.0 } +end +)" + ), + R"( +; function foo($arg0) line 2 +bb_bytecode_0: + SET_SAVEDPC 1u + %1 = NEW_TABLE 2u, 0u + STORE_POINTER R1, %1 + STORE_TAG R1, ttable + CHECK_GC + %5 = LOAD_TAG R0 + %6 = LOAD_DOUBLE R0 + %7 = CMP_SPLIT_TVALUE %5, tnumber, %6, 4, eq + STORE_TAG R2, tboolean + STORE_INT R2, %7 + JUMP bb_bytecode_2 +bb_bytecode_2: + %14 = LOAD_TAG R0 + %15 = LOAD_DOUBLE R0 + %16 = CMP_SPLIT_TVALUE %14, tnumber, %15, 3, not_eq + STORE_TAG R3, tboolean + STORE_INT R3, %16 + JUMP bb_bytecode_4 +bb_bytecode_4: + SETLIST 10u, R1, R2, 2i, 1u, 2u + INTERRUPT 12u + RETURN R1, 1i +)" + ); +} + +TEST_CASE("TypeCompare") +{ + ScopedFastFlag luauCodegenDirectCompare{FFlag::LuauCodegenDirectCompare2, true}; + + CHECK_EQ( + "\n" + getCodegenAssembly( + R"( +local function foo(a) + return type(a) == "number" +end +)" + ), + R"( +; function foo($arg0) line 2 +bb_bytecode_0: + CHECK_SAFE_ENV exit(1) + %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 + JUMP bb_bytecode_2 +bb_bytecode_2: + INTERRUPT 9u + RETURN R1, 1i +)" + ); +} + +TEST_CASE("TypeofCompare") +{ + ScopedFastFlag luauCodegenDirectCompare{FFlag::LuauCodegenDirectCompare2, true}; + + CHECK_EQ( + "\n" + getCodegenAssembly( + R"( +local function foo(a) + return typeof(a) == "number" +end +)" + ), + R"( +; function foo($arg0) line 2 +bb_bytecode_0: + CHECK_SAFE_ENV exit(1) + %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 + JUMP bb_bytecode_2 +bb_bytecode_2: + INTERRUPT 9u + RETURN R1, 1i +)" + ); +} + +TEST_CASE("TypeofCompareCustom") +{ + ScopedFastFlag luauCodegenDirectCompare{FFlag::LuauCodegenDirectCompare2, true}; + + CHECK_EQ( + "\n" + getCodegenAssembly( + R"( +local function foo(a) + return typeof(a) == "User" +end +)" + ), + R"( +; function foo($arg0) line 2 +bb_bytecode_0: + CHECK_SAFE_ENV exit(1) + %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 + STORE_INT R1, %7 + JUMP bb_bytecode_2 +bb_bytecode_2: + INTERRUPT 9u + RETURN R1, 1i +)" + ); +} + +TEST_CASE("TypeCondition") +{ + ScopedFastFlag luauCodegenDirectCompare{FFlag::LuauCodegenDirectCompare2, true}; + + CHECK_EQ( + "\n" + getCodegenAssembly( + R"( +local function foo(a, b) + if type(a) == "number" then + return a + b + end + return nil +end +)" + ), + R"( +; function foo($arg0, $arg1) line 2 +bb_bytecode_0: + CHECK_SAFE_ENV exit(1) + %1 = LOAD_TAG R0 + %2 = GET_TYPE %1 + STORE_POINTER R2, %2 + STORE_TAG R2, tstring + JUMP bb_4 +bb_4: + %7 = LOAD_POINTER R2 + %8 = LOAD_POINTER K2 ('number') + JUMP_EQ_POINTER %7, %8, bb_3, bb_bytecode_1 +bb_3: + CHECK_TAG R0, tnumber, bb_fallback_5 + CHECK_TAG R1, tnumber, bb_fallback_5 + %14 = LOAD_DOUBLE R0 + %16 = ADD_NUM %14, R1 + STORE_DOUBLE R2, %16 + STORE_TAG R2, tnumber + JUMP bb_6 +bb_6: + INTERRUPT 8u + RETURN R2, 1i +bb_bytecode_1: + STORE_TAG R2, tnil + INTERRUPT 10u + RETURN R2, 1i +)" + ); +} + +TEST_CASE("AssertTypeGuard") +{ + ScopedFastFlag luauCodegenDirectCompare{FFlag::LuauCodegenDirectCompare2, true}; + + CHECK_EQ( + "\n" + getCodegenAssembly( + R"( +local function foo(a) + assert(type(a) == "number") + return a * 2 +end +)" + ), + R"( +; function foo($arg0) line 2 +bb_bytecode_0: + CHECK_SAFE_ENV exit(1) + %1 = LOAD_TAG R0 + %2 = GET_TYPE %1 + STORE_POINTER R3, %2 + STORE_TAG R3, tstring + %8 = CMP_TAG %1, tnumber, eq + STORE_TAG R2, tboolean + STORE_INT R2, %8 + JUMP bb_bytecode_2 +bb_bytecode_2: + CHECK_TRUTHY tboolean, R2, exit(10) + JUMP bb_5 +bb_5: + CHECK_TAG R0, tnumber, bb_fallback_6 + %28 = LOAD_DOUBLE R0 + %29 = ADD_NUM %28, %28 + STORE_DOUBLE R1, %29 + STORE_TAG R1, tnumber + JUMP bb_7 +bb_7: + INTERRUPT 14u + RETURN R1, 1i +)" + ); +} + TEST_CASE("VectorConstantTag") { CHECK_EQ( @@ -2295,8 +2661,6 @@ end TEST_CASE("Bit32BtestDirect") { - ScopedFastFlag luauCodeGenDirectBtest{FFlag::LuauCodeGenDirectBtest, true}; - CHECK_EQ( "\n" + getCodegenAssembly(R"( local function foo(a: number) diff --git a/tests/Linter.test.cpp b/tests/Linter.test.cpp index c2f44b5b..a686fedc 100644 --- a/tests/Linter.test.cpp +++ b/tests/Linter.test.cpp @@ -8,7 +8,6 @@ #include "doctest.h" LUAU_FASTFLAG(LuauSolverV2); -LUAU_FASTFLAG(LuauParametrizedAttributeSyntax) using namespace Luau; @@ -1875,8 +1874,6 @@ end TEST_CASE_FIXTURE(Fixture, "DeprecatedAttributeWithParams") { - ScopedFastFlag sff{FFlag::LuauParametrizedAttributeSyntax, true}; - // @deprecated works on local functions { LintResult result = lint(R"( diff --git a/tests/NonStrictTypeChecker.test.cpp b/tests/NonStrictTypeChecker.test.cpp index 9a735a14..5e45c801 100644 --- a/tests/NonStrictTypeChecker.test.cpp +++ b/tests/NonStrictTypeChecker.test.cpp @@ -15,11 +15,8 @@ #include "doctest.h" #include -LUAU_FASTFLAG(LuauNewNonStrictMoreUnknownSymbols) -LUAU_FASTFLAG(LuauNewNonStrictNoErrorsPassingNever) -LUAU_FASTFLAG(LuauNewNonStrictSuppressesDynamicRequireErrors) -LUAU_FASTFLAG(LuauNewNonStrictReportsOneIndexedErrors) LUAU_FASTFLAG(LuauUnreducedTypeFunctionsDontTriggerWarnings) +LUAU_FASTFLAG(LuauNewNonStrictBetterCheckedFunctionErrorMessage) using namespace Luau; @@ -66,7 +63,7 @@ using namespace Luau; struct NonStrictTypeCheckerFixture : Fixture { - NonStrictTypeCheckerFixture() {} + NonStrictTypeCheckerFixture() = default; CheckResult checkNonStrict(const std::string& code) { @@ -199,18 +196,9 @@ local x abs(lower(x)) )"); - if (FFlag::LuauNewNonStrictMoreUnknownSymbols) - { - LUAU_REQUIRE_ERROR_COUNT(2, result); - NONSTRICT_REQUIRE_CHECKED_ERR(Position(2, 4), "abs", result); - NONSTRICT_REQUIRE_CHECKED_ERR(Position(2, 10), "lower", result); - } - else - { - - LUAU_REQUIRE_ERROR_COUNT(1, result); - NONSTRICT_REQUIRE_CHECKED_ERR(Position(2, 4), "abs", result); - } + LUAU_REQUIRE_ERROR_COUNT(2, result); + NONSTRICT_REQUIRE_CHECKED_ERR(Position(2, 4), "abs", result); + NONSTRICT_REQUIRE_CHECKED_ERR(Position(2, 10), "lower", result); } @@ -224,14 +212,8 @@ else lower(x) end )"); - if (FFlag::LuauNewNonStrictNoErrorsPassingNever) - LUAU_REQUIRE_NO_ERRORS(result); - else - { - LUAU_REQUIRE_ERROR_COUNT(2, result); - NONSTRICT_REQUIRE_CHECKED_ERR(Position(3, 8), "abs", result); - NONSTRICT_REQUIRE_CHECKED_ERR(Position(5, 10), "lower", result); - } + + LUAU_REQUIRE_NO_ERRORS(result); } TEST_CASE_FIXTURE(NonStrictTypeCheckerFixture, "if_then_else_warns_nil_branches") @@ -274,13 +256,8 @@ if cond() then end )"); - if (FFlag::LuauNewNonStrictMoreUnknownSymbols) - { - LUAU_REQUIRE_ERROR_COUNT(1, result); - NONSTRICT_REQUIRE_CHECKED_ERR(Position(3, 8), "abs", result); - } - else - LUAU_REQUIRE_NO_ERRORS(result); + LUAU_REQUIRE_ERROR_COUNT(1, result); + NONSTRICT_REQUIRE_CHECKED_ERR(Position(3, 8), "abs", result); } TEST_CASE_FIXTURE(NonStrictTypeCheckerFixture, "if_then_no_else_err_in_cond") @@ -313,14 +290,7 @@ local x : never local y = if cond() then abs(x) else lower(x) )"); - if (FFlag::LuauNewNonStrictNoErrorsPassingNever) - LUAU_REQUIRE_NO_ERRORS(result); - else - { - LUAU_REQUIRE_ERROR_COUNT(2, result); - NONSTRICT_REQUIRE_CHECKED_ERR(Position(2, 29), "abs", result); - NONSTRICT_REQUIRE_CHECKED_ERR(Position(2, 43), "lower", result); - } + LUAU_REQUIRE_NO_ERRORS(result); } TEST_CASE_FIXTURE(NonStrictTypeCheckerFixture, "if_then_else_expr_doesnt_warn_else_branch") @@ -402,19 +372,8 @@ function f(x) end )"); - - if (FFlag::LuauNewNonStrictNoErrorsPassingNever) - { - LUAU_REQUIRE_ERROR_COUNT(1, result); - NONSTRICT_REQUIRE_FUNC_DEFINITION_ERR(Position(1, 11), "x", result); - } - else - { - LUAU_REQUIRE_ERROR_COUNT(3, result); - NONSTRICT_REQUIRE_CHECKED_ERR(Position(2, 8), "abs", result); - NONSTRICT_REQUIRE_CHECKED_ERR(Position(3, 10), "lower", result); - NONSTRICT_REQUIRE_FUNC_DEFINITION_ERR(Position(1, 11), "x", result); - } + LUAU_REQUIRE_ERROR_COUNT(1, result); + NONSTRICT_REQUIRE_FUNC_DEFINITION_ERR(Position(1, 11), "x", result); } TEST_CASE_FIXTURE(NonStrictTypeCheckerFixture, "function_def_sequencing_errors_2") @@ -426,18 +385,11 @@ local t = {function(x) end} )"); - if (FFlag::LuauNewNonStrictNoErrorsPassingNever) - { - LUAU_REQUIRE_ERROR_COUNT(1, result); - CHECK(toString(result.errors[0]) == "Argument x with type 'unknown' is used in a way that will run time error"); - } + LUAU_REQUIRE_ERROR_COUNT(1, result); + if (FFlag::LuauNewNonStrictBetterCheckedFunctionErrorMessage) + CHECK(toString(result.errors[0]) == "the argument 'x' is used in a way that will error at runtime"); else - { - LUAU_REQUIRE_ERROR_COUNT(3, result); - NONSTRICT_REQUIRE_CHECKED_ERR(Position(2, 8), "abs", result); - NONSTRICT_REQUIRE_CHECKED_ERR(Position(3, 10), "lower", result); - CHECK(toString(result.errors[2]) == "Argument x with type 'unknown' is used in a way that will run time error"); - } + CHECK(toString(result.errors[0]) == "Argument x with type 'unknown' is used in a way that will run time error"); } TEST_CASE_FIXTURE(NonStrictTypeCheckerFixture, "local_fn_produces_error") @@ -475,14 +427,7 @@ function f(x: never) end )"); - if (FFlag::LuauNewNonStrictNoErrorsPassingNever) - LUAU_REQUIRE_NO_ERRORS(result); - else - { - LUAU_REQUIRE_ERROR_COUNT(2, result); - NONSTRICT_REQUIRE_CHECKED_ERR(Position(3, 12), "abs", result); - NONSTRICT_REQUIRE_CHECKED_ERR(Position(5, 14), "lower", result); - } + LUAU_REQUIRE_NO_ERRORS(result); } TEST_CASE_FIXTURE(NonStrictTypeCheckerFixture, "function_def_if_no_else") @@ -788,8 +733,6 @@ TEST_CASE_FIXTURE(Fixture, "incomplete_function_annotation") TEST_CASE_FIXTURE(Fixture, "unknown_globals_in_function_calls") { - ScopedFastFlag sff{FFlag::LuauNewNonStrictMoreUnknownSymbols, true}; - CheckResult result = check(Mode::Nonstrict, R"( local function foo() : () bar() @@ -804,8 +747,6 @@ TEST_CASE_FIXTURE(Fixture, "unknown_globals_in_function_calls") TEST_CASE_FIXTURE(Fixture, "unknown_globals_in_one_sided_conditionals") { - ScopedFastFlag sff{FFlag::LuauNewNonStrictMoreUnknownSymbols, true}; - CheckResult result = check(Mode::Nonstrict, R"( local function foo(cond) : () if cond then @@ -822,7 +763,7 @@ TEST_CASE_FIXTURE(Fixture, "unknown_globals_in_one_sided_conditionals") TEST_CASE_FIXTURE(BuiltinsFixture, "new_non_strict_should_suppress_dynamic_require_errors") { - ScopedFastFlag sffs[] = {{FFlag::LuauSolverV2, true}, {FFlag::LuauNewNonStrictSuppressesDynamicRequireErrors, true}}; + ScopedFastFlag sff{FFlag::LuauSolverV2, true}; // Avoid warning about dynamic requires in new nonstrict mode CheckResult result = check(Mode::Nonstrict, R"( function passThrough(module) @@ -845,7 +786,7 @@ end TEST_CASE_FIXTURE(BuiltinsFixture, "new_non_strict_should_suppress_unknown_require_errors") { - ScopedFastFlag sffs[] = {{FFlag::LuauSolverV2, true}, {FFlag::LuauNewNonStrictSuppressesDynamicRequireErrors, true}}; + ScopedFastFlag sff{FFlag::LuauSolverV2, true}; // Avoid warning about dynamic requires in new nonstrict mode CheckResult result = check(Mode::Nonstrict, R"( @@ -869,7 +810,6 @@ require("@self/NonExistent") TEST_CASE_FIXTURE(NonStrictTypeCheckerFixture, "new_non_strict_stringifies_checked_function_errors_as_one_indexed") { - ScopedFastFlag sff = {FFlag::LuauNewNonStrictReportsOneIndexedErrors, true}; CheckResult result = checkNonStrict(R"( getAllTheArgsWrong(3, true, "what") )"); @@ -880,9 +820,24 @@ getAllTheArgsWrong(3, true, "what") CHECK(err1 != nullptr); CHECK(err2 != nullptr); CHECK(err3 != nullptr); - CHECK_EQ("Function 'getAllTheArgsWrong' expects 'string' at argument #1, but got 'number'", toString(result.errors[0])); - CHECK_EQ("Function 'getAllTheArgsWrong' expects 'number' at argument #2, but got 'boolean'", toString(result.errors[1])); - CHECK_EQ("Function 'getAllTheArgsWrong' expects 'boolean' at argument #3, but got 'string'", toString(result.errors[2])); + if (FFlag::LuauNewNonStrictBetterCheckedFunctionErrorMessage) + { + CHECK_EQ( + "the function 'getAllTheArgsWrong' expects to get a string as its 1st argument, but is being given a number", toString(result.errors[0]) + ); + CHECK_EQ( + "the function 'getAllTheArgsWrong' expects to get a number as its 2nd argument, but is being given a boolean", toString(result.errors[1]) + ); + CHECK_EQ( + "the function 'getAllTheArgsWrong' expects to get a boolean as its 3rd argument, but is being given a string", toString(result.errors[2]) + ); + } + else + { + CHECK_EQ("Function 'getAllTheArgsWrong' expects 'string' at argument #1, but got 'number'", toString(result.errors[0])); + CHECK_EQ("Function 'getAllTheArgsWrong' expects 'number' at argument #2, but got 'boolean'", toString(result.errors[1])); + CHECK_EQ("Function 'getAllTheArgsWrong' expects 'boolean' at argument #3, but got 'string'", toString(result.errors[2])); + } } TEST_CASE_FIXTURE(NonStrictTypeCheckerFixture, "new_non_strict_skips_warnings_on_unreduced_typefunctions") diff --git a/tests/NonstrictMode.test.cpp b/tests/NonstrictMode.test.cpp index b7ea3a0e..62a8f856 100644 --- a/tests/NonstrictMode.test.cpp +++ b/tests/NonstrictMode.test.cpp @@ -60,8 +60,6 @@ TEST_CASE_FIXTURE(Fixture, "infer_the_maximum_number_of_values_the_function_coul REQUIRE_EQ("(any) -> (...any)", toString(t)); } -#if 0 -// Maybe we want this? TEST_CASE_FIXTURE(Fixture, "return_annotation_is_still_checked") { CheckResult result = check(R"( @@ -72,7 +70,6 @@ TEST_CASE_FIXTURE(Fixture, "return_annotation_is_still_checked") REQUIRE_NE(*getBuiltins()->anyType, *requireType("foo")); } -#endif TEST_CASE_FIXTURE(Fixture, "function_parameters_are_any") { diff --git a/tests/Normalize.test.cpp b/tests/Normalize.test.cpp index fbae3183..ea298353 100644 --- a/tests/Normalize.test.cpp +++ b/tests/Normalize.test.cpp @@ -5,19 +5,17 @@ #include "Luau/AstQuery.h" #include "Luau/Common.h" #include "Luau/Type.h" +#include "ScopedFlags.h" #include "doctest.h" #include "Luau/Normalize.h" -#include "Luau/BuiltinDefinitions.h" #include LUAU_FASTFLAG(LuauSolverV2) LUAU_FASTINT(LuauTypeInferRecursionLimit) LUAU_FASTINT(LuauNormalizeIntersectionLimit) LUAU_FASTINT(LuauNormalizeUnionLimit) -LUAU_FASTFLAG(LuauNormalizationReorderFreeTypeIntersect) -LUAU_FASTFLAG(LuauReturnMappedGenericPacksFromSubtyping2) -LUAU_FASTFLAG(LuauSolverAgnosticStringification) +LUAU_FASTFLAG(LuauNormalizerUnionTyvarsTakeMaxSize) using namespace Luau; @@ -105,9 +103,10 @@ TEST_CASE_FIXTURE(IsSubtypeFixture, "variadic_functions_with_no_head") CHECK(!isSubtype(a, b)); } -#if 0 TEST_CASE_FIXTURE(IsSubtypeFixture, "variadic_function_with_head") { + ScopedFastFlag _{FFlag::LuauSolverV2, true}; + check(R"( local a: (...number) -> () local b: (number, number) -> () @@ -119,7 +118,6 @@ TEST_CASE_FIXTURE(IsSubtypeFixture, "variadic_function_with_head") CHECK(!isSubtype(b, a)); CHECK(isSubtype(a, b)); } -#endif TEST_CASE_FIXTURE(IsSubtypeFixture, "union") { @@ -254,9 +252,10 @@ TEST_CASE_FIXTURE(IsSubtypeFixture, "tables") CHECK(!isSubtype(b, d)); } -#if 0 TEST_CASE_FIXTURE(IsSubtypeFixture, "table_indexers_are_invariant") { + ScopedFastFlag _{FFlag::LuauSolverV2, true}; + check(R"( local a: {[string]: number} local b: {[string]: any} @@ -276,6 +275,8 @@ TEST_CASE_FIXTURE(IsSubtypeFixture, "table_indexers_are_invariant") TEST_CASE_FIXTURE(IsSubtypeFixture, "mismatched_indexers") { + ScopedFastFlag _{FFlag::LuauSolverV2, true}; + check(R"( local a: {x: number} local b: {[string]: number} @@ -293,9 +294,9 @@ TEST_CASE_FIXTURE(IsSubtypeFixture, "mismatched_indexers") CHECK(isSubtype(b, c)); } +#if 0 TEST_CASE_FIXTURE(IsSubtypeFixture, "cyclic_table") { - ScopedFastFlag sff{FFlag::LuauSolverAgnosticStringification, true}; check(R"( type A = {method: (A) -> ()} local a: A @@ -733,7 +734,7 @@ TEST_CASE_FIXTURE(NormalizeFixture, "union_function_and_top_function") TEST_CASE_FIXTURE(NormalizeFixture, "negated_function_is_anything_except_a_function") { - CHECK("(boolean | buffer | class | number | string | table | thread)?" == toString(normal(R"( + CHECK("(boolean | buffer | number | string | table | thread | userdata)?" == toString(normal(R"( Not )"))); } @@ -758,7 +759,7 @@ TEST_CASE_FIXTURE(NormalizeFixture, "trivial_intersection_inhabited") TEST_CASE_FIXTURE(NormalizeFixture, "bare_negated_boolean") { - CHECK("(buffer | class | function | number | string | table | thread)?" == toString(normal(R"( + CHECK("(buffer | function | number | string | table | thread | userdata)?" == toString(normal(R"( Not )"))); } @@ -794,7 +795,6 @@ TEST_CASE_FIXTURE(Fixture, "higher_order_function_with_annotation") TEST_CASE_FIXTURE(Fixture, "cyclic_table_normalizes_sensibly") { - ScopedFastFlag sff{FFlag::LuauSolverAgnosticStringification, true}; CheckResult result = check(R"( local Cyclic = {} function Cyclic.get() @@ -855,7 +855,6 @@ TEST_CASE_FIXTURE(NormalizeFixture, "narrow_union_of_extern_types_with_intersect TEST_CASE_FIXTURE(NormalizeFixture, "intersection_of_metatables_where_the_metatable_is_top_or_bottom") { - ScopedFastFlag sff{FFlag::LuauSolverAgnosticStringification, true}; CHECK("{ @metatable *error-type*, { } }" == toString(normal("Mt<{}, any> & Mt<{}, err>"))); } @@ -923,9 +922,10 @@ TEST_CASE_FIXTURE(NormalizeFixture, "negations_of_extern_types") { createSomeExternTypes(getFrontend()); CHECK("(Parent & ~Child) | Unrelated" == toString(normal("(Parent & Not) | Unrelated"))); - CHECK("((class & ~Child) | boolean | buffer | function | number | string | table | thread)?" == toString(normal("Not"))); + + CHECK("((userdata & ~Child) | boolean | buffer | function | number | string | table | thread)?" == toString(normal("Not"))); CHECK("never" == toString(normal("Not & Child"))); - CHECK("((class & ~Parent) | Child | boolean | buffer | function | number | string | table | thread)?" == toString(normal("Not | Child"))); + CHECK("((userdata & ~Parent) | Child | boolean | buffer | function | number | string | table | thread)?" == toString(normal("Not | Child"))); CHECK("(boolean | buffer | function | number | string | table | thread)?" == toString(normal("Not"))); CHECK( "(Parent | Unrelated | boolean | buffer | function | number | string | table | thread)?" == @@ -948,7 +948,6 @@ TEST_CASE_FIXTURE(NormalizeFixture, "extern_types_and_never") TEST_CASE_FIXTURE(NormalizeFixture, "top_table_type") { - ScopedFastFlag sff{FFlag::LuauSolverAgnosticStringification, true}; CHECK("table" == toString(normal("{} | tbl"))); CHECK("{ }" == toString(normal("{} & tbl"))); CHECK("never" == toString(normal("number & tbl"))); @@ -957,7 +956,7 @@ TEST_CASE_FIXTURE(NormalizeFixture, "top_table_type") TEST_CASE_FIXTURE(NormalizeFixture, "negations_of_tables") { CHECK(nullptr == toNormalizedType("Not<{}>", FFlag::LuauSolverV2 ? 1 : 0)); - CHECK("(boolean | buffer | class | function | number | string | thread)?" == toString(normal("Not"))); + CHECK("(boolean | buffer | function | number | string | thread | userdata)?" == toString(normal("Not"))); CHECK("table" == toString(normal("Not>"))); } @@ -1119,10 +1118,7 @@ TEST_CASE_FIXTURE(NormalizeFixture, "free_type_and_not_truthy") TEST_CASE_FIXTURE(NormalizeFixture, "free_type_intersection_ordering") { - ScopedFastFlag sff[] = { - {FFlag::LuauSolverV2, true}, // Affects stringification of free types. - {FFlag::LuauNormalizationReorderFreeTypeIntersect, true}, - }; + ScopedFastFlag sff{FFlag::LuauSolverV2, true}; // Affects stringification of free types. TypeId freeTy = arena.freshType(getBuiltins(), getGlobalScope()); TypeId orderA = arena.addType(IntersectionType{{freeTy, getBuiltins()->stringType}}); @@ -1133,10 +1129,30 @@ TEST_CASE_FIXTURE(NormalizeFixture, "free_type_intersection_ordering") TypeId orderB = arena.addType(IntersectionType{{getBuiltins()->stringType, freeTy}}); auto normB = normalize(orderB); REQUIRE(normB); - // Prior to LuauNormalizationReorderFreeTypeIntersect this became `never` :skull: CHECK_EQ("'a & string", toString(typeFromNormal(*normB))); } +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())); + + TypeId target = arena.addType(IntersectionType{ + { + getBuiltins()->unknownType, + arena.addType(UnionType{std::move(options)}) + }, + }); + + // If we try to normalize 120 free variables against `unknown`, then exit + // early and claim we've hit the limit. + auto norm = normalize(target); + REQUIRE(!norm); +} + TEST_CASE_FIXTURE(BuiltinsFixture, "normalizer_should_be_able_to_detect_cyclic_tables_and_not_stack_overflow") { if (!FFlag::LuauSolverV2) @@ -1261,13 +1277,11 @@ do end InternalCompilerError ); } + #if 0 TEST_CASE_FIXTURE(BuiltinsFixture, "fuzz_union_type_pack_cycle") { - ScopedFastFlag sff[] = { - {FFlag::LuauSolverV2, true}, - {FFlag::LuauReturnMappedGenericPacksFromSubtyping2, true}, - }; + ScopedFastFlag sff{FFlag::LuauSolverV2, true}; ScopedFastInt sfi{FInt::LuauTypeInferRecursionLimit, 0}; // FIXME CLI-153131: This is constructing a cyclic type pack diff --git a/tests/OverloadResolver.test.cpp b/tests/OverloadResolver.test.cpp new file mode 100644 index 00000000..94e565a2 --- /dev/null +++ b/tests/OverloadResolver.test.cpp @@ -0,0 +1,163 @@ +// This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details + +#include "doctest.h" +#include "Fixture.h" + +#include "Luau/OverloadResolution.h" +#include "Luau/Normalize.h" +#include "Luau/UnifierSharedState.h" + +LUAU_FASTFLAG(LuauConsiderErrorSuppressionInTypes) + +using namespace Luau; + +struct OverloadResolverFixture : Fixture +{ + TypeArena arena; + SimplifierPtr simplifier = newSimplifier(NotNull{&arena}, getBuiltins()); + UnifierSharedState sharedState{&ice}; + Normalizer normalizer{&arena, getBuiltins(), NotNull{&sharedState}, FFlag::LuauSolverV2 ? SolverMode::New : SolverMode::Old}; + InternalErrorReporter iceReporter; + TypeCheckLimits limits; + TypeFunctionRuntime typeFunctionRuntime{NotNull{&iceReporter}, NotNull{&limits}}; + Scope rootScope{getBuiltins()->emptyTypePack}; + Location callLocation; + + OverloadResolver resolver = mkResolver(); + + OverloadResolver mkResolver() + { + return OverloadResolver{ + getBuiltins(), + NotNull{&arena}, + NotNull{simplifier.get()}, + NotNull{&normalizer}, + NotNull{&typeFunctionRuntime}, + NotNull{&rootScope}, + NotNull{&iceReporter}, + NotNull{&limits}, + callLocation + }; + } + + DenseHashSet kEmptySet{nullptr}; + Location kDummyLocation; + AstExprConstantNil kDummyExpr{kDummyLocation}; + std::vector kEmptyExprs; + + TypePackId pack(std::initializer_list tys) + { + return arena.addTypePack(tys); + } + + TypePackId pack(std::initializer_list tys, TypePackVariant tail) + { + return arena.addTypePack(tys, arena.addTypePack(std::move(tail))); + } + + TypeId fn(std::initializer_list args, std::initializer_list rets) + { + return arena.addType(FunctionType{pack(args), pack(rets)}); + } + + // `&` + TypeId meet(TypeId a, TypeId b) + { + return arena.addType(IntersectionType{{a, b}}); + } + TypeId meet(std::initializer_list parts) + { + return arena.addType(IntersectionType{parts}); + } + + // (number) -> number + const TypeId numberToNumber = fn({getBuiltins()->numberType}, {getBuiltins()->numberType}); + // (number, number) -> number + const TypeId numberNumberToNumber = fn({getBuiltins()->numberType, getBuiltins()->numberType}, {getBuiltins()->numberType}); + // (number) -> string + const TypeId numberToString = fn({getBuiltins()->numberType}, {getBuiltins()->stringType}); + // (string) -> string + const TypeId stringToString = fn({getBuiltins()->stringType}, {getBuiltins()->stringType}); + + // (number) -> number & (string) -> string + const TypeId numberToNumberAndStringToString = meet(numberToNumber, stringToString); + // (number) -> number & (number, number) -> number + const TypeId numberToNumberAndNumberNumberToNumber = meet(numberToNumber, numberNumberToNumber); +}; + +TEST_SUITE_BEGIN("OverloadResolverTest"); + +TEST_CASE_FIXTURE(OverloadResolverFixture, "basic_overload_selection") +{ + // ty: (number) -> number & (string) -> string + // args: (number) + auto [analysis, overload] = + resolver.selectOverload(numberToNumberAndStringToString, pack({getBuiltins()->numberType}), NotNull{&kEmptySet}, false); + + REQUIRE_EQ(OverloadResolver::Analysis::Ok, analysis); + REQUIRE_EQ(numberToNumber, overload); +} + +TEST_CASE_FIXTURE(OverloadResolverFixture, "basic_overload_selection1") +{ + // ty: (number) -> number & (string) -> string + // args: (string) + auto [analysis, overload] = + resolver.selectOverload(numberToNumberAndStringToString, pack({getBuiltins()->stringType}), NotNull{&kEmptySet}, false); + + REQUIRE_EQ(OverloadResolver::Analysis::Ok, analysis); + REQUIRE_EQ(stringToString, overload); +} + +TEST_CASE_FIXTURE(OverloadResolverFixture, "overloads_with_different_arities") +{ + // ty: (number) -> number & (number, number) -> number + // args: (number) + auto [analysis, overload] = + resolver.selectOverload(numberToNumberAndNumberNumberToNumber, pack({getBuiltins()->numberType}), NotNull{&kEmptySet}, false); + + REQUIRE_EQ(OverloadResolver::Analysis::Ok, analysis); + REQUIRE_EQ(numberToNumber, overload); +} + +TEST_CASE_FIXTURE(OverloadResolverFixture, "overloads_with_different_arities1") +{ + // ty: (number) -> number & (number, number) -> number + // args: (number, number) + auto [analysis, overload] = resolver.selectOverload( + numberToNumberAndNumberNumberToNumber, pack({getBuiltins()->numberType, getBuiltins()->numberType}), NotNull{&kEmptySet}, false + ); + + REQUIRE_EQ(OverloadResolver::Analysis::Ok, analysis); + REQUIRE_EQ(numberNumberToNumber, overload); +} + +TEST_CASE_FIXTURE(OverloadResolverFixture, "separate_non_viable_overloads_by_arity_mismatch") +{ + // ty: ((number)->number) & ((number)->string) & ((number, number)->number) + // args: (string) + OverloadResolver r = mkResolver(); + + const TypePack args = TypePack{{builtinTypes->stringType}, std::nullopt}; + r.resolve(meet({numberToNumber, numberToString, numberNumberToNumber}), &args, &kDummyExpr, &kEmptyExprs, NotNull{&kEmptySet}); + + CHECK(r.ok.empty()); + CHECK(r.nonFunctions.empty()); + CHECK_EQ(1, r.arityMismatches.size()); + CHECK_EQ(numberNumberToNumber, r.arityMismatches[0].first); + + CHECK_EQ(2, r.nonviableOverloads.size()); + bool numberToNumberFound = false; + bool numberToStringFound = false; + for (const auto& [ty, _] : r.nonviableOverloads) + { + if (ty == numberToNumber) + numberToNumberFound = true; + else if (ty == numberToString) + numberToStringFound = true; + } + CHECK(numberToNumberFound); + CHECK(numberToStringFound); +} + +TEST_SUITE_END(); // OverloadResolverTest diff --git a/tests/Parser.test.cpp b/tests/Parser.test.cpp index 2ca71e73..e275a5ab 100644 --- a/tests/Parser.test.cpp +++ b/tests/Parser.test.cpp @@ -18,8 +18,6 @@ LUAU_FASTINT(LuauTypeLengthLimit) LUAU_FASTINT(LuauParseErrorLimit) LUAU_FASTFLAG(LuauSolverV2) LUAU_DYNAMIC_FASTFLAG(DebugLuauReportReturnTypeVariadicWithTypeSuffix) -LUAU_FASTFLAG(LuauParseIncompleteInterpStringsWithLocation) -LUAU_FASTFLAG(LuauParametrizedAttributeSyntax) // Clip with DebugLuauReportReturnTypeVariadicWithTypeSuffix extern bool luau_telemetry_parsed_return_type_variadic_with_type_suffix; @@ -952,7 +950,6 @@ TEST_CASE_FIXTURE(Fixture, "parse_interpolated_string_double_brace_mid") TEST_CASE_FIXTURE(Fixture, "parse_interpolated_string_without_end_brace") { - ScopedFastFlag sff{FFlag::LuauParseIncompleteInterpStringsWithLocation, true}; auto columnOfEndBraceError = [this](const char* code) { try @@ -3731,8 +3728,6 @@ end)"); TEST_CASE_FIXTURE(Fixture, "parse_parametrized_attribute_on_function_stat") { - ScopedFastFlag sff{FFlag::LuauParametrizedAttributeSyntax, true}; - AstStatBlock* stat = parse(R"( @[deprecated{ use = "greetng", reason = "Using is too causal"}] function hello(x, y) @@ -3753,7 +3748,6 @@ end)"); TEST_CASE_FIXTURE(Fixture, "non_literal_attribute_arguments_is_not_allowed") { - ScopedFastFlag sff{FFlag::LuauParametrizedAttributeSyntax, true}; ParseResult result = tryParse(R"( @[deprecated{ reason = reasonString }] function hello(x, y) @@ -3767,7 +3761,6 @@ end)"); TEST_CASE_FIXTURE(Fixture, "unknown_arguments_for_depricated_is_not_allowed") { - ScopedFastFlag sff{FFlag::LuauParametrizedAttributeSyntax, true}; ParseResult result = tryParse(R"( @[deprecated({}, "Very deprecated")] function hello(x, y) @@ -3808,7 +3801,6 @@ end)"); TEST_CASE_FIXTURE(Fixture, "do_not_hang_on_incomplete_attribute_list") { - ScopedFastFlag sff{FFlag::LuauParametrizedAttributeSyntax, true}; ParseResult result = tryParse(R"( @[] function hello(x, y) @@ -4310,7 +4302,6 @@ TEST_CASE_FIXTURE(Fixture, "parsing_string_union_indexers") TEST_CASE_FIXTURE(Fixture, "parsing_incomplete_string_interpolation_missing_curly_at_eof") { - ScopedFastFlag _{FFlag::LuauParseIncompleteInterpStringsWithLocation, true}; auto parseResult = tryParse(R"(print(`{e.x} {e.a)"); const auto first = parseResult.root->body.data[0]; auto expr = first->as(); @@ -4331,7 +4322,6 @@ TEST_CASE_FIXTURE(Fixture, "parsing_incomplete_string_interpolation_missing_curl TEST_CASE_FIXTURE(Fixture, "parsing_incomplete_string_interpolation_missing_backtick_at_eof") { - ScopedFastFlag _{FFlag::LuauParseIncompleteInterpStringsWithLocation, true}; auto parseResult = tryParse(R"(print(`{e.x} {e.a})"); const auto first = parseResult.root->body.data[0]; auto expr = first->as(); @@ -4352,7 +4342,6 @@ TEST_CASE_FIXTURE(Fixture, "parsing_incomplete_string_interpolation_missing_back TEST_CASE_FIXTURE(Fixture, "parsing_incomplete_string_interpolation_missing_curly_with_backtick_at_eof") { - ScopedFastFlag _{FFlag::LuauParseIncompleteInterpStringsWithLocation, true}; auto parseResult = tryParse(R"(print(`{e.x} {e.a`)"); const auto first = parseResult.root->body.data[0]; auto expr = first->as(); @@ -4373,7 +4362,6 @@ TEST_CASE_FIXTURE(Fixture, "parsing_incomplete_string_interpolation_missing_curl TEST_CASE_FIXTURE(Fixture, "parsing_incomplete_string_interpolation_missing_curly_broken_string") { - ScopedFastFlag _{FFlag::LuauParseIncompleteInterpStringsWithLocation, true}; auto parseResult = tryParse(R"(print(`{e.x} {e.a )"); const auto first = parseResult.root->body.data[0]; @@ -4395,7 +4383,6 @@ TEST_CASE_FIXTURE(Fixture, "parsing_incomplete_string_interpolation_missing_curl TEST_CASE_FIXTURE(Fixture, "parsing_incomplete_string_interpolation_missing_backtick_broken_string") { - ScopedFastFlag _{FFlag::LuauParseIncompleteInterpStringsWithLocation, true}; auto parseResult = tryParse(R"(print(`{e.x} {e.a} )"); const auto first = parseResult.root->body.data[0]; @@ -4417,7 +4404,6 @@ TEST_CASE_FIXTURE(Fixture, "parsing_incomplete_string_interpolation_missing_back TEST_CASE_FIXTURE(Fixture, "parsing_incomplete_string_interpolation_missing_curly_with_backtick_broken_string") { - ScopedFastFlag _{FFlag::LuauParseIncompleteInterpStringsWithLocation, true}; auto parseResult = tryParse(R"(print(`{e.x} {e.a` )"); const auto first = parseResult.root->body.data[0]; diff --git a/tests/Transpiler.test.cpp b/tests/PrettyPrinter.test.cpp similarity index 60% rename from tests/Transpiler.test.cpp rename to tests/PrettyPrinter.test.cpp index cd47e8ff..ba842386 100644 --- a/tests/Transpiler.test.cpp +++ b/tests/PrettyPrinter.test.cpp @@ -1,9 +1,9 @@ // This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details #include "Luau/Parser.h" +#include "Luau/PrettyPrinter.h" #include "Luau/TypeAttach.h" #include "Luau/TypeInfer.h" #include "Luau/Type.h" -#include "Luau/Transpiler.h" #include "Fixture.h" #include "ScopedFlags.h" @@ -12,7 +12,7 @@ using namespace Luau; -TEST_SUITE_BEGIN("TranspilerTests"); +TEST_SUITE_BEGIN("PrettyPrinterTests"); TEST_CASE("test_1") { @@ -26,50 +26,50 @@ local function isPortal(element) end )"; - CHECK_EQ(example, transpile(example).code); + CHECK_EQ(example, prettyPrint(example).code); } TEST_CASE("string_literals") { const std::string code = R"( local S='abcdef\n\f\a\020' )"; - CHECK_EQ(code, transpile(code).code); + CHECK_EQ(code, prettyPrint(code).code); } TEST_CASE("string_literals_containing_utf8") { const std::string code = R"( local S='lalala こんにちは' )"; // Konichiwa! - CHECK_EQ(code, transpile(code).code); + CHECK_EQ(code, prettyPrint(code).code); } TEST_CASE("if_stmt_spaces_around_tokens") { const std::string one = R"( if This then Once() end)"; - CHECK_EQ(one, transpile(one).code); + CHECK_EQ(one, prettyPrint(one).code); const std::string two = R"( if This then Once() end)"; - CHECK_EQ(two, transpile(two).code); + CHECK_EQ(two, prettyPrint(two).code); const std::string three = R"( if This then Once() end)"; - CHECK_EQ(three, transpile(three).code); + CHECK_EQ(three, prettyPrint(three).code); const std::string four = R"( if This then Once() end)"; - CHECK_EQ(four, transpile(four).code); + CHECK_EQ(four, prettyPrint(four).code); const std::string five = R"( if This then Once() else Other() end)"; - CHECK_EQ(five, transpile(five).code); + CHECK_EQ(five, prettyPrint(five).code); const std::string six = R"( if This then Once() else Other() end)"; - CHECK_EQ(six, transpile(six).code); + CHECK_EQ(six, prettyPrint(six).code); const std::string seven = R"( if This then Once() elseif true then Other() end)"; - CHECK_EQ(seven, transpile(seven).code); + CHECK_EQ(seven, prettyPrint(seven).code); const std::string eight = R"( if This then Once() elseif true then Other() end)"; - CHECK_EQ(eight, transpile(eight).code); + CHECK_EQ(eight, prettyPrint(eight).code); const std::string nine = R"( if This then Once() elseif true then Other() end)"; - CHECK_EQ(nine, transpile(nine).code); + CHECK_EQ(nine, prettyPrint(nine).code); } TEST_CASE("elseif_chains_indent_sensibly") @@ -86,118 +86,118 @@ TEST_CASE("elseif_chains_indent_sensibly") end )"; - CHECK_EQ(code, transpile(code).code); + CHECK_EQ(code, prettyPrint(code).code); } TEST_CASE("strips_type_annotations") { const std::string code = R"( local s: string= 'hello there' )"; const std::string expected = R"( local s = 'hello there' )"; - CHECK_EQ(expected, transpile(code).code); + CHECK_EQ(expected, prettyPrint(code).code); } TEST_CASE("strips_type_assertion_expressions") { const std::string code = R"( local s= some_function() :: any+ something_else() :: number )"; const std::string expected = R"( local s= some_function() + something_else() )"; - CHECK_EQ(expected, transpile(code).code); + CHECK_EQ(expected, prettyPrint(code).code); } TEST_CASE("function_taking_ellipsis") { const std::string code = R"( function F(...) end )"; - CHECK_EQ(code, transpile(code).code); + CHECK_EQ(code, prettyPrint(code).code); } TEST_CASE("omit_decimal_place_for_integers") { const std::string code = R"( local a=5, 6, 7, 3.141, 1.1290000000000002e+45 )"; - CHECK_EQ(code, transpile(code).code); + CHECK_EQ(code, prettyPrint(code).code); } TEST_CASE("for_loop") { const std::string one = R"( for i=1,10 do end )"; - CHECK_EQ(one, transpile(one).code); + CHECK_EQ(one, prettyPrint(one).code); const std::string code = R"( for i=5,6,7 do end )"; - CHECK_EQ(code, transpile(code).code); + CHECK_EQ(code, prettyPrint(code).code); } TEST_CASE("for_loop_spaces_around_tokens") { const std::string one = R"( for index = 1, 10 do call(index) end )"; - CHECK_EQ(one, transpile(one).code); + CHECK_EQ(one, prettyPrint(one).code); const std::string two = R"( for index = 1 , 10 do call(index) end )"; - CHECK_EQ(two, transpile(two).code); + CHECK_EQ(two, prettyPrint(two).code); const std::string three = R"( for index = 1, 10 , 3 do call(index) end )"; - CHECK_EQ(three, transpile(three).code); + CHECK_EQ(three, prettyPrint(three).code); const std::string four = R"( for index = 1, 10 do call(index) end )"; - CHECK_EQ(four, transpile(four).code); + CHECK_EQ(four, prettyPrint(four).code); const std::string five = R"( for index = 1, 10 do call(index) end )"; - CHECK_EQ(five, transpile(five).code); + CHECK_EQ(five, prettyPrint(five).code); } TEST_CASE("for_in_loop") { const std::string code = R"( for k, v in ipairs(x)do end )"; - CHECK_EQ(code, transpile(code).code); + CHECK_EQ(code, prettyPrint(code).code); } TEST_CASE("for_in_loop_spaces_around_tokens") { const std::string one = R"( for k, v in ipairs(x) do end )"; - CHECK_EQ(one, transpile(one).code); + CHECK_EQ(one, prettyPrint(one).code); const std::string two = R"( for k, v in ipairs(x) do end )"; - CHECK_EQ(two, transpile(two).code); + CHECK_EQ(two, prettyPrint(two).code); const std::string three = R"( for k , v in ipairs(x) do end )"; - CHECK_EQ(three, transpile(three).code); + CHECK_EQ(three, prettyPrint(three).code); const std::string four = R"( for k, v in next , t do end )"; - CHECK_EQ(four, transpile(four).code); + CHECK_EQ(four, prettyPrint(four).code); const std::string five = R"( for k, v in ipairs(x) do end )"; - CHECK_EQ(five, transpile(five).code); + CHECK_EQ(five, prettyPrint(five).code); } TEST_CASE("for_in_single_variable") { const std::string one = R"( for key in pairs(x) do end )"; - CHECK_EQ(one, transpile(one).code); + CHECK_EQ(one, prettyPrint(one).code); } TEST_CASE("while_loop") { const std::string code = R"( while f(x)do print() end )"; - CHECK_EQ(code, transpile(code).code); + CHECK_EQ(code, prettyPrint(code).code); } TEST_CASE("while_loop_spaces_around_tokens") { const std::string one = R"( while f(x) do print() end )"; - CHECK_EQ(one, transpile(one).code); + CHECK_EQ(one, prettyPrint(one).code); const std::string two = R"( while f(x) do print() end )"; - CHECK_EQ(two, transpile(two).code); + CHECK_EQ(two, prettyPrint(two).code); const std::string three = R"( while f(x) do print() end )"; - CHECK_EQ(three, transpile(three).code); + CHECK_EQ(three, prettyPrint(three).code); const std::string four = R"( while f(x) do print() end )"; - CHECK_EQ(four, transpile(four).code); + CHECK_EQ(four, prettyPrint(four).code); } TEST_CASE("repeat_until_loop") { const std::string code = R"( repeat print() until f(x) )"; - CHECK_EQ(code, transpile(code).code); + CHECK_EQ(code, prettyPrint(code).code); } TEST_CASE("repeat_until_loop_condition_on_new_line") @@ -207,271 +207,271 @@ TEST_CASE("repeat_until_loop_condition_on_new_line") print() until f(x) )"; - CHECK_EQ(code, transpile(code).code); + CHECK_EQ(code, prettyPrint(code).code); } TEST_CASE("lambda") { const std::string one = R"( local p=function(o, m, g) return 77 end )"; - CHECK_EQ(one, transpile(one).code); + CHECK_EQ(one, prettyPrint(one).code); const std::string two = R"( local p=function(o, m, g,...) return 77 end )"; - CHECK_EQ(two, transpile(two).code); + CHECK_EQ(two, prettyPrint(two).code); } TEST_CASE("local_assignment") { const std::string one = R"( local x = 1 )"; - CHECK_EQ(one, transpile(one).code); + CHECK_EQ(one, prettyPrint(one).code); const std::string two = R"( local x, y, z = 1, 2, 3 )"; - CHECK_EQ(two, transpile(two).code); + CHECK_EQ(two, prettyPrint(two).code); const std::string three = R"( local x )"; - CHECK_EQ(three, transpile(three).code); + CHECK_EQ(three, prettyPrint(three).code); } TEST_CASE("local_assignment_spaces_around_tokens") { const std::string one = R"( local x = 1 )"; - CHECK_EQ(one, transpile(one).code); + CHECK_EQ(one, prettyPrint(one).code); const std::string two = R"( local x = 1 )"; - CHECK_EQ(two, transpile(two).code); + CHECK_EQ(two, prettyPrint(two).code); const std::string three = R"( local x = 1 )"; - CHECK_EQ(three, transpile(three).code); + CHECK_EQ(three, prettyPrint(three).code); const std::string four = R"( local x , y = 1, 2 )"; - CHECK_EQ(four, transpile(four).code); + CHECK_EQ(four, prettyPrint(four).code); const std::string five = R"( local x, y = 1, 2 )"; - CHECK_EQ(five, transpile(five).code); + CHECK_EQ(five, prettyPrint(five).code); const std::string six = R"( local x, y = 1 , 2 )"; - CHECK_EQ(six, transpile(six).code); + CHECK_EQ(six, prettyPrint(six).code); const std::string seven = R"( local x, y = 1, 2 )"; - CHECK_EQ(seven, transpile(seven).code); + CHECK_EQ(seven, prettyPrint(seven).code); } TEST_CASE("local_function") { const std::string one = R"( local function p(o, m, g) return 77 end )"; - CHECK_EQ(one, transpile(one).code); + CHECK_EQ(one, prettyPrint(one).code); const std::string two = R"( local function p(o, m, g,...) return 77 end )"; - CHECK_EQ(two, transpile(two).code); + CHECK_EQ(two, prettyPrint(two).code); } TEST_CASE("local_function_spaces_around_tokens") { const std::string one = R"( local function p(o, m, ...) end )"; - CHECK_EQ(one, transpile(one).code); + CHECK_EQ(one, prettyPrint(one).code); const std::string two = R"( local function p(o, m, ...) end )"; - CHECK_EQ(two, transpile(two).code); + CHECK_EQ(two, prettyPrint(two).code); } TEST_CASE("function") { const std::string one = R"( function p(o, m, g) return 77 end )"; - CHECK_EQ(one, transpile(one).code); + CHECK_EQ(one, prettyPrint(one).code); const std::string two = R"( function p(o, m, g,...) return 77 end )"; - CHECK_EQ(two, transpile(two).code); + CHECK_EQ(two, prettyPrint(two).code); } TEST_CASE("function_spaces_around_tokens") { const std::string two = R"( function p(o, m, ...) end )"; - CHECK_EQ(two, transpile(two).code); + CHECK_EQ(two, prettyPrint(two).code); const std::string three = R"( function p( o, m, ...) end )"; - CHECK_EQ(three, transpile(three).code); + CHECK_EQ(three, prettyPrint(three).code); const std::string four = R"( function p(o , m, ...) end )"; - CHECK_EQ(four, transpile(four).code); + CHECK_EQ(four, prettyPrint(four).code); const std::string five = R"( function p(o, m, ...) end )"; - CHECK_EQ(five, transpile(five).code); + CHECK_EQ(five, prettyPrint(five).code); const std::string six = R"( function p(o, m , ...) end )"; - CHECK_EQ(six, transpile(six).code); + CHECK_EQ(six, prettyPrint(six).code); const std::string seven = R"( function p(o, m, ...) end )"; - CHECK_EQ(seven, transpile(seven).code); + CHECK_EQ(seven, prettyPrint(seven).code); const std::string eight = R"( function p(o, m, ... ) end )"; - CHECK_EQ(eight, transpile(eight).code); + CHECK_EQ(eight, prettyPrint(eight).code); const std::string nine = R"( function p(o, m, ...) end )"; - CHECK_EQ(nine, transpile(nine).code); + CHECK_EQ(nine, prettyPrint(nine).code); } TEST_CASE("function_with_types_spaces_around_tokens") { std::string code = R"( function p(o: string, m: number, ...: any): string end )"; - CHECK_EQ(code, transpile(code, {}, true).code); + CHECK_EQ(code, prettyPrint(code, {}, true).code); code = R"( function p (o: string, m: number, ...: any): string end )"; - CHECK_EQ(code, transpile(code, {}, true).code); + CHECK_EQ(code, prettyPrint(code, {}, true).code); code = R"( function p(o: string, m: number, ...: any): string end )"; - CHECK_EQ(code, transpile(code, {}, true).code); + CHECK_EQ(code, prettyPrint(code, {}, true).code); code = R"( function p(o: string, m: number, ...: any): string end )"; - CHECK_EQ(code, transpile(code, {}, true).code); + CHECK_EQ(code, prettyPrint(code, {}, true).code); code = R"( function p(o: string, m: number, ...: any): string end )"; - CHECK_EQ(code, transpile(code, {}, true).code); + CHECK_EQ(code, prettyPrint(code, {}, true).code); code = R"( function p(o: string, m: number, ...: any): string end )"; - CHECK_EQ(code, transpile(code, {}, true).code); + CHECK_EQ(code, prettyPrint(code, {}, true).code); code = R"( function p(o: string, m: number, ...: any): string end )"; - CHECK_EQ(code, transpile(code, {}, true).code); + CHECK_EQ(code, prettyPrint(code, {}, true).code); code = R"( function p (o: string, m: number, ...: any): string end )"; - CHECK_EQ(code, transpile(code, {}, true).code); + CHECK_EQ(code, prettyPrint(code, {}, true).code); code = R"( function p(o : string, m: number, ...: any): string end )"; - CHECK_EQ(code, transpile(code, {}, true).code); + CHECK_EQ(code, prettyPrint(code, {}, true).code); code = R"( function p(o: string, m: number, ...: any): string end )"; - CHECK_EQ(code, transpile(code, {}, true).code); + CHECK_EQ(code, prettyPrint(code, {}, true).code); code = R"( function p(o: string , m: number, ...: any): string end )"; - CHECK_EQ(code, transpile(code, {}, true).code); + CHECK_EQ(code, prettyPrint(code, {}, true).code); code = R"( function p(o: string, m: number, ...: any): string end )"; - CHECK_EQ(code, transpile(code, {}, true).code); + CHECK_EQ(code, prettyPrint(code, {}, true).code); code = R"( function p(o: string, m: number, ...: any): string end )"; - CHECK_EQ(code, transpile(code, {}, true).code); + CHECK_EQ(code, prettyPrint(code, {}, true).code); code = R"( function p(o: string, m: number, ... : any): string end )"; - CHECK_EQ(code, transpile(code, {}, true).code); + CHECK_EQ(code, prettyPrint(code, {}, true).code); code = R"( function p(o: string, m: number, ...: any): string end )"; - CHECK_EQ(code, transpile(code, {}, true).code); + CHECK_EQ(code, prettyPrint(code, {}, true).code); code = R"( function p(o: string, m: number, ...: any ): string end )"; - CHECK_EQ(code, transpile(code, {}, true).code); + CHECK_EQ(code, prettyPrint(code, {}, true).code); code = R"( function p(o: string, m: number, ...: any) :string end )"; - CHECK_EQ(code, transpile(code, {}, true).code); + CHECK_EQ(code, prettyPrint(code, {}, true).code); code = R"( function p(o: string, m: number, ...: any): string end )"; - CHECK_EQ(code, transpile(code, {}, true).code); + CHECK_EQ(code, prettyPrint(code, {}, true).code); } TEST_CASE("returns_spaces_around_tokens") { const std::string one = R"( return 1 )"; - CHECK_EQ(one, transpile(one).code); + CHECK_EQ(one, prettyPrint(one).code); const std::string two = R"( return 1 , 2 )"; - CHECK_EQ(two, transpile(two).code); + CHECK_EQ(two, prettyPrint(two).code); const std::string three = R"( return 1, 2 )"; - CHECK_EQ(three, transpile(three).code); + CHECK_EQ(three, prettyPrint(three).code); } TEST_CASE_FIXTURE(Fixture, "type_alias_spaces_around_tokens") { std::string code = R"( type Foo = string )"; - CHECK_EQ(code, transpile(code, {}, true).code); + CHECK_EQ(code, prettyPrint(code, {}, true).code); code = R"( type Foo = string )"; - CHECK_EQ(code, transpile(code, {}, true).code); + CHECK_EQ(code, prettyPrint(code, {}, true).code); code = R"( type Foo = string )"; - CHECK_EQ(code, transpile(code, {}, true).code); + CHECK_EQ(code, prettyPrint(code, {}, true).code); code = R"( type Foo = string )"; - CHECK_EQ(code, transpile(code, {}, true).code); + CHECK_EQ(code, prettyPrint(code, {}, true).code); code = R"( export type Foo = string )"; - CHECK_EQ(code, transpile(code, {}, true).code); + CHECK_EQ(code, prettyPrint(code, {}, true).code); code = R"( export type Foo = string )"; - CHECK_EQ(code, transpile(code, {}, true).code); + CHECK_EQ(code, prettyPrint(code, {}, true).code); code = R"( type Foo = string )"; - CHECK_EQ(code, transpile(code, {}, true).code); + CHECK_EQ(code, prettyPrint(code, {}, true).code); code = R"( type Foo = string )"; - CHECK_EQ(code, transpile(code, {}, true).code); + CHECK_EQ(code, prettyPrint(code, {}, true).code); code = R"( type Foo< X, Y, Z...> = string )"; - CHECK_EQ(code, transpile(code, {}, true).code); + CHECK_EQ(code, prettyPrint(code, {}, true).code); code = R"( type Foo = string )"; - CHECK_EQ(code, transpile(code, {}, true).code); + CHECK_EQ(code, prettyPrint(code, {}, true).code); code = R"( type Foo = string )"; - CHECK_EQ(code, transpile(code, {}, true).code); + CHECK_EQ(code, prettyPrint(code, {}, true).code); code = R"( type Foo = string )"; - CHECK_EQ(code, transpile(code, {}, true).code); + CHECK_EQ(code, prettyPrint(code, {}, true).code); code = R"( type Foo = string )"; - CHECK_EQ(code, transpile(code, {}, true).code); + CHECK_EQ(code, prettyPrint(code, {}, true).code); code = R"( type Foo = string )"; - CHECK_EQ(code, transpile(code, {}, true).code); + CHECK_EQ(code, prettyPrint(code, {}, true).code); code = R"( type Foo = string )"; - CHECK_EQ(code, transpile(code, {}, true).code); + CHECK_EQ(code, prettyPrint(code, {}, true).code); } TEST_CASE_FIXTURE(Fixture, "type_alias_with_defaults_spaces_around_tokens") { std::string code = R"( type Foo = string )"; - CHECK_EQ(code, transpile(code, {}, true).code); + CHECK_EQ(code, prettyPrint(code, {}, true).code); code = R"( type Foo = string )"; - CHECK_EQ(code, transpile(code, {}, true).code); + CHECK_EQ(code, prettyPrint(code, {}, true).code); code = R"( type Foo = string )"; - CHECK_EQ(code, transpile(code, {}, true).code); + CHECK_EQ(code, prettyPrint(code, {}, true).code); code = R"( type Foo = string )"; - CHECK_EQ(code, transpile(code, {}, true).code); + CHECK_EQ(code, prettyPrint(code, {}, true).code); code = R"( type Foo = string )"; - CHECK_EQ(code, transpile(code, {}, true).code); + CHECK_EQ(code, prettyPrint(code, {}, true).code); } TEST_CASE("table_literals") { const std::string code = R"( local t={1, 2, 3, foo='bar', baz=99,[5.5]='five point five', 'end'} )"; - CHECK_EQ(code, transpile(code).code); + CHECK_EQ(code, prettyPrint(code).code); } TEST_CASE("more_table_literals") { const std::string code = R"( local t={['Content-Type']='text/plain'} )"; - CHECK_EQ(code, transpile(code).code); + CHECK_EQ(code, prettyPrint(code).code); } TEST_CASE("table_literal_preserves_record_vs_general") { const std::string code = R"( local t={['foo']='bar',quux=42} )"; - CHECK_EQ(code, transpile(code).code); + CHECK_EQ(code, prettyPrint(code).code); } TEST_CASE("table_literal_with_numeric_key") { const std::string code = R"( local t={[5]='five',[6]='six'} )"; - CHECK_EQ(code, transpile(code).code); + CHECK_EQ(code, prettyPrint(code).code); } TEST_CASE("table_literal_with_keyword_key") { const std::string code = R"( local t={['nil']=nil,['true']=true} )"; - CHECK_EQ(code, transpile(code).code); + CHECK_EQ(code, prettyPrint(code).code); } TEST_CASE("table_literal_closing_brace_at_correct_position") @@ -483,7 +483,7 @@ TEST_CASE("table_literal_closing_brace_at_correct_position") } )"; - CHECK_EQ(code, transpile(code).code); + CHECK_EQ(code, prettyPrint(code).code); } TEST_CASE("table_literal_with_semicolon_separators") @@ -492,7 +492,7 @@ TEST_CASE("table_literal_with_semicolon_separators") local t = { x = 1; y = 2 } )"; - CHECK_EQ(code, transpile(code).code); + CHECK_EQ(code, prettyPrint(code).code); } TEST_CASE("table_literal_with_trailing_separators") @@ -501,7 +501,7 @@ TEST_CASE("table_literal_with_trailing_separators") local t = { x = 1, y = 2, } )"; - CHECK_EQ(code, transpile(code).code); + CHECK_EQ(code, prettyPrint(code).code); } TEST_CASE("table_literal_with_spaces_around_separator") @@ -510,7 +510,7 @@ TEST_CASE("table_literal_with_spaces_around_separator") local t = { x = 1 , y = 2 } )"; - CHECK_EQ(code, transpile(code).code); + CHECK_EQ(code, prettyPrint(code).code); } TEST_CASE("table_literal_with_spaces_around_equals") @@ -519,7 +519,7 @@ TEST_CASE("table_literal_with_spaces_around_equals") local t = { x = 1 } )"; - CHECK_EQ(code, transpile(code).code); + CHECK_EQ(code, prettyPrint(code).code); } TEST_CASE("table_literal_multiline_with_indexers") @@ -531,97 +531,97 @@ TEST_CASE("table_literal_multiline_with_indexers") } )"; - CHECK_EQ(code, transpile(code).code); + CHECK_EQ(code, prettyPrint(code).code); } TEST_CASE("method_calls") { const std::string code = R"( foo.bar.baz:quux() )"; - CHECK_EQ(code, transpile(code).code); + CHECK_EQ(code, prettyPrint(code).code); } TEST_CASE("method_definitions") { const std::string code = R"( function foo.bar.baz:quux() end )"; - CHECK_EQ(code, transpile(code).code); + CHECK_EQ(code, prettyPrint(code).code); } TEST_CASE("spaces_between_keywords_even_if_it_pushes_the_line_estimation_off") { const std::string code = R"( if math.abs(raySlope) < .01 then return 0 end )"; - CHECK_EQ(code, transpile(code).code); + CHECK_EQ(code, prettyPrint(code).code); } TEST_CASE("numbers") { const std::string code = R"( local a=2510238627 )"; - CHECK_EQ(code, transpile(code).code); + CHECK_EQ(code, prettyPrint(code).code); } TEST_CASE("infinity") { const std::string code = R"( local a = 1e500 local b = 1e400 )"; - CHECK_EQ(code, transpile(code).code); + CHECK_EQ(code, prettyPrint(code).code); } TEST_CASE("numbers_with_separators") { const std::string code = R"( local a = 123_456_789 )"; - CHECK_EQ(code, transpile(code).code); + CHECK_EQ(code, prettyPrint(code).code); } TEST_CASE("hexadecimal_numbers") { const std::string code = R"( local a = 0xFFFF )"; - CHECK_EQ(code, transpile(code).code); + CHECK_EQ(code, prettyPrint(code).code); } TEST_CASE("binary_numbers") { const std::string code = R"( local a = 0b0101 )"; - CHECK_EQ(code, transpile(code).code); + CHECK_EQ(code, prettyPrint(code).code); } TEST_CASE("single_quoted_strings") { const std::string code = R"( local a = 'hello world' )"; - CHECK_EQ(code, transpile(code).code); + CHECK_EQ(code, prettyPrint(code).code); } TEST_CASE("double_quoted_strings") { const std::string code = R"( local a = "hello world" )"; - CHECK_EQ(code, transpile(code).code); + CHECK_EQ(code, prettyPrint(code).code); } TEST_CASE("simple_interp_string") { const std::string code = R"( local a = `hello world` )"; - CHECK_EQ(code, transpile(code).code); + CHECK_EQ(code, prettyPrint(code).code); } TEST_CASE("raw_strings") { const std::string code = R"( local a = [[ hello world ]] )"; - CHECK_EQ(code, transpile(code).code); + CHECK_EQ(code, prettyPrint(code).code); } TEST_CASE("raw_strings_with_blocks") { const std::string code = R"( local a = [==[ hello world ]==] )"; - CHECK_EQ(code, transpile(code).code); + CHECK_EQ(code, prettyPrint(code).code); } TEST_CASE("escaped_strings") { const std::string code = R"( local s='\\b\\t\\n\\\\' )"; - CHECK_EQ(code, transpile(code).code); + CHECK_EQ(code, prettyPrint(code).code); } TEST_CASE("escaped_strings_2") { const std::string code = R"( local s="\a\b\f\n\r\t\v\'\"\\" )"; - CHECK_EQ(code, transpile(code).code); + CHECK_EQ(code, prettyPrint(code).code); } TEST_CASE("escaped_strings_newline") @@ -630,13 +630,13 @@ TEST_CASE("escaped_strings_newline") print("foo \ bar") )"; - CHECK_EQ(code, transpile(code).code); + CHECK_EQ(code, prettyPrint(code).code); } TEST_CASE("escaped_strings_raw") { const std::string code = R"( local x = [=[\v<((do|load)file|require)\s*\(?['"]\zs[^'"]+\ze['"]]=] )"; - CHECK_EQ(code, transpile(code).code); + CHECK_EQ(code, prettyPrint(code).code); } TEST_CASE("position_correctly_updated_when_writing_multiline_string") @@ -645,91 +645,91 @@ TEST_CASE("position_correctly_updated_when_writing_multiline_string") call([[ testing ]]) )"; - CHECK_EQ(code, transpile(code).code); + CHECK_EQ(code, prettyPrint(code).code); } TEST_CASE("need_a_space_between_number_literals_and_dots") { const std::string code = R"( return point and math.ceil(point* 100000* 100)/ 100000 .. '%'or '' )"; - CHECK_EQ(code, transpile(code).code); + CHECK_EQ(code, prettyPrint(code).code); } TEST_CASE("binary_keywords") { const std::string code = "local c = a0 ._ or b0 ._"; - CHECK_EQ(code, transpile(code).code); + CHECK_EQ(code, prettyPrint(code).code); } TEST_CASE("function_call_parentheses_no_args") { const std::string code = R"( call() )"; - CHECK_EQ(code, transpile(code).code); + CHECK_EQ(code, prettyPrint(code).code); } TEST_CASE("function_call_parentheses_one_arg") { const std::string code = R"( call(arg) )"; - CHECK_EQ(code, transpile(code).code); + CHECK_EQ(code, prettyPrint(code).code); } TEST_CASE("function_call_parentheses_multiple_args") { const std::string code = R"( call(arg1, arg3, arg3) )"; - CHECK_EQ(code, transpile(code).code); + CHECK_EQ(code, prettyPrint(code).code); } TEST_CASE("function_call_parentheses_multiple_args_no_space") { const std::string code = R"( call(arg1,arg3,arg3) )"; - CHECK_EQ(code, transpile(code).code); + CHECK_EQ(code, prettyPrint(code).code); } TEST_CASE("function_call_parentheses_multiple_args_space_before_commas") { const std::string code = R"( call(arg1 ,arg3 ,arg3) )"; - CHECK_EQ(code, transpile(code).code); + CHECK_EQ(code, prettyPrint(code).code); } TEST_CASE("function_call_spaces_before_parentheses") { const std::string code = R"( call () )"; - CHECK_EQ(code, transpile(code).code); + CHECK_EQ(code, prettyPrint(code).code); } TEST_CASE("function_call_spaces_within_parentheses") { const std::string code = R"( call( ) )"; - CHECK_EQ(code, transpile(code).code); + CHECK_EQ(code, prettyPrint(code).code); } TEST_CASE("function_call_string_double_quotes") { const std::string code = R"( call "string" )"; - CHECK_EQ(code, transpile(code).code); + CHECK_EQ(code, prettyPrint(code).code); } TEST_CASE("function_call_string_single_quotes") { const std::string code = R"( call 'string' )"; - CHECK_EQ(code, transpile(code).code); + CHECK_EQ(code, prettyPrint(code).code); } TEST_CASE("function_call_string_no_space") { const std::string code = R"( call'string' )"; - CHECK_EQ(code, transpile(code).code); + CHECK_EQ(code, prettyPrint(code).code); } TEST_CASE("function_call_table_literal") { const std::string code = R"( call { x = 1 } )"; - CHECK_EQ(code, transpile(code).code); + CHECK_EQ(code, prettyPrint(code).code); } TEST_CASE("function_call_table_literal_no_space") { const std::string code = R"( call{x=1} )"; - CHECK_EQ(code, transpile(code).code); + CHECK_EQ(code, prettyPrint(code).code); } TEST_CASE("do_blocks") @@ -745,7 +745,7 @@ TEST_CASE("do_blocks") foo2() )"; - CHECK_EQ(code, transpile(code).code); + CHECK_EQ(code, prettyPrint(code).code); } TEST_CASE("nested_do_block") @@ -758,7 +758,7 @@ TEST_CASE("nested_do_block") end )"; - CHECK_EQ(code, transpile(code).code); + CHECK_EQ(code, prettyPrint(code).code); } TEST_CASE("emit_a_do_block_in_cases_of_potentially_ambiguous_syntax") @@ -767,7 +767,7 @@ TEST_CASE("emit_a_do_block_in_cases_of_potentially_ambiguous_syntax") f(); (g or f)() )"; - CHECK_EQ(code, transpile(code).code); + CHECK_EQ(code, prettyPrint(code).code); } TEST_CASE_FIXTURE(Fixture, "parentheses_multiline") @@ -778,16 +778,16 @@ local test = ( ) )"; - CHECK_EQ(code, transpile(code, {}, true).code); + CHECK_EQ(code, prettyPrint(code, {}, true).code); } TEST_CASE_FIXTURE(Fixture, "stmt_semicolon") { std::string code = R"( local test = 1; )"; - CHECK_EQ(code, transpile(code, {}, true).code); + CHECK_EQ(code, prettyPrint(code, {}, true).code); code = R"( local test = 1 ; )"; - CHECK_EQ(code, transpile(code, {}, true).code); + CHECK_EQ(code, prettyPrint(code, {}, true).code); } TEST_CASE_FIXTURE(Fixture, "do_block_ending_with_semicolon") @@ -797,7 +797,7 @@ TEST_CASE_FIXTURE(Fixture, "do_block_ending_with_semicolon") return; end; )"; - CHECK_EQ(code, transpile(code, {}, true).code); + CHECK_EQ(code, prettyPrint(code, {}, true).code); } TEST_CASE_FIXTURE(Fixture, "if_stmt_semicolon") @@ -807,7 +807,7 @@ TEST_CASE_FIXTURE(Fixture, "if_stmt_semicolon") x = string.sub(x, utf8.offset(x, init)); end; )"; - CHECK_EQ(code, transpile(code, {}, true).code); + CHECK_EQ(code, prettyPrint(code, {}, true).code); } TEST_CASE_FIXTURE(Fixture, "if_stmt_semicolon_2") @@ -815,7 +815,7 @@ TEST_CASE_FIXTURE(Fixture, "if_stmt_semicolon_2") std::string code = R"( if (t < 1) then return c/2*t*t + b end; )"; - CHECK_EQ(code, transpile(code, {}, true).code); + CHECK_EQ(code, prettyPrint(code, {}, true).code); } TEST_CASE_FIXTURE(Fixture, "for_loop_stmt_semicolon") @@ -824,7 +824,7 @@ TEST_CASE_FIXTURE(Fixture, "for_loop_stmt_semicolon") for i,v in ... do end; )"; - CHECK_EQ(code, transpile(code, {}, true).code); + CHECK_EQ(code, prettyPrint(code, {}, true).code); } TEST_CASE_FIXTURE(Fixture, "while_do_semicolon") @@ -833,7 +833,7 @@ TEST_CASE_FIXTURE(Fixture, "while_do_semicolon") while true do end; )"; - CHECK_EQ(code, transpile(code, {}, true).code); + CHECK_EQ(code, prettyPrint(code, {}, true).code); } TEST_CASE_FIXTURE(Fixture, "function_definition_semicolon") @@ -842,7 +842,7 @@ TEST_CASE_FIXTURE(Fixture, "function_definition_semicolon") function foo() end; )"; - CHECK_EQ(code, transpile(code, {}, true).code); + CHECK_EQ(code, prettyPrint(code, {}, true).code); } TEST_CASE("roundtrip_types") @@ -864,7 +864,7 @@ TEST_CASE("roundtrip_types") ParseResult parseResult = Parser::parse(code.data(), code.size(), names, allocator, options); REQUIRE(parseResult.errors.empty()); - CHECK_EQ(code, transpileWithTypes(*parseResult.root)); + CHECK_EQ(code, prettyPrintWithTypes(*parseResult.root)); } TEST_CASE("roundtrip_generic_types") @@ -880,7 +880,7 @@ TEST_CASE("roundtrip_generic_types") ParseResult parseResult = Parser::parse(code.data(), code.size(), names, allocator, options); REQUIRE(parseResult.errors.empty()); - CHECK_EQ(code, transpileWithTypes(*parseResult.root)); + CHECK_EQ(code, prettyPrintWithTypes(*parseResult.root)); } TEST_CASE_FIXTURE(Fixture, "attach_types") @@ -907,15 +907,15 @@ TEST_CASE("a_table_key_can_be_the_empty_string") { std::string code = "local T = {[''] = true}"; - CHECK_EQ(code, transpile(code).code); + CHECK_EQ(code, prettyPrint(code).code); } -// There's a bit of login in the transpiler that always adds a space before a dot if the previous symbol ends in a digit. +// There's a bit of login in the prettyPrintr that always adds a space before a dot if the previous symbol ends in a digit. // This was surfacing an issue where we might not insert a space after the 'local' keyword. TEST_CASE("always_emit_a_space_after_local_keyword") { std::string code = "do local aZZZZ = Workspace.P1.Shape local bZZZZ = Enum.PartType.Cylinder end"; - CHECK_EQ(code, transpile(code).code); + CHECK_EQ(code, prettyPrint(code).code); } TEST_CASE_FIXTURE(Fixture, "types_should_not_be_considered_cyclic_if_they_are_not_recursive") @@ -989,37 +989,37 @@ TEST_CASE_FIXTURE(Fixture, "function_type_location") CHECK_EQ(expected, actual); } -TEST_CASE_FIXTURE(Fixture, "transpile_type_assertion") +TEST_CASE_FIXTURE(Fixture, "prettyPrint_type_assertion") { std::string code = "local a = 5 :: number"; - CHECK_EQ(code, transpile(code, {}, true).code); + CHECK_EQ(code, prettyPrint(code, {}, true).code); } TEST_CASE_FIXTURE(Fixture, "type_assertion_spaces_around_tokens") { std::string code = "local a = 5 :: number"; - CHECK_EQ(code, transpile(code, {}, true).code); + CHECK_EQ(code, prettyPrint(code, {}, true).code); code = "local a = 5 :: number"; - CHECK_EQ(code, transpile(code, {}, true).code); + CHECK_EQ(code, prettyPrint(code, {}, true).code); } -TEST_CASE_FIXTURE(Fixture, "transpile_if_then_else") +TEST_CASE_FIXTURE(Fixture, "prettyPrint_if_then_else") { std::string code = "local a = if 1 then 2 else 3"; - CHECK_EQ(code, transpile(code).code); + CHECK_EQ(code, prettyPrint(code).code); } -TEST_CASE_FIXTURE(Fixture, "transpile_if_then_else_multiple_conditions") +TEST_CASE_FIXTURE(Fixture, "prettyPrint_if_then_else_multiple_conditions") { std::string code = "local a = if 1 then 2 elseif 3 then 4 else 5"; - CHECK_EQ(code, transpile(code).code); + CHECK_EQ(code, prettyPrint(code).code); } -TEST_CASE_FIXTURE(Fixture, "transpile_if_then_else_multiple_conditions_2") +TEST_CASE_FIXTURE(Fixture, "prettyPrint_if_then_else_multiple_conditions_2") { std::string code = R"( local x = if yes @@ -1031,43 +1031,43 @@ TEST_CASE_FIXTURE(Fixture, "transpile_if_then_else_multiple_conditions_2") else nil )"; - CHECK_EQ(code, transpile(code).code); + CHECK_EQ(code, prettyPrint(code).code); } TEST_CASE_FIXTURE(Fixture, "if_then_else_spaces_around_tokens") { std::string code = "local a = if 1 then 2 else 3"; - CHECK_EQ(code, transpile(code).code); + CHECK_EQ(code, prettyPrint(code).code); code = "local a = if 1 then 2 else 3"; - CHECK_EQ(code, transpile(code).code); + CHECK_EQ(code, prettyPrint(code).code); code = "local a = if 1 then 2 else 3"; - CHECK_EQ(code, transpile(code).code); + CHECK_EQ(code, prettyPrint(code).code); code = "local a = if 1 then 2 else 3"; - CHECK_EQ(code, transpile(code).code); + CHECK_EQ(code, prettyPrint(code).code); code = "local a = if 1 then 2 else 3"; - CHECK_EQ(code, transpile(code).code); + CHECK_EQ(code, prettyPrint(code).code); code = "local a = if 1 then 2 elseif 3 then 4 else 5"; - CHECK_EQ(code, transpile(code).code); + CHECK_EQ(code, prettyPrint(code).code); code = "local a = if 1 then 2 elseif 3 then 4 else 5"; - CHECK_EQ(code, transpile(code).code); + CHECK_EQ(code, prettyPrint(code).code); code = "local a = if 1 then 2 elseif 3 then 4 else 5"; - CHECK_EQ(code, transpile(code).code); + CHECK_EQ(code, prettyPrint(code).code); code = "local a = if 1 then 2 elseif 3 then 4 else 5"; - CHECK_EQ(code, transpile(code).code); + CHECK_EQ(code, prettyPrint(code).code); code = "local a = if 1 then 2 elseif 3 then 4 else 5"; - CHECK_EQ(code, transpile(code).code); + CHECK_EQ(code, prettyPrint(code).code); code = "local a = if 1 then 2 elseif 3 then 4 else 5"; - CHECK_EQ(code, transpile(code).code); + CHECK_EQ(code, prettyPrint(code).code); } TEST_CASE_FIXTURE(Fixture, "if_then_else_spaces_between_else_if") @@ -1079,10 +1079,10 @@ TEST_CASE_FIXTURE(Fixture, "if_then_else_spaces_between_else_if") if c then "was c" else "was nothing!" )"; - CHECK_EQ(code, transpile(code).code); + CHECK_EQ(code, prettyPrint(code).code); } -TEST_CASE_FIXTURE(Fixture, "transpile_type_reference_import") +TEST_CASE_FIXTURE(Fixture, "prettyPrint_type_reference_import") { fileResolver.source["game/A"] = R"( export type Type = { a: number } @@ -1094,82 +1094,82 @@ local Import = require(game.A) local a: Import.Type )"; - CHECK_EQ(code, transpile(code, {}, true).code); + CHECK_EQ(code, prettyPrint(code, {}, true).code); } -TEST_CASE_FIXTURE(Fixture, "transpile_type_reference_spaces_around_tokens") +TEST_CASE_FIXTURE(Fixture, "prettyPrint_type_reference_spaces_around_tokens") { std::string code = R"( local _: Foo.Type )"; - CHECK_EQ(code, transpile(code, {}, true).code); + CHECK_EQ(code, prettyPrint(code, {}, true).code); code = R"( local _: Foo .Type )"; - CHECK_EQ(code, transpile(code, {}, true).code); + CHECK_EQ(code, prettyPrint(code, {}, true).code); code = R"( local _: Foo. Type )"; - CHECK_EQ(code, transpile(code, {}, true).code); + CHECK_EQ(code, prettyPrint(code, {}, true).code); code = R"( local _: Type <> )"; - CHECK_EQ(code, transpile(code, {}, true).code); + CHECK_EQ(code, prettyPrint(code, {}, true).code); code = R"( local _: Type< > )"; - CHECK_EQ(code, transpile(code, {}, true).code); + CHECK_EQ(code, prettyPrint(code, {}, true).code); code = R"( local _: Type< number> )"; - CHECK_EQ(code, transpile(code, {}, true).code); + CHECK_EQ(code, prettyPrint(code, {}, true).code); code = R"( local _: Type )"; - CHECK_EQ(code, transpile(code, {}, true).code); + CHECK_EQ(code, prettyPrint(code, {}, true).code); code = R"( local _: Type )"; - CHECK_EQ(code, transpile(code, {}, true).code); + CHECK_EQ(code, prettyPrint(code, {}, true).code); } -TEST_CASE_FIXTURE(Fixture, "transpile_type_annotation_spaces_around_tokens") +TEST_CASE_FIXTURE(Fixture, "prettyPrint_type_annotation_spaces_around_tokens") { std::string code = R"( local _: Type )"; - CHECK_EQ(code, transpile(code, {}, true).code); + CHECK_EQ(code, prettyPrint(code, {}, true).code); code = R"( local _ : Type )"; - CHECK_EQ(code, transpile(code, {}, true).code); + CHECK_EQ(code, prettyPrint(code, {}, true).code); code = R"( local _: Type )"; - CHECK_EQ(code, transpile(code, {}, true).code); + CHECK_EQ(code, prettyPrint(code, {}, true).code); code = R"( local x: Type, y = 1 )"; - CHECK_EQ(code, transpile(code, {}, true).code); + CHECK_EQ(code, prettyPrint(code, {}, true).code); code = R"( local x : Type, y = 1 )"; - CHECK_EQ(code, transpile(code, {}, true).code); + CHECK_EQ(code, prettyPrint(code, {}, true).code); } -TEST_CASE_FIXTURE(Fixture, "transpile_for_loop_annotation_spaces_around_tokens") +TEST_CASE_FIXTURE(Fixture, "prettyPrint_for_loop_annotation_spaces_around_tokens") { std::string code = R"( for i: number = 1, 10 do end )"; - CHECK_EQ(code, transpile(code, {}, true).code); + CHECK_EQ(code, prettyPrint(code, {}, true).code); code = R"( for i : number = 1, 10 do end )"; - CHECK_EQ(code, transpile(code, {}, true).code); + CHECK_EQ(code, prettyPrint(code, {}, true).code); code = R"( for i: number = 1, 10 do end )"; - CHECK_EQ(code, transpile(code, {}, true).code); + CHECK_EQ(code, prettyPrint(code, {}, true).code); code = R"( for x: number, y: number in ... do end )"; - CHECK_EQ(code, transpile(code, {}, true).code); + CHECK_EQ(code, prettyPrint(code, {}, true).code); code = R"( for x : number, y: number in ... do end )"; - CHECK_EQ(code, transpile(code, {}, true).code); + CHECK_EQ(code, prettyPrint(code, {}, true).code); code = R"( for x: number, y: number in ... do end )"; - CHECK_EQ(code, transpile(code, {}, true).code); + CHECK_EQ(code, prettyPrint(code, {}, true).code); code = R"( for x: number, y : number in ... do end )"; - CHECK_EQ(code, transpile(code, {}, true).code); + CHECK_EQ(code, prettyPrint(code, {}, true).code); code = R"( for x: number, y: number in ... do end )"; - CHECK_EQ(code, transpile(code, {}, true).code); + CHECK_EQ(code, prettyPrint(code, {}, true).code); } -TEST_CASE_FIXTURE(Fixture, "transpile_type_packs") +TEST_CASE_FIXTURE(Fixture, "prettyPrint_type_packs") { std::string code = R"( type Packed = (T...)->(T...) @@ -1177,224 +1177,224 @@ local a: Packed<> local b: Packed<(number, string)> )"; - CHECK_EQ(code, transpile(code, {}, true).code); + CHECK_EQ(code, prettyPrint(code, {}, true).code); } TEST_CASE_FIXTURE(Fixture, "type_packs_spaces_around_tokens") { std::string code = R"( type _ = Packed< T...> )"; - CHECK_EQ(code, transpile(code, {}, true).code); + CHECK_EQ(code, prettyPrint(code, {}, true).code); code = R"( type _ = Packed )"; - CHECK_EQ(code, transpile(code, {}, true).code); + CHECK_EQ(code, prettyPrint(code, {}, true).code); code = R"( type _ = Packed< ...T> )"; - CHECK_EQ(code, transpile(code, {}, true).code); + CHECK_EQ(code, prettyPrint(code, {}, true).code); code = R"( type _ = Packed<... T> )"; - CHECK_EQ(code, transpile(code, {}, true).code); + CHECK_EQ(code, prettyPrint(code, {}, true).code); code = R"( type _ = Packed< ()> )"; - CHECK_EQ(code, transpile(code, {}, true).code); + CHECK_EQ(code, prettyPrint(code, {}, true).code); code = R"( type _ = Packed< (string, number)> )"; - CHECK_EQ(code, transpile(code, {}, true).code); + CHECK_EQ(code, prettyPrint(code, {}, true).code); code = R"( type _ = Packed<( string, number)> )"; - CHECK_EQ(code, transpile(code, {}, true).code); + CHECK_EQ(code, prettyPrint(code, {}, true).code); code = R"( type _ = Packed<(string , number)> )"; - CHECK_EQ(code, transpile(code, {}, true).code); + CHECK_EQ(code, prettyPrint(code, {}, true).code); code = R"( type _ = Packed<(string, number)> )"; - CHECK_EQ(code, transpile(code, {}, true).code); + CHECK_EQ(code, prettyPrint(code, {}, true).code); code = R"( type _ = Packed<(string, number )> )"; - CHECK_EQ(code, transpile(code, {}, true).code); + CHECK_EQ(code, prettyPrint(code, {}, true).code); code = R"( type _ = Packed<(string, number) > )"; - CHECK_EQ(code, transpile(code, {}, true).code); + CHECK_EQ(code, prettyPrint(code, {}, true).code); code = R"( type _ = Packed<( )> )"; - CHECK_EQ(code, transpile(code, {}, true).code); + CHECK_EQ(code, prettyPrint(code, {}, true).code); code = R"( type _ = Packed<() > )"; - CHECK_EQ(code, transpile(code, {}, true).code); + CHECK_EQ(code, prettyPrint(code, {}, true).code); } -TEST_CASE_FIXTURE(Fixture, "transpile_union_type_nested") +TEST_CASE_FIXTURE(Fixture, "prettyPrint_union_type_nested") { std::string code = "local a: ((number)->(string))|((string)->(string))"; - CHECK_EQ(code, transpile(code, {}, true).code); + CHECK_EQ(code, prettyPrint(code, {}, true).code); } -TEST_CASE_FIXTURE(Fixture, "transpile_union_type_nested_2") +TEST_CASE_FIXTURE(Fixture, "prettyPrint_union_type_nested_2") { std::string code = "local a: (number&string)|(string&boolean)"; - CHECK_EQ(code, transpile(code, {}, true).code); + CHECK_EQ(code, prettyPrint(code, {}, true).code); } -TEST_CASE_FIXTURE(Fixture, "transpile_union_type_nested_3") +TEST_CASE_FIXTURE(Fixture, "prettyPrint_union_type_nested_3") { std::string code = "local a: nil | (string & number)"; - CHECK_EQ(code, transpile(code, {}, true).code); + CHECK_EQ(code, prettyPrint(code, {}, true).code); } -TEST_CASE_FIXTURE(Fixture, "transpile_intersection_type_nested") +TEST_CASE_FIXTURE(Fixture, "prettyPrint_intersection_type_nested") { std::string code = "local a: ((number)->(string))&((string)->(string))"; - CHECK_EQ(code, transpile(code, {}, true).code); + CHECK_EQ(code, prettyPrint(code, {}, true).code); } -TEST_CASE_FIXTURE(Fixture, "transpile_intersection_type_nested_2") +TEST_CASE_FIXTURE(Fixture, "prettyPrint_intersection_type_nested_2") { std::string code = "local a: (number|string)&(string|boolean)"; - CHECK_EQ(code, transpile(code, {}, true).code); + CHECK_EQ(code, prettyPrint(code, {}, true).code); } -TEST_CASE_FIXTURE(Fixture, "transpile_intersection_type_with_function") +TEST_CASE_FIXTURE(Fixture, "prettyPrint_intersection_type_with_function") { std::string code = "type FnB = () -> U... & T"; - CHECK_EQ(code, transpile(code, {}, true).code); + CHECK_EQ(code, prettyPrint(code, {}, true).code); } -TEST_CASE_FIXTURE(Fixture, "transpile_leading_union_pipe") +TEST_CASE_FIXTURE(Fixture, "prettyPrint_leading_union_pipe") { std::string code = "local a: | string | number"; - CHECK_EQ(code, transpile(code, {}, true).code); + CHECK_EQ(code, prettyPrint(code, {}, true).code); code = "local a: | string"; - CHECK_EQ(code, transpile(code, {}, true).code); + CHECK_EQ(code, prettyPrint(code, {}, true).code); } -TEST_CASE_FIXTURE(Fixture, "transpile_union_spaces_around_tokens") +TEST_CASE_FIXTURE(Fixture, "prettyPrint_union_spaces_around_tokens") { std::string code = "local a: string | number"; - CHECK_EQ(code, transpile(code, {}, true).code); + CHECK_EQ(code, prettyPrint(code, {}, true).code); code = "local a: string | number"; - CHECK_EQ(code, transpile(code, {}, true).code); + CHECK_EQ(code, prettyPrint(code, {}, true).code); } -TEST_CASE_FIXTURE(Fixture, "transpile_leading_intersection_ampersand") +TEST_CASE_FIXTURE(Fixture, "prettyPrint_leading_intersection_ampersand") { std::string code = "local a: & string & number"; - CHECK_EQ(code, transpile(code, {}, true).code); + CHECK_EQ(code, prettyPrint(code, {}, true).code); code = "local a: & string"; - CHECK_EQ(code, transpile(code, {}, true).code); + CHECK_EQ(code, prettyPrint(code, {}, true).code); } -TEST_CASE_FIXTURE(Fixture, "transpile_intersection_spaces_around_tokens") +TEST_CASE_FIXTURE(Fixture, "prettyPrint_intersection_spaces_around_tokens") { std::string code = "local a: string & number"; - CHECK_EQ(code, transpile(code, {}, true).code); + CHECK_EQ(code, prettyPrint(code, {}, true).code); code = "local a: string & number"; - CHECK_EQ(code, transpile(code, {}, true).code); + CHECK_EQ(code, prettyPrint(code, {}, true).code); } -TEST_CASE_FIXTURE(Fixture, "transpile_mixed_union_intersection") +TEST_CASE_FIXTURE(Fixture, "prettyPrint_mixed_union_intersection") { std::string code = "local a: string | (Foo & Bar)"; - CHECK_EQ(code, transpile(code, {}, true).code); + CHECK_EQ(code, prettyPrint(code, {}, true).code); code = "local a: string | (Foo & Bar)"; - CHECK_EQ(code, transpile(code, {}, true).code); + CHECK_EQ(code, prettyPrint(code, {}, true).code); code = "local a: string | ( Foo & Bar)"; - CHECK_EQ(code, transpile(code, {}, true).code); + CHECK_EQ(code, prettyPrint(code, {}, true).code); code = "local a: string | (Foo & Bar )"; - CHECK_EQ(code, transpile(code, {}, true).code); + CHECK_EQ(code, prettyPrint(code, {}, true).code); code = "local a: string & (Foo | Bar)"; - CHECK_EQ(code, transpile(code, {}, true).code); + CHECK_EQ(code, prettyPrint(code, {}, true).code); code = "local a: string & ( Foo | Bar)"; - CHECK_EQ(code, transpile(code, {}, true).code); + CHECK_EQ(code, prettyPrint(code, {}, true).code); code = "local a: string & (Foo | Bar )"; - CHECK_EQ(code, transpile(code, {}, true).code); + CHECK_EQ(code, prettyPrint(code, {}, true).code); } -TEST_CASE_FIXTURE(Fixture, "transpile_preserve_union_optional_style") +TEST_CASE_FIXTURE(Fixture, "prettyPrint_preserve_union_optional_style") { std::string code = "local a: string | nil"; - CHECK_EQ(code, transpile(code, {}, true).code); + CHECK_EQ(code, prettyPrint(code, {}, true).code); code = "local a: string?"; - CHECK_EQ(code, transpile(code, {}, true).code); + CHECK_EQ(code, prettyPrint(code, {}, true).code); code = "local a: string???"; - CHECK_EQ(code, transpile(code, {}, true).code); + CHECK_EQ(code, prettyPrint(code, {}, true).code); code = "local a: string? | nil"; - CHECK_EQ(code, transpile(code, {}, true).code); + CHECK_EQ(code, prettyPrint(code, {}, true).code); code = "local a: string | nil | number"; - CHECK_EQ(code, transpile(code, {}, true).code); + CHECK_EQ(code, prettyPrint(code, {}, true).code); code = "local a: string | nil | number?"; - CHECK_EQ(code, transpile(code, {}, true).code); + CHECK_EQ(code, prettyPrint(code, {}, true).code); code = "local a: string? | number?"; - CHECK_EQ(code, transpile(code, {}, true).code); + CHECK_EQ(code, prettyPrint(code, {}, true).code); } -TEST_CASE_FIXTURE(Fixture, "transpile_varargs") +TEST_CASE_FIXTURE(Fixture, "prettyPrint_varargs") { std::string code = "local function f(...) return ... end"; - CHECK_EQ(code, transpile(code, {}, true).code); + CHECK_EQ(code, prettyPrint(code, {}, true).code); } TEST_CASE_FIXTURE(Fixture, "index_name_spaces_around_tokens") { std::string one = "local _ = a.name"; - CHECK_EQ(one, transpile(one, {}, true).code); + CHECK_EQ(one, prettyPrint(one, {}, true).code); std::string two = "local _ = a .name"; - CHECK_EQ(two, transpile(two, {}, true).code); + CHECK_EQ(two, prettyPrint(two, {}, true).code); std::string three = "local _ = a. name"; - CHECK_EQ(three, transpile(three, {}, true).code); + CHECK_EQ(three, prettyPrint(three, {}, true).code); } TEST_CASE_FIXTURE(Fixture, "index_name_ends_with_digit") { std::string code = "sparkles.Color = Color3.new()"; - CHECK_EQ(code, transpile(code, {}, true).code); + CHECK_EQ(code, prettyPrint(code, {}, true).code); } -TEST_CASE_FIXTURE(Fixture, "transpile_index_expr") +TEST_CASE_FIXTURE(Fixture, "prettyPrint_index_expr") { std::string code = "local a = {1, 2, 3} local b = a[2]"; - CHECK_EQ(code, transpile(code, {}, true).code); + CHECK_EQ(code, prettyPrint(code, {}, true).code); } TEST_CASE_FIXTURE(Fixture, "index_expr_spaces_around_tokens") { std::string one = "local _ = a[2]"; - CHECK_EQ(one, transpile(one, {}, true).code); + CHECK_EQ(one, prettyPrint(one, {}, true).code); std::string two = "local _ = a [2]"; - CHECK_EQ(two, transpile(two, {}, true).code); + CHECK_EQ(two, prettyPrint(two, {}, true).code); std::string three = "local _ = a[ 2]"; - CHECK_EQ(three, transpile(three, {}, true).code); + CHECK_EQ(three, prettyPrint(three, {}, true).code); std::string four = "local _ = a[2 ]"; - CHECK_EQ(four, transpile(four, {}, true).code); + CHECK_EQ(four, prettyPrint(four, {}, true).code); } -TEST_CASE_FIXTURE(Fixture, "transpile_unary") +TEST_CASE_FIXTURE(Fixture, "prettyPrint_unary") { std::string code = R"( local a = 1 @@ -1405,7 +1405,7 @@ local e = 'hello' local d = #e )"; - CHECK_EQ(code, transpile(code, {}, true).code); + CHECK_EQ(code, prettyPrint(code, {}, true).code); } TEST_CASE_FIXTURE(Fixture, "unary_spaces_around_tokens") @@ -1419,7 +1419,7 @@ local _ = #e local _ = # e )"; - CHECK_EQ(code, transpile(code, {}, true).code); + CHECK_EQ(code, prettyPrint(code, {}, true).code); } TEST_CASE_FIXTURE(Fixture, "binary_spaces_around_tokens") @@ -1430,10 +1430,10 @@ local _ = 1 +1 local _ = 1+ 1 )"; - CHECK_EQ(code, transpile(code, {}, true).code); + CHECK_EQ(code, prettyPrint(code, {}, true).code); } -TEST_CASE_FIXTURE(Fixture, "transpile_break_continue") +TEST_CASE_FIXTURE(Fixture, "prettyPrint_break_continue") { std::string code = R"( local a, b, c @@ -1443,10 +1443,10 @@ repeat until c )"; - CHECK_EQ(code, transpile(code, {}, true).code); + CHECK_EQ(code, prettyPrint(code, {}, true).code); } -TEST_CASE_FIXTURE(Fixture, "transpile_compound_assignment") +TEST_CASE_FIXTURE(Fixture, "prettyPrint_compound_assignment") { std::string code = R"( local a = 1 @@ -1460,73 +1460,73 @@ a ^= 7 a ..= ' - result' )"; - CHECK_EQ(code, transpile(code, {}, true).code); + CHECK_EQ(code, prettyPrint(code, {}, true).code); } TEST_CASE_FIXTURE(Fixture, "compound_assignment_spaces_around_tokens") { std::string one = R"( a += 1 )"; - CHECK_EQ(one, transpile(one, {}, true).code); + CHECK_EQ(one, prettyPrint(one, {}, true).code); std::string two = R"( a += 1 )"; - CHECK_EQ(two, transpile(two, {}, true).code); + CHECK_EQ(two, prettyPrint(two, {}, true).code); } -TEST_CASE_FIXTURE(Fixture, "transpile_assign_multiple") +TEST_CASE_FIXTURE(Fixture, "prettyPrint_assign_multiple") { std::string code = "a, b, c = 1, 2, 3"; - CHECK_EQ(code, transpile(code, {}, true).code); + CHECK_EQ(code, prettyPrint(code, {}, true).code); } -TEST_CASE_FIXTURE(Fixture, "transpile_assign_spaces_around_tokens") +TEST_CASE_FIXTURE(Fixture, "prettyPrint_assign_spaces_around_tokens") { std::string one = "a = 1"; - CHECK_EQ(one, transpile(one).code); + CHECK_EQ(one, prettyPrint(one).code); std::string two = "a = 1"; - CHECK_EQ(two, transpile(two).code); + CHECK_EQ(two, prettyPrint(two).code); std::string three = "a = 1"; - CHECK_EQ(three, transpile(three).code); + CHECK_EQ(three, prettyPrint(three).code); std::string four = "a , b = 1, 2"; - CHECK_EQ(four, transpile(four).code); + CHECK_EQ(four, prettyPrint(four).code); std::string five = "a, b = 1, 2"; - CHECK_EQ(five, transpile(five).code); + CHECK_EQ(five, prettyPrint(five).code); std::string six = "a, b = 1 , 2"; - CHECK_EQ(six, transpile(six).code); + CHECK_EQ(six, prettyPrint(six).code); std::string seven = "a, b = 1, 2"; - CHECK_EQ(seven, transpile(seven).code); + CHECK_EQ(seven, prettyPrint(seven).code); } -TEST_CASE_FIXTURE(Fixture, "transpile_generic_function") +TEST_CASE_FIXTURE(Fixture, "prettyPrint_generic_function") { std::string code = R"( local function foo(a: T, ...: S...) return 1 end local f: (T, S...)->(number) = foo )"; - CHECK_EQ(code, transpile(code, {}, true).code); + CHECK_EQ(code, prettyPrint(code, {}, true).code); } -TEST_CASE_FIXTURE(Fixture, "transpile_union_reverse") +TEST_CASE_FIXTURE(Fixture, "prettyPrint_union_reverse") { std::string code = "local a: nil | number"; - CHECK_EQ(code, transpile(code, {}, true).code); + CHECK_EQ(code, prettyPrint(code, {}, true).code); } -TEST_CASE_FIXTURE(Fixture, "transpile_for_in_multiple") +TEST_CASE_FIXTURE(Fixture, "prettyPrint_for_in_multiple") { std::string code = "for k,v in next,{}do print(k,v) end"; - CHECK_EQ(code, transpile(code, {}, true).code); + CHECK_EQ(code, prettyPrint(code, {}, true).code); } -TEST_CASE_FIXTURE(Fixture, "transpile_error_expr") +TEST_CASE_FIXTURE(Fixture, "prettyPrint_error_expr") { std::string code = "local a = f:-"; @@ -1534,10 +1534,10 @@ TEST_CASE_FIXTURE(Fixture, "transpile_error_expr") auto names = AstNameTable{allocator}; ParseResult parseResult = Parser::parse(code.data(), code.size(), names, allocator, {}); - CHECK_EQ("local a = (error-expr: f:%error-id%)-(error-expr)", transpileWithTypes(*parseResult.root)); + CHECK_EQ("local a = (error-expr: f:%error-id%)-(error-expr)", prettyPrintWithTypes(*parseResult.root)); } -TEST_CASE_FIXTURE(Fixture, "transpile_error_stat") +TEST_CASE_FIXTURE(Fixture, "prettyPrint_error_stat") { std::string code = "-"; @@ -1545,10 +1545,10 @@ TEST_CASE_FIXTURE(Fixture, "transpile_error_stat") auto names = AstNameTable{allocator}; ParseResult parseResult = Parser::parse(code.data(), code.size(), names, allocator, {}); - CHECK_EQ("(error-stat: (error-expr))", transpileWithTypes(*parseResult.root)); + CHECK_EQ("(error-stat: (error-expr))", prettyPrintWithTypes(*parseResult.root)); } -TEST_CASE_FIXTURE(Fixture, "transpile_error_type") +TEST_CASE_FIXTURE(Fixture, "prettyPrint_error_type") { std::string code = "local a: "; @@ -1556,19 +1556,19 @@ TEST_CASE_FIXTURE(Fixture, "transpile_error_type") auto names = AstNameTable{allocator}; ParseResult parseResult = Parser::parse(code.data(), code.size(), names, allocator, {}); - CHECK_EQ("local a:%error-type%", transpileWithTypes(*parseResult.root)); + CHECK_EQ("local a:%error-type%", prettyPrintWithTypes(*parseResult.root)); } -TEST_CASE_FIXTURE(Fixture, "transpile_parse_error") +TEST_CASE_FIXTURE(Fixture, "prettyPrint_parse_error") { std::string code = "local a = -"; - auto result = transpile(code); + auto result = prettyPrint(code); CHECK_EQ("", result.code); CHECK_EQ("Expected identifier when parsing expression, got ", result.parseError); } -TEST_CASE_FIXTURE(Fixture, "transpile_declare_global_stat") +TEST_CASE_FIXTURE(Fixture, "prettyPrint_declare_global_stat") { std::string code = "declare _G: any"; @@ -1579,12 +1579,12 @@ TEST_CASE_FIXTURE(Fixture, "transpile_declare_global_stat") auto names = AstNameTable{allocator}; ParseResult parseResult = Parser::parse(code.data(), code.size(), names, allocator, options); - auto result = transpileWithTypes(*parseResult.root); + auto result = prettyPrintWithTypes(*parseResult.root); CHECK_EQ(result, code); } -TEST_CASE_FIXTURE(Fixture, "transpile_to_string") +TEST_CASE_FIXTURE(Fixture, "prettyPrint_to_string") { std::string code = "local a: string = 'hello'"; @@ -1606,17 +1606,17 @@ TEST_CASE_FIXTURE(Fixture, "transpile_to_string") CHECK_EQ("'hello'", toString(expr)); } -TEST_CASE_FIXTURE(Fixture, "transpile_type_alias_default_type_parameters") +TEST_CASE_FIXTURE(Fixture, "prettyPrint_type_alias_default_type_parameters") { std::string code = R"( type Packed = (T, U, V...)->(W...) local a: Packed )"; - CHECK_EQ(code, transpile(code, {}, true).code); + CHECK_EQ(code, prettyPrint(code, {}, true).code); } -TEST_CASE_FIXTURE(Fixture, "transpile_singleton_types") +TEST_CASE_FIXTURE(Fixture, "prettyPrint_singleton_types") { std::string code = R"( type t1 = 'hello' @@ -1625,43 +1625,43 @@ type t3 = '' type t4 = false )"; - CHECK_EQ(code, transpile(code, {}, true).code); + CHECK_EQ(code, prettyPrint(code, {}, true).code); } -TEST_CASE_FIXTURE(Fixture, "transpile_array_types") +TEST_CASE_FIXTURE(Fixture, "prettyPrint_array_types") { std::string code = R"( type t1 = {number} type t2 = {[string]: number} )"; - CHECK_EQ(code, transpile(code, {}, true).code); + CHECK_EQ(code, prettyPrint(code, {}, true).code); } -TEST_CASE_FIXTURE(Fixture, "transpile_for_in_multiple_types") +TEST_CASE_FIXTURE(Fixture, "prettyPrint_for_in_multiple_types") { std::string code = "for k:string,v:boolean in next,{}do end"; - CHECK_EQ(code, transpile(code, {}, true).code); + CHECK_EQ(code, prettyPrint(code, {}, true).code); } -TEST_CASE_FIXTURE(Fixture, "transpile_string_interp") +TEST_CASE_FIXTURE(Fixture, "prettyPrint_string_interp") { std::string code = R"( local _ = `hello {name}` )"; - CHECK_EQ(code, transpile(code, {}, true).code); + CHECK_EQ(code, prettyPrint(code, {}, true).code); } -TEST_CASE_FIXTURE(Fixture, "transpile_string_interp_multiline") +TEST_CASE_FIXTURE(Fixture, "prettyPrint_string_interp_multiline") { std::string code = R"( local _ = `hello { name }!` )"; - CHECK_EQ(code, transpile(code, {}, true).code); + CHECK_EQ(code, prettyPrint(code, {}, true).code); } -TEST_CASE_FIXTURE(Fixture, "transpile_string_interp_on_new_line") +TEST_CASE_FIXTURE(Fixture, "prettyPrint_string_interp_on_new_line") { std::string code = R"( error( @@ -1669,92 +1669,92 @@ TEST_CASE_FIXTURE(Fixture, "transpile_string_interp_on_new_line") ) )"; - CHECK_EQ(code, transpile(code, {}, true).code); + CHECK_EQ(code, prettyPrint(code, {}, true).code); } -TEST_CASE_FIXTURE(Fixture, "transpile_string_interp_multiline_escape") +TEST_CASE_FIXTURE(Fixture, "prettyPrint_string_interp_multiline_escape") { std::string code = R"( local _ = `hello \ world!` )"; - CHECK_EQ(code, transpile(code, {}, true).code); + CHECK_EQ(code, prettyPrint(code, {}, true).code); } -TEST_CASE_FIXTURE(Fixture, "transpile_string_literal_escape") +TEST_CASE_FIXTURE(Fixture, "prettyPrint_string_literal_escape") { std::string code = R"( local _ = ` bracket = \{, backtick = \` = {'ok'} ` )"; - CHECK_EQ(code, transpile(code, {}, true).code); + CHECK_EQ(code, prettyPrint(code, {}, true).code); } -TEST_CASE_FIXTURE(Fixture, "transpile_type_functions") +TEST_CASE_FIXTURE(Fixture, "prettyPrint_type_functions") { std::string code = R"( type function foo(arg1, arg2) if arg1 == arg2 then return arg1 end return arg2 end )"; - CHECK_EQ(code, transpile(code, {}, true).code); + CHECK_EQ(code, prettyPrint(code, {}, true).code); } -TEST_CASE_FIXTURE(Fixture, "transpile_type_functions_spaces_around_tokens") +TEST_CASE_FIXTURE(Fixture, "prettyPrint_type_functions_spaces_around_tokens") { std::string code = R"( type function foo() end )"; - CHECK_EQ(code, transpile(code, {}, true).code); + CHECK_EQ(code, prettyPrint(code, {}, true).code); code = R"( type function foo() end )"; - CHECK_EQ(code, transpile(code, {}, true).code); + CHECK_EQ(code, prettyPrint(code, {}, true).code); code = R"( type function foo () end )"; - CHECK_EQ(code, transpile(code, {}, true).code); + CHECK_EQ(code, prettyPrint(code, {}, true).code); code = R"( export type function foo() end )"; - CHECK_EQ(code, transpile(code, {}, true).code); + CHECK_EQ(code, prettyPrint(code, {}, true).code); } -TEST_CASE_FIXTURE(Fixture, "transpile_typeof_spaces_around_tokens") +TEST_CASE_FIXTURE(Fixture, "prettyPrint_typeof_spaces_around_tokens") { std::string code = R"( type X = typeof(x) )"; - CHECK_EQ(code, transpile(code, {}, true).code); + CHECK_EQ(code, prettyPrint(code, {}, true).code); code = R"( type X = typeof(x) )"; - CHECK_EQ(code, transpile(code, {}, true).code); + CHECK_EQ(code, prettyPrint(code, {}, true).code); code = R"( type X = typeof (x) )"; - CHECK_EQ(code, transpile(code, {}, true).code); + CHECK_EQ(code, prettyPrint(code, {}, true).code); code = R"( type X = typeof( x) )"; - CHECK_EQ(code, transpile(code, {}, true).code); + CHECK_EQ(code, prettyPrint(code, {}, true).code); code = R"( type X = typeof(x ) )"; - CHECK_EQ(code, transpile(code, {}, true).code); + CHECK_EQ(code, prettyPrint(code, {}, true).code); } -TEST_CASE("transpile_single_quoted_string_types") +TEST_CASE("prettyPrint_single_quoted_string_types") { const std::string code = R"( type a = 'hello world' )"; - CHECK_EQ(code, transpile(code, {}, true).code); + CHECK_EQ(code, prettyPrint(code, {}, true).code); } -TEST_CASE("transpile_double_quoted_string_types") +TEST_CASE("prettyPrint_double_quoted_string_types") { const std::string code = R"( type a = "hello world" )"; - CHECK_EQ(code, transpile(code, {}, true).code); + CHECK_EQ(code, prettyPrint(code, {}, true).code); } -TEST_CASE("transpile_raw_string_types") +TEST_CASE("prettyPrint_raw_string_types") { std::string code = R"( type a = [[ hello world ]] )"; - CHECK_EQ(code, transpile(code, {}, true).code); + CHECK_EQ(code, prettyPrint(code, {}, true).code); code = R"( type a = [==[ hello world ]==] )"; - CHECK_EQ(code, transpile(code, {}, true).code); + CHECK_EQ(code, prettyPrint(code, {}, true).code); } -TEST_CASE("transpile_escaped_string_types") +TEST_CASE("prettyPrint_escaped_string_types") { const std::string code = R"( type a = "\\b\\t\\n\\\\" )"; - CHECK_EQ(code, transpile(code, {}, true).code); + CHECK_EQ(code, prettyPrint(code, {}, true).code); } -TEST_CASE("transpile_type_table_semicolon_separators") +TEST_CASE("prettyPrint_type_table_semicolon_separators") { const std::string code = R"( type Foo = { @@ -1762,10 +1762,10 @@ TEST_CASE("transpile_type_table_semicolon_separators") baz: number; } )"; - CHECK_EQ(code, transpile(code, {}, true).code); + CHECK_EQ(code, prettyPrint(code, {}, true).code); } -TEST_CASE("transpile_type_table_access_modifiers") +TEST_CASE("prettyPrint_type_table_access_modifiers") { std::string code = R"( type Foo = { @@ -1773,76 +1773,76 @@ TEST_CASE("transpile_type_table_access_modifiers") write baz: number, } )"; - CHECK_EQ(code, transpile(code, {}, true).code); + CHECK_EQ(code, prettyPrint(code, {}, true).code); code = R"( type Foo = { read string } )"; - CHECK_EQ(code, transpile(code, {}, true).code); + CHECK_EQ(code, prettyPrint(code, {}, true).code); code = R"( type Foo = { read [string]: number, read ["property"]: number } )"; - CHECK_EQ(code, transpile(code, {}, true).code); + CHECK_EQ(code, prettyPrint(code, {}, true).code); } -TEST_CASE("transpile_type_table_spaces_between_tokens") +TEST_CASE("prettyPrint_type_table_spaces_between_tokens") { std::string code = R"( type Foo = { bar: number, } )"; - CHECK_EQ(code, transpile(code, {}, true).code); + CHECK_EQ(code, prettyPrint(code, {}, true).code); code = R"( type Foo = { bar: number, } )"; - CHECK_EQ(code, transpile(code, {}, true).code); + CHECK_EQ(code, prettyPrint(code, {}, true).code); code = R"( type Foo = { bar : number, } )"; - CHECK_EQ(code, transpile(code, {}, true).code); + CHECK_EQ(code, prettyPrint(code, {}, true).code); code = R"( type Foo = { bar: number, } )"; - CHECK_EQ(code, transpile(code, {}, true).code); + CHECK_EQ(code, prettyPrint(code, {}, true).code); code = R"( type Foo = { bar: number , } )"; - CHECK_EQ(code, transpile(code, {}, true).code); + CHECK_EQ(code, prettyPrint(code, {}, true).code); code = R"( type Foo = { bar: number, } )"; - CHECK_EQ(code, transpile(code, {}, true).code); + CHECK_EQ(code, prettyPrint(code, {}, true).code); code = R"( type Foo = { bar: number } )"; - CHECK_EQ(code, transpile(code, {}, true).code); + CHECK_EQ(code, prettyPrint(code, {}, true).code); code = R"( type Foo = { [string]: number } )"; - CHECK_EQ(code, transpile(code, {}, true).code); + CHECK_EQ(code, prettyPrint(code, {}, true).code); code = R"( type Foo = { [string]: number } )"; - CHECK_EQ(code, transpile(code, {}, true).code); + CHECK_EQ(code, prettyPrint(code, {}, true).code); code = R"( type Foo = { [ string]: number } )"; - CHECK_EQ(code, transpile(code, {}, true).code); + CHECK_EQ(code, prettyPrint(code, {}, true).code); code = R"( type Foo = { [string ]: number } )"; - CHECK_EQ(code, transpile(code, {}, true).code); + CHECK_EQ(code, prettyPrint(code, {}, true).code); code = R"( type Foo = { [string] : number } )"; - CHECK_EQ(code, transpile(code, {}, true).code); + CHECK_EQ(code, prettyPrint(code, {}, true).code); code = R"( type Foo = { [string]: number } )"; - CHECK_EQ(code, transpile(code, {}, true).code); + CHECK_EQ(code, prettyPrint(code, {}, true).code); } -TEST_CASE("transpile_type_table_preserve_original_indexer_style") +TEST_CASE("prettyPrint_type_table_preserve_original_indexer_style") { std::string code = R"( type Foo = { [number]: string } )"; - CHECK_EQ(code, transpile(code, {}, true).code); + CHECK_EQ(code, prettyPrint(code, {}, true).code); code = R"( type Foo = { { number } } )"; - CHECK_EQ(code, transpile(code, {}, true).code); + CHECK_EQ(code, prettyPrint(code, {}, true).code); } -TEST_CASE("transpile_type_table_preserve_indexer_location") +TEST_CASE("prettyPrint_type_table_preserve_indexer_location") { std::string code = R"( type Foo = { @@ -1850,7 +1850,7 @@ TEST_CASE("transpile_type_table_preserve_indexer_location") property: number, } )"; - CHECK_EQ(code, transpile(code, {}, true).code); + CHECK_EQ(code, prettyPrint(code, {}, true).code); code = R"( type Foo = { @@ -1858,7 +1858,7 @@ TEST_CASE("transpile_type_table_preserve_indexer_location") [number]: string, } )"; - CHECK_EQ(code, transpile(code, {}, true).code); + CHECK_EQ(code, prettyPrint(code, {}, true).code); code = R"( type Foo = { @@ -1867,10 +1867,10 @@ TEST_CASE("transpile_type_table_preserve_indexer_location") property2: number, } )"; - CHECK_EQ(code, transpile(code, {}, true).code); + CHECK_EQ(code, prettyPrint(code, {}, true).code); } -TEST_CASE("transpile_type_table_preserve_property_definition_style") +TEST_CASE("prettyPrint_type_table_preserve_property_definition_style") { std::string code = R"( type Foo = { @@ -1878,10 +1878,10 @@ TEST_CASE("transpile_type_table_preserve_property_definition_style") ['$$typeof2']: string, } )"; - CHECK_EQ(code, transpile(code, {}, true).code); + CHECK_EQ(code, prettyPrint(code, {}, true).code); } -TEST_CASE("transpile_type_table_string_properties_spaces_between_tokens") +TEST_CASE("prettyPrint_type_table_string_properties_spaces_between_tokens") { std::string code = R"( type Foo = { @@ -1889,25 +1889,25 @@ TEST_CASE("transpile_type_table_string_properties_spaces_between_tokens") ['$$typeof2' ]: string, } )"; - CHECK_EQ(code, transpile(code, {}, true).code); + CHECK_EQ(code, prettyPrint(code, {}, true).code); } -TEST_CASE("transpile_types_preserve_parentheses_style") +TEST_CASE("prettyPrint_types_preserve_parentheses_style") { std::string code = R"( type Foo = number )"; - CHECK_EQ(code, transpile(code, {}, true).code); + CHECK_EQ(code, prettyPrint(code, {}, true).code); code = R"( type Foo = (number) )"; - CHECK_EQ(code, transpile(code, {}, true).code); + CHECK_EQ(code, prettyPrint(code, {}, true).code); code = R"( type Foo = ((number)) )"; - CHECK_EQ(code, transpile(code, {}, true).code); + CHECK_EQ(code, prettyPrint(code, {}, true).code); code = R"( type Foo = ( (number) ) )"; - CHECK_EQ(code, transpile(code, {}, true).code); + CHECK_EQ(code, prettyPrint(code, {}, true).code); } -TEST_CASE("fuzzer_transpile_with_zero_location") +TEST_CASE("fuzzer_prettyPrint_with_zero_location") { const std::string example = R"( if _ then @@ -1926,205 +1926,205 @@ end auto names = std::make_unique(*allocator); ParseResult parseResult = Parser::parse(example.data(), example.size(), *names, *allocator, parseOptions); - transpileWithTypes(*parseResult.root); + prettyPrintWithTypes(*parseResult.root); } -TEST_CASE("transpile_type_function_unnamed_arguments") +TEST_CASE("prettyPrint_type_function_unnamed_arguments") { std::string code = R"( type Foo = () -> () )"; - CHECK_EQ(code, transpile(code, {}, true).code); + CHECK_EQ(code, prettyPrint(code, {}, true).code); code = R"( type Foo = () -> () )"; - CHECK_EQ(code, transpile(code, {}, true).code); + CHECK_EQ(code, prettyPrint(code, {}, true).code); code = R"( type Foo = (string) -> () )"; - CHECK_EQ(code, transpile(code, {}, true).code); + CHECK_EQ(code, prettyPrint(code, {}, true).code); code = R"( type Foo = (string, number) -> () )"; - CHECK_EQ(code, transpile(code, {}, true).code); + CHECK_EQ(code, prettyPrint(code, {}, true).code); code = R"( type Foo = ( string, number) -> () )"; - CHECK_EQ(code, transpile(code, {}, true).code); + CHECK_EQ(code, prettyPrint(code, {}, true).code); code = R"( type Foo = (string , number) -> () )"; - CHECK_EQ(code, transpile(code, {}, true).code); + CHECK_EQ(code, prettyPrint(code, {}, true).code); code = R"( type Foo = (string, number) -> () )"; - CHECK_EQ(code, transpile(code, {}, true).code); + CHECK_EQ(code, prettyPrint(code, {}, true).code); code = R"( type Foo = (string, number ) -> () )"; - CHECK_EQ(code, transpile(code, {}, true).code); + CHECK_EQ(code, prettyPrint(code, {}, true).code); code = R"( type Foo = (string, number) -> () )"; - CHECK_EQ(code, transpile(code, {}, true).code); + CHECK_EQ(code, prettyPrint(code, {}, true).code); code = R"( type Foo = (string, number) -> () )"; - CHECK_EQ(code, transpile(code, {}, true).code); + CHECK_EQ(code, prettyPrint(code, {}, true).code); } -TEST_CASE("transpile_type_function_named_arguments") +TEST_CASE("prettyPrint_type_function_named_arguments") { std::string code = R"( type Foo = (x: string) -> () )"; - CHECK_EQ(code, transpile(code, {}, true).code); + CHECK_EQ(code, prettyPrint(code, {}, true).code); code = R"( type Foo = (x: string, y: number) -> () )"; - CHECK_EQ(code, transpile(code, {}, true).code); + CHECK_EQ(code, prettyPrint(code, {}, true).code); code = R"( type Foo = ( x: string, y: number) -> () )"; - CHECK_EQ(code, transpile(code, {}, true).code); + CHECK_EQ(code, prettyPrint(code, {}, true).code); code = R"( type Foo = (x : string, y: number) -> () )"; - CHECK_EQ(code, transpile(code, {}, true).code); + CHECK_EQ(code, prettyPrint(code, {}, true).code); code = R"( type Foo = (x: string, y: number) -> () )"; - CHECK_EQ(code, transpile(code, {}, true).code); + CHECK_EQ(code, prettyPrint(code, {}, true).code); code = R"( type Foo = (x: string, y: number) -> () )"; - CHECK_EQ(code, transpile(code, {}, true).code); + CHECK_EQ(code, prettyPrint(code, {}, true).code); code = R"( type Foo = (number, info: string) -> () )"; - CHECK_EQ(code, transpile(code, {}, true).code); + CHECK_EQ(code, prettyPrint(code, {}, true).code); code = R"( type Foo = (first: string, second: string, ...string) -> () )"; - CHECK_EQ(code, transpile(code, {}, true).code); + CHECK_EQ(code, prettyPrint(code, {}, true).code); } -TEST_CASE("transpile_type_function_generics") +TEST_CASE("prettyPrint_type_function_generics") { std::string code = R"( type Foo = () -> () )"; - CHECK_EQ(code, transpile(code, {}, true).code); + CHECK_EQ(code, prettyPrint(code, {}, true).code); code = R"( type Foo = () -> () )"; - CHECK_EQ(code, transpile(code, {}, true).code); + CHECK_EQ(code, prettyPrint(code, {}, true).code); code = R"( type Foo = < X, Y, Z...>() -> () )"; - CHECK_EQ(code, transpile(code, {}, true).code); + CHECK_EQ(code, prettyPrint(code, {}, true).code); code = R"( type Foo = () -> () )"; - CHECK_EQ(code, transpile(code, {}, true).code); + CHECK_EQ(code, prettyPrint(code, {}, true).code); code = R"( type Foo = () -> () )"; - CHECK_EQ(code, transpile(code, {}, true).code); + CHECK_EQ(code, prettyPrint(code, {}, true).code); code = R"( type Foo = () -> () )"; - CHECK_EQ(code, transpile(code, {}, true).code); + CHECK_EQ(code, prettyPrint(code, {}, true).code); code = R"( type Foo = () -> () )"; - CHECK_EQ(code, transpile(code, {}, true).code); + CHECK_EQ(code, prettyPrint(code, {}, true).code); code = R"( type Foo = () -> () )"; - CHECK_EQ(code, transpile(code, {}, true).code); + CHECK_EQ(code, prettyPrint(code, {}, true).code); code = R"( type Foo = () -> () )"; - CHECK_EQ(code, transpile(code, {}, true).code); + CHECK_EQ(code, prettyPrint(code, {}, true).code); code = R"( type Foo = () -> () )"; - CHECK_EQ(code, transpile(code, {}, true).code); + CHECK_EQ(code, prettyPrint(code, {}, true).code); } -TEST_CASE("transpile_type_function_return_types") +TEST_CASE("prettyPrint_type_function_return_types") { std::string code = R"( type Foo = () -> () )"; - CHECK_EQ(code, transpile(code, {}, true).code); + CHECK_EQ(code, prettyPrint(code, {}, true).code); code = R"( type Foo = () -> ( ) )"; - CHECK_EQ(code, transpile(code, {}, true).code); + CHECK_EQ(code, prettyPrint(code, {}, true).code); code = R"( type Foo = () -> string )"; - CHECK_EQ(code, transpile(code, {}, true).code); + CHECK_EQ(code, prettyPrint(code, {}, true).code); code = R"( type Foo = () -> string )"; - CHECK_EQ(code, transpile(code, {}, true).code); + CHECK_EQ(code, prettyPrint(code, {}, true).code); code = R"( type Foo = () -> (string) )"; - CHECK_EQ(code, transpile(code, {}, true).code); + CHECK_EQ(code, prettyPrint(code, {}, true).code); code = R"( type Foo = () -> (string) )"; - CHECK_EQ(code, transpile(code, {}, true).code); + CHECK_EQ(code, prettyPrint(code, {}, true).code); code = R"( type Foo = () -> ...any )"; - CHECK_EQ(code, transpile(code, {}, true).code); + CHECK_EQ(code, prettyPrint(code, {}, true).code); code = R"( type Foo = () -> ...any )"; - CHECK_EQ(code, transpile(code, {}, true).code); + CHECK_EQ(code, prettyPrint(code, {}, true).code); code = R"( type Foo = () -> ... any )"; - CHECK_EQ(code, transpile(code, {}, true).code); + CHECK_EQ(code, prettyPrint(code, {}, true).code); code = R"( type Foo = () -> (...any) )"; - CHECK_EQ(code, transpile(code, {}, true).code); + CHECK_EQ(code, prettyPrint(code, {}, true).code); code = R"( type Foo = () -> ( string, number) )"; - CHECK_EQ(code, transpile(code, {}, true).code); + CHECK_EQ(code, prettyPrint(code, {}, true).code); code = R"( type Foo = () -> (string , number) )"; - CHECK_EQ(code, transpile(code, {}, true).code); + CHECK_EQ(code, prettyPrint(code, {}, true).code); code = R"( type Foo = () -> (string, number) )"; - CHECK_EQ(code, transpile(code, {}, true).code); + CHECK_EQ(code, prettyPrint(code, {}, true).code); code = R"( type Foo = () -> (string, number ) )"; - CHECK_EQ(code, transpile(code, {}, true).code); + CHECK_EQ(code, prettyPrint(code, {}, true).code); } -TEST_CASE("transpile_chained_function_types") +TEST_CASE("prettyPrint_chained_function_types") { std::string code = R"( type Foo = () -> () -> () )"; - CHECK_EQ(code, transpile(code, {}, true).code); + CHECK_EQ(code, prettyPrint(code, {}, true).code); code = R"( type Foo = () -> () -> () )"; - CHECK_EQ(code, transpile(code, {}, true).code); + CHECK_EQ(code, prettyPrint(code, {}, true).code); code = R"( type Foo = () -> () -> () )"; - CHECK_EQ(code, transpile(code, {}, true).code); + CHECK_EQ(code, prettyPrint(code, {}, true).code); } TEST_CASE("fuzzer_nil_optional") { const std::string code = R"( local x: nil? )"; - CHECK_EQ(code, transpile(code, {}, true).code); + CHECK_EQ(code, prettyPrint(code, {}, true).code); } -TEST_CASE("transpile_function_attributes") +TEST_CASE("prettyPrint_function_attributes") { std::string code = R"( @native function foo() end )"; - CHECK_EQ(code, transpile(code, {}, true).code); + CHECK_EQ(code, prettyPrint(code, {}, true).code); code = R"( @native local function foo() end )"; - CHECK_EQ(code, transpile(code, {}, true).code); + CHECK_EQ(code, prettyPrint(code, {}, true).code); code = R"( @checked local function foo() end )"; - CHECK_EQ(code, transpile(code, {}, true).code); + CHECK_EQ(code, prettyPrint(code, {}, true).code); code = R"( local foo = @native function() end )"; - CHECK_EQ(code, transpile(code, {}, true).code); + CHECK_EQ(code, prettyPrint(code, {}, true).code); code = R"( @native function foo:bar() end )"; - CHECK_EQ(code, transpile(code, {}, true).code); + CHECK_EQ(code, prettyPrint(code, {}, true).code); code = R"( @native @checked function foo:bar() end )"; - CHECK_EQ(code, transpile(code, {}, true).code); + CHECK_EQ(code, prettyPrint(code, {}, true).code); } TEST_SUITE_END(); diff --git a/tests/RequireByString.test.cpp b/tests/RequireByString.test.cpp index 9b45b82a..93cd38ec 100644 --- a/tests/RequireByString.test.cpp +++ b/tests/RequireByString.test.cpp @@ -406,6 +406,27 @@ TEST_CASE_FIXTURE(ReplWithPathFixture, "RequireWithFileAmbiguity") ); } +TEST_CASE_FIXTURE(ReplWithPathFixture, "RequireWithAmbiguityInAliasDiscovery") +{ + char executable[] = "luau"; + std::vector paths = { + getLuauDirectory(PathType::Relative) + "/tests/require/config_tests/with_config/parent_ambiguity/folder/requirer.luau", + getLuauDirectory(PathType::Absolute) + "/tests/require/config_tests/with_config/parent_ambiguity/folder/requirer.luau", + getLuauDirectory(PathType::Relative) + "/tests/require/config_tests/with_config_luau/parent_ambiguity/folder/requirer.luau", + getLuauDirectory(PathType::Absolute) + "/tests/require/config_tests/with_config_luau/parent_ambiguity/folder/requirer.luau", + }; + + for (const std::string& path : paths) + { + std::vector pathStr(path.size() + 1); + strncpy(pathStr.data(), path.c_str(), path.size()); + pathStr[path.size()] = '\0'; + + char* argv[2] = {executable, pathStr.data()}; + CHECK_EQ(replMain(2, argv), 0); + } +} + TEST_CASE_FIXTURE(ReplWithPathFixture, "RequireWithDirectoryAmbiguity") { std::string ambiguousPath = getLuauDirectory(PathType::Relative) + "/tests/require/without_config/ambiguous_directory_requirer"; @@ -615,23 +636,44 @@ TEST_CASE_FIXTURE(ReplWithPathFixture, "RequireUnprefixedPath") TEST_CASE_FIXTURE(ReplWithPathFixture, "RequirePathWithAlias") { - std::string path = getLuauDirectory(PathType::Relative) + "/tests/require/with_config/src/alias_requirer"; - runProtectedRequire(path); - assertOutputContainsAll({"true", "result from dependency"}); + { + std::string path = getLuauDirectory(PathType::Relative) + "/tests/require/config_tests/with_config/src/alias_requirer"; + runProtectedRequire(path); + assertOutputContainsAll({"true", "result from dependency"}); + } + { + std::string path = getLuauDirectory(PathType::Relative) + "/tests/require/config_tests/with_config_luau/src/alias_requirer"; + runProtectedRequire(path); + assertOutputContainsAll({"true", "result from dependency"}); + } } TEST_CASE_FIXTURE(ReplWithPathFixture, "RequirePathWithParentAlias") { - std::string path = getLuauDirectory(PathType::Relative) + "/tests/require/with_config/src/parent_alias_requirer"; - runProtectedRequire(path); - assertOutputContainsAll({"true", "result from other_dependency"}); + { + std::string path = getLuauDirectory(PathType::Relative) + "/tests/require/config_tests/with_config/src/parent_alias_requirer"; + runProtectedRequire(path); + assertOutputContainsAll({"true", "result from other_dependency"}); + } + { + std::string path = getLuauDirectory(PathType::Relative) + "/tests/require/config_tests/with_config_luau/src/parent_alias_requirer"; + runProtectedRequire(path); + assertOutputContainsAll({"true", "result from other_dependency"}); + } } TEST_CASE_FIXTURE(ReplWithPathFixture, "RequirePathWithAliasPointingToDirectory") { - std::string path = getLuauDirectory(PathType::Relative) + "/tests/require/with_config/src/directory_alias_requirer"; - runProtectedRequire(path); - assertOutputContainsAll({"true", "result from subdirectory_dependency"}); + { + std::string path = getLuauDirectory(PathType::Relative) + "/tests/require/config_tests/with_config/src/directory_alias_requirer"; + runProtectedRequire(path); + assertOutputContainsAll({"true", "result from subdirectory_dependency"}); + } + { + std::string path = getLuauDirectory(PathType::Relative) + "/tests/require/config_tests/with_config_luau/src/directory_alias_requirer"; + runProtectedRequire(path); + assertOutputContainsAll({"true", "result from subdirectory_dependency"}); + } } TEST_CASE_FIXTURE(ReplWithPathFixture, "RequireAliasThatDoesNotExist") @@ -666,6 +708,27 @@ TEST_CASE_FIXTURE(ReplWithPathFixture, "AliasHasIllegalFormat") assertOutputContainsAll({"false", " is not a valid alias"}); } +TEST_CASE_FIXTURE(ReplWithPathFixture, "AliasNotParsedIfConfigsAmbiguous") +{ + std::string path = getLuauDirectory(PathType::Relative) + "/tests/require/config_tests/config_ambiguity/requirer"; + runProtectedRequire(path); + assertOutputContainsAll({"false", "could not resolve alias \"dep\" (ambiguous configuration file)"}); +} + +TEST_CASE_FIXTURE(ReplWithPathFixture, "AliasNotParsedIfLuauConfigTimesOut" * doctest::timeout(5)) +{ + std::string path = getLuauDirectory(PathType::Relative) + "/tests/require/config_tests/config_luau_timeout/requirer"; + runProtectedRequire(path); + assertOutputContainsAll({"false", "configuration execution timed out"}); +} + +TEST_CASE_FIXTURE(ReplWithPathFixture, "CannotRequireConfigLuau") +{ + std::string path = getLuauDirectory(PathType::Relative) + "/tests/require/config_tests/config_cannot_be_required/requirer"; + runProtectedRequire(path); + assertOutputContainsAll({"false", "could not resolve child component \".config\""}); +} + TEST_CASE_FIXTURE(ReplWithPathFixture, "RequireFromLuauBinary") { char executable[] = "luau"; @@ -676,8 +739,10 @@ TEST_CASE_FIXTURE(ReplWithPathFixture, "RequireFromLuauBinary") getLuauDirectory(PathType::Absolute) + "/tests/require/without_config/module.luau", getLuauDirectory(PathType::Relative) + "/tests/require/without_config/nested/init.luau", getLuauDirectory(PathType::Absolute) + "/tests/require/without_config/nested/init.luau", - getLuauDirectory(PathType::Relative) + "/tests/require/with_config/src/submodule/init.luau", - getLuauDirectory(PathType::Absolute) + "/tests/require/with_config/src/submodule/init.luau", + getLuauDirectory(PathType::Relative) + "/tests/require/config_tests/with_config/src/submodule/init.luau", + getLuauDirectory(PathType::Absolute) + "/tests/require/config_tests/with_config/src/submodule/init.luau", + getLuauDirectory(PathType::Relative) + "/tests/require/config_tests/with_config_luau/src/submodule/init.luau", + getLuauDirectory(PathType::Absolute) + "/tests/require/config_tests/with_config_luau/src/submodule/init.luau", }; for (const std::string& path : paths) diff --git a/tests/RuntimeLimits.test.cpp b/tests/RuntimeLimits.test.cpp index a4b1220c..ac30ff2e 100644 --- a/tests/RuntimeLimits.test.cpp +++ b/tests/RuntimeLimits.test.cpp @@ -11,6 +11,7 @@ #include "Fixture.h" +#include "Luau/Error.h" #include "doctest.h" #include @@ -23,13 +24,12 @@ LUAU_FASTINT(LuauTypeInferRecursionLimit) LUAU_FASTFLAG(LuauSolverV2) LUAU_FASTFLAG(LuauIceLess) -LUAU_FASTFLAG(LuauSimplifyAnyAndUnion) -LUAU_FASTFLAG(LuauLimitDynamicConstraintSolving3) LUAU_FASTFLAG(LuauDontDynamicallyCreateRedundantSubtypeConstraints) LUAU_FASTFLAG(LuauLimitUnification) -LUAU_FASTFLAG(LuauSubtypingGenericsDoesntUseVariance) LUAU_FASTFLAG(LuauReduceSetTypeStackPressure) +LUAU_FASTFLAG(LuauUseNativeStackGuard) LUAU_FASTINT(LuauGenericCounterMaxDepth) +LUAU_FASTFLAG(LuauNormalizerStepwiseFuel) struct LimitFixture : BuiltinsFixture { @@ -290,14 +290,10 @@ TEST_CASE_FIXTURE(LimitFixture, "typescript_port_of_Result_type") CHECK(hasError(result)); } -TEST_CASE_FIXTURE(LimitFixture, "Signal_exerpt" * doctest::timeout(0.5)) +TEST_CASE_FIXTURE(LimitFixture, "Signal_exerpt" * doctest::timeout(1.0)) { ScopedFastFlag sff[] = { - // These flags are required to surface the problem. {FFlag::LuauSolverV2, true}, - - // And this flag is the one that fixes it. - {FFlag::LuauSimplifyAnyAndUnion, true}, }; constexpr const char* src = R"LUAU( @@ -341,10 +337,7 @@ TEST_CASE_FIXTURE(LimitFixture, "Signal_exerpt" * doctest::timeout(0.5)) TEST_CASE_FIXTURE(Fixture, "limit_number_of_dynamically_created_constraints") { - ScopedFastFlag sff[] = { - {FFlag::LuauSolverV2, true}, - {FFlag::LuauLimitDynamicConstraintSolving3, true}, - }; + ScopedFastFlag sff{FFlag::LuauSolverV2, true}; constexpr const char* src = R"( type Array = {T} @@ -375,7 +368,6 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "limit_number_of_dynamically_created_constrai { ScopedFastFlag sff[] = { {FFlag::LuauSolverV2, true}, - {FFlag::LuauLimitDynamicConstraintSolving3, true}, {FFlag::LuauDontDynamicallyCreateRedundantSubtypeConstraints, true}, }; @@ -429,13 +421,9 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "limit_number_of_dynamically_created_constrai CHECK(frontend->stats.dynamicConstraintsCreated < 40); } -TEST_CASE_FIXTURE(BuiltinsFixture, "subtyping_should_cache_pairs_in_seen_set" * doctest::timeout(0.5)) +TEST_CASE_FIXTURE(BuiltinsFixture, "subtyping_should_cache_pairs_in_seen_set" * doctest::timeout(1.0)) { - ScopedFastFlag sff[] = { - {FFlag::LuauSolverV2, true}, - // This flags surfaced and solves the problem. (The original PR was reverted) - {FFlag::LuauSubtypingGenericsDoesntUseVariance, true}, - }; + ScopedFastFlag sff{FFlag::LuauSolverV2, true}; constexpr const char* src = R"LUAU( type DataProxy = any @@ -598,4 +586,110 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "unification_runs_a_limited_number_of_iterati LUAU_REQUIRE_ERROR(result, UnificationTooComplex); } +#if defined(_MSC_VER) || defined(__APPLE__) + +TEST_CASE_FIXTURE(BuiltinsFixture, "native_stack_guard_prevents_stack_overflows" * doctest::timeout(4.0)) +{ + ScopedFastFlag sff[] = { + {FFlag::LuauSolverV2, true}, + {FFlag::LuauLimitUnification, true}, + {FFlag::LuauUseNativeStackGuard, true}, + }; + + // Disable the iteration limit and very tightly constrain the stack. + ScopedFastInt sfi{FInt::LuauTypeInferIterationLimit, 0}; + limitStackSize(38600); + + try + { + (void)check(R"( + local function l0() + for l0=_,_ do + end + end + + _ = if _._ then function(l0) + end elseif _._G then if `` then {n0=_,} else "luauExprConstantSt" elseif _[_][l0] then function() + end elseif _.n0 then if _[_] then if _ then _ else "aeld" elseif false then 0 else "lead" + return _.n0 + )"); + } + catch (InternalCompilerError& err) + { + // HACK: This test doesn't consistently stack overflow in the same subsystem because + // there is some other unrelated source of nondeterminism in the solver. + // For this test, it's aside from the point, so we write it to be a little bit flexible. + const std::string prefix = "Stack overflow in "; + CHECK(prefix == std::string(err.what()).substr(0, prefix.size())); + return; + } + + CHECK_MESSAGE(false, "An expected InternalCompilerError was not thrown!"); +} + +#endif + +TEST_CASE_FIXTURE(BuiltinsFixture, "fusion_normalization_spin" * doctest::timeout(1.0)) +{ + LUAU_REQUIRE_ERRORS(check(R"( +type Task = unknown +type Constructors = unknown +export type Scope = {Task} & Constructors +export type DeriveScopeConstructor = ((Scope) -> Scope) + & ((Scope, A & {}) -> Scope) + & ((Scope, A & {}, B & {}) -> Scope) + & ((Scope, A & {}, B & {}, C & {}) -> Scope) + & ((Scope, A & {}, B & {}, C & {}, D & {}) -> Scope) + & ((Scope, A & {}, B & {}, C & {}, D & {}, E & {}) -> Scope) + & ((Scope, A & {}, B & {}, C & {}, D & {}, E & {}, F & {}) -> Scope) + & ((Scope, A & {}, B & {}, C & {}, D & {}, E & {}, F & {}, G & {}) -> Scope) + & ((Scope, A & {}, B & {}, C & {}, D & {}, E & {}, F & {}, G & {}, H & {}) -> Scope) + & ((Scope, A & {}, B & {}, C & {}, D & {}, E & {}, F & {}, G & {}, H & {}, I & {}) -> Scope) + & ((Scope, A & {}, B & {}, C & {}, D & {}, E & {}, F & {}, G & {}, H & {}, I & {}, J & {}) -> Scope) + & ((Scope, A & {}, B & {}, C & {}, D & {}, E & {}, F & {}, G & {}, H & {}, I & {}, J & {}, K & {}) -> Scope) + & ((Scope, A & {}, B & {}, C & {}, D & {}, E & {}, F & {}, G & {}, H & {}, I & {}, J & {}, K & {}, L & {}) -> Scope) + +local deriveScopeImpl : DeriveScopeConstructor = (nil :: any) + +local function innerScope( + existing: Types.Scope, + ...: {[unknown]: unknown} +): any + local new = deriveScopeImpl(existing, ...) +end + + )")); +} + +TEST_CASE_FIXTURE(BuiltinsFixture, "fuzzer_" * doctest::timeout(4.0)) +{ + ScopedFastFlag _{FFlag::LuauNormalizerStepwiseFuel, true}; + + LUAU_REQUIRE_ERRORS(check(R"( + _ = if _ then {n0=# _,[_]=_,``,[function(l0,l0,l0) + do end + end]=_,setmetatable,[l0(_ + _)]=_,} else _(),_,_ + _[_](_,_(coroutine,_,_,nil),_(0,_()),function() + end) + )")); +} + +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 _._,... + _ = if _ then _ elseif _ then `` else _.n0,true,... + _G = if "" then _ else _.n0,_ + _ = if _[_] then _ elseif _ then _ + n0 else _._,32804,... + _.readstring = _,_ + local l0 = require(module0) + _ = _,l0,_ + do end + _.readstring += _ + )")); +} + TEST_SUITE_END(); diff --git a/tests/Simplify.test.cpp b/tests/Simplify.test.cpp index a63ad778..714f99d8 100644 --- a/tests/Simplify.test.cpp +++ b/tests/Simplify.test.cpp @@ -9,8 +9,9 @@ using namespace Luau; LUAU_FASTFLAG(LuauSolverV2) -LUAU_FASTFLAG(LuauSimplifyAnyAndUnion) +LUAU_FASTFLAG(LuauSimplifyRefinementOfReadOnlyProperty) LUAU_DYNAMIC_FASTINT(LuauSimplificationComplexityLimit) +LUAU_FASTFLAG(LuauSimplifyIntersectionNoTreeSet) namespace { @@ -91,11 +92,11 @@ struct SimplifyFixture : Fixture return bool(get(follow(a))); } - TypeId mkTable(std::map propTypes) + TypeId mkTable(std::map propTypes) { TableType::Props props; - for (const auto& [name, ty] : propTypes) - props[name] = Property{ty}; + for (const auto& [name, prop] : propTypes) + props[name] = prop; return arena->addType(TableType{props, {}, TypeLevel{}, TableState::Sealed}); } @@ -623,8 +624,6 @@ TEST_CASE_FIXTURE(SimplifyFixture, "cyclic_never_union_and_string") TEST_CASE_FIXTURE(SimplifyFixture, "any & (error | string)") { - ScopedFastFlag sff{FFlag::LuauSimplifyAnyAndUnion, true}; - TypeId errStringTy = arena->addType(UnionType{{getBuiltins()->errorType, getBuiltins()->stringType}}); auto res = intersect(builtinTypes->anyType, errStringTy); @@ -634,8 +633,6 @@ TEST_CASE_FIXTURE(SimplifyFixture, "any & (error | string)") TEST_CASE_FIXTURE(SimplifyFixture, "(error | string) & any") { - ScopedFastFlag sff{FFlag::LuauSimplifyAnyAndUnion, true}; - TypeId errStringTy = arena->addType(UnionType{{getBuiltins()->errorType, getBuiltins()->stringType}}); auto res = intersect(errStringTy, builtinTypes->anyType); @@ -643,4 +640,59 @@ TEST_CASE_FIXTURE(SimplifyFixture, "(error | string) & any") CHECK("*error-type* | string" == toString(res)); } +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)}}); + + CHECK(leftTy == intersect(leftTy, rightTy)); +} + +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)}}); + + CHECK(leftTy == intersect(leftTy, rightTy)); +} + +TEST_CASE_FIXTURE(SimplifyFixture, "{ read x: Child } & { x: Parent }") +{ + ScopedFastFlag sff{FFlag::LuauSimplifyRefinementOfReadOnlyProperty, true}; + + createSomeExternTypes(getFrontend()); + + TypeId parentTy = getFrontend().globals.globalScope->exportedTypeBindings["Parent"].type; + REQUIRE(parentTy); + + TypeId childTy = getFrontend().globals.globalScope->exportedTypeBindings["Child"].type; + REQUIRE(childTy); + + TypeId leftTy = mkTable({{"x", Property::readonly(childTy)}}); + TypeId rightTy = mkTable({{"x", parentTy}}); + + // TODO: This could be { read x: Child, write x: Parent } + CHECK("{ read x: Child } & { x: Parent }" == toString(intersect(leftTy, rightTy))); +} + +TEST_CASE_FIXTURE(SimplifyFixture, "intersect_parts_empty_table_non_empty") +{ + ScopedFastFlag _{FFlag::LuauSimplifyIntersectionNoTreeSet, true}; + + TableType empty; + empty.state = TableState::Sealed; + TypeId emptyTable = arena->addType(std::move(empty)); + + TableType nonEmpty; + nonEmpty.props["p"] = arena->addType(UnionType{{getBuiltins()->numberType, getBuiltins()->stringType}}); + nonEmpty.state = TableState::Sealed; + TypeId nonEmptyTable = arena->addType(std::move(nonEmpty)); + + CHECK("{ p: number | string }" == toString(simplifyIntersection(getBuiltins(), arena, {nonEmptyTable, emptyTable}).result)); +} + TEST_SUITE_END(); diff --git a/tests/Subtyping.test.cpp b/tests/Subtyping.test.cpp index 943d5e0b..aeb2bb55 100644 --- a/tests/Subtyping.test.cpp +++ b/tests/Subtyping.test.cpp @@ -16,10 +16,8 @@ #include LUAU_FASTFLAG(LuauSolverV2) -LUAU_FASTFLAG(LuauReturnMappedGenericPacksFromSubtyping2) -LUAU_FASTFLAG(LuauSubtypingGenericsDoesntUseVariance) -LUAU_FASTFLAG(LuauVariadicAnyPackShouldBeErrorSuppressing) -LUAU_FASTFLAG(LuauSubtypingGenericPacksDoesntUseVariance) +LUAU_FASTFLAG(LuauPassBindableGenericsByReference) +LUAU_FASTFLAG(LuauConsiderErrorSuppressionInTypes) using namespace Luau; @@ -75,6 +73,7 @@ struct SubtypeFixture : Fixture TypeFunctionRuntime typeFunctionRuntime{NotNull{&iceReporter}, NotNull{&limits}}; ScopedFastFlag sff{FFlag::LuauSolverV2, true}; + ScopedFastFlag sff1{FFlag::LuauPassBindableGenericsByReference, true}; ScopePtr rootScope{new Scope(getBuiltins()->emptyTypePack)}; ScopePtr moduleScope{new Scope(rootScope)}; @@ -198,7 +197,7 @@ struct SubtypeFixture : Fixture SubtypingResult isSubtype(TypePackId subTy, TypePackId superTy) { - return subtyping.isSubtype(subTy, superTy, NotNull{rootScope.get()}); + return subtyping.isSubtype(subTy, superTy, NotNull{rootScope.get()}, {}); } TypeId helloType = arena.addType(SingletonType{StringSingleton{"hello"}}); @@ -216,7 +215,7 @@ struct SubtypeFixture : Fixture TypeId booleanAndTrueType = meet(getBuiltins()->booleanType, getBuiltins()->trueType); /** - * class + * userdata * \- Root * |- Child * | |-GrandchildOne @@ -972,12 +971,12 @@ TEST_IS_NOT_SUBTYPE(getBuiltins()->unknownType, negate(getBuiltins()->stringType TEST_IS_NOT_SUBTYPE(getBuiltins()->unknownType, negate(getBuiltins()->threadType)); TEST_IS_NOT_SUBTYPE(getBuiltins()->unknownType, negate(getBuiltins()->bufferType)); -TEST_CASE_FIXTURE(SubtypeFixture, "Root <: class") +TEST_CASE_FIXTURE(SubtypeFixture, "Root <: userdata") { CHECK_IS_SUBTYPE(rootClass, getBuiltins()->externType); } -TEST_CASE_FIXTURE(SubtypeFixture, "Child | AnotherChild <: class") +TEST_CASE_FIXTURE(SubtypeFixture, "Child | AnotherChild <: userdata") { CHECK_IS_SUBTYPE(join(childClass, anotherChildClass), getBuiltins()->externType); } @@ -992,17 +991,17 @@ TEST_CASE_FIXTURE(SubtypeFixture, "Child | Root <: Root") CHECK_IS_SUBTYPE(join(childClass, rootClass), rootClass); } -TEST_CASE_FIXTURE(SubtypeFixture, "Child & AnotherChild <: class") +TEST_CASE_FIXTURE(SubtypeFixture, "Child & AnotherChild <: userdata") { CHECK_IS_SUBTYPE(meet(childClass, anotherChildClass), getBuiltins()->externType); } -TEST_CASE_FIXTURE(SubtypeFixture, "Child & Root <: class") +TEST_CASE_FIXTURE(SubtypeFixture, "Child & Root <: userdata") { CHECK_IS_SUBTYPE(meet(childClass, rootClass), getBuiltins()->externType); } -TEST_CASE_FIXTURE(SubtypeFixture, "Child & ~Root <: class") +TEST_CASE_FIXTURE(SubtypeFixture, "Child & ~Root <: userdata") { CHECK_IS_SUBTYPE(meet(childClass, negate(rootClass)), getBuiltins()->externType); } @@ -1403,9 +1402,6 @@ TEST_CASE_FIXTURE(SubtypeFixture, "({ x: T }) -> T <: ({ method: ({ x: T } TEST_CASE_FIXTURE(SubtypeFixture, "subtyping_reasonings_to_follow_a_reduced_type_function_instance") { - ScopedFastFlag sff{FFlag::LuauReturnMappedGenericPacksFromSubtyping2, true}; - ScopedFastFlag sff1{FFlag::LuauSubtypingGenericPacksDoesntUseVariance, true}; - TypeId longTy = arena.addType( UnionType{ {getBuiltins()->booleanType, @@ -1438,10 +1434,65 @@ TEST_CASE_FIXTURE(SubtypeFixture, "subtyping_reasonings_to_follow_a_reduced_type } } -TEST_CASE_FIXTURE(SubtypeFixture, "(() -> number) -> () <: (() -> T) -> ()") +TEST_CASE_FIXTURE(SubtypeFixture, "(number, number...) numberType}, arena.addTypePack(VariadicTypePack{builtinTypes->numberType})); + TypePackId rightTp = arena.addTypePack({builtinTypes->numberType}, arena.addTypePack(VariadicTypePack{builtinTypes->stringType})); + + CHECK_IS_NOT_SUBTYPE(leftTp, rightTp); +} + +TEST_CASE_FIXTURE(SubtypeFixture, "subtyping_reasonings_check_for_error_suppression_in_union_type_path") +{ + ScopedFastFlag sff{FFlag::LuauConsiderErrorSuppressionInTypes, true}; + TypeId subTy = arena.addType(UnionType{{getBuiltins()->numberType, getBuiltins()->errorType}}); + TypeId superTy = getBuiltins()->booleanType; + SubtypingResult result = isSubtype(subTy, superTy); + CHECK(!result.isSubtype); + + for (const SubtypingReasoning& reasoning : result.reasoning) + { + if (reasoning.subPath.empty() && reasoning.superPath.empty()) + continue; + + std::optional optSubLeaf = traverseForType(subTy, reasoning.subPath, getBuiltins(), NotNull{&arena}); + std::optional optSuperLeaf = traverseForType(superTy, reasoning.superPath, getBuiltins(), NotNull{&arena}); + + if (!optSubLeaf || !optSuperLeaf) + CHECK(false); + + if (optSubLeaf != getBuiltins()->errorType) + CHECK(false); + } +} + +TEST_CASE_FIXTURE(SubtypeFixture, "subtyping_reasonings_check_for_error_suppression_in_intersect_type_path") { - ScopedFastFlag _{FFlag::LuauSubtypingGenericsDoesntUseVariance, true}; + ScopedFastFlag sff{FFlag::LuauConsiderErrorSuppressionInTypes, true}; + TypeId subTy = getBuiltins()->booleanType; + TypeId superTy = arena.addType(IntersectionType{{getBuiltins()->numberType, getBuiltins()->errorType}}); + SubtypingResult result = isSubtype(subTy, superTy); + CHECK(!result.isSubtype); + + for (const SubtypingReasoning& reasoning : result.reasoning) + { + if (reasoning.subPath.empty() && reasoning.superPath.empty()) + continue; + + std::optional optSubLeaf = traverseForType(subTy, reasoning.subPath, getBuiltins(), NotNull{&arena}); + std::optional optSuperLeaf = traverseForType(superTy, reasoning.superPath, getBuiltins(), NotNull{&arena}); + + if (!optSubLeaf || !optSuperLeaf) + CHECK(false); + + if (optSuperLeaf != getBuiltins()->errorType) + CHECK(false); + } +} + +TEST_CASE_FIXTURE(SubtypeFixture, "(() -> number) -> () <: (() -> T) -> ()") +{ TypeId f1 = fn({nothingToNumberType}, {}); TypeId f2 = fn({genericNothingToTType}, {}); CHECK_IS_SUBTYPE(f1, f2); @@ -1449,8 +1500,6 @@ TEST_CASE_FIXTURE(SubtypeFixture, "(() -> number) -> () <: (() -> T) -> ()") TEST_CASE_FIXTURE(SubtypeFixture, "((number) -> ()) -> () <: ((T) -> ()) -> ()") { - ScopedFastFlag _{FFlag::LuauSubtypingGenericsDoesntUseVariance, true}; - TypeId f1 = fn({numberToNothingType}, {}); TypeId f2 = fn({genericTToNothingType}, {}); CHECK_IS_SUBTYPE(f1, f2); @@ -1458,8 +1507,6 @@ TEST_CASE_FIXTURE(SubtypeFixture, "((number) -> ()) -> () <: ((T) -> ()) -> ( TEST_CASE_FIXTURE(SubtypeFixture, "((number) -> number) -> () <: ((T) -> T) -> ()") { - ScopedFastFlag _{FFlag::LuauSubtypingGenericsDoesntUseVariance, true}; - TypeId f1 = fn({numberToNumberType}, {}); TypeId f2 = fn({genericTToTType}, {}); CHECK_IS_SUBTYPE(f1, f2); @@ -1467,8 +1514,6 @@ TEST_CASE_FIXTURE(SubtypeFixture, "((number) -> number) -> () <: ((T) -> T) - TEST_CASE_FIXTURE(SubtypeFixture, "(x: T, y: T, f: (T, T) -> T) -> T <: (number, number, (U, U) -> add) -> number") { - ScopedFastFlag _{FFlag::LuauSubtypingGenericsDoesntUseVariance, true}; - TypeId f1 = arena.addType(FunctionType( {genericT}, {}, @@ -1493,8 +1538,6 @@ TEST_CASE_FIXTURE(SubtypeFixture, "(x: T, y: T, f: (T, T) -> T) -> T <: (numb TEST_CASE_FIXTURE(SubtypeFixture, "(A...) -> ((A...) -> ()) <: (string -> ((number) -> ())") { - ScopedFastFlag sff{FFlag::LuauSubtypingGenericPacksDoesntUseVariance, true}; - // (A...) -> () TypeId asToNothing = arena.addType(FunctionType({}, {genericAs}, genericAs, getBuiltins()->emptyTypePack, std::nullopt, false)); TypeId f1 = arena.addType(FunctionType({}, {genericAs}, genericAs, pack({asToNothing}), std::nullopt, false)); @@ -1505,10 +1548,7 @@ TEST_CASE_FIXTURE(SubtypeFixture, "(A...) -> ((A...) -> ()) <: (stri TEST_CASE_FIXTURE(SubtypeFixture, "no_caching_type_function_instances_with_mapped_generics") { - ScopedFastFlag _{FFlag::LuauSubtypingGenericsDoesntUseVariance, true}; - // ((U) -> keyof, (U) -> keyof) "a", ({"b" : number}) -> "a") - TypeId keyOfU = arena.addType(TypeFunctionInstanceType{builtinTypeFunctions.keyofFunc, {genericU}}); // (U) -> keyof TypeId uToKeyOfU = arena.addType(FunctionType({genericU}, {}, arena.addTypePack({genericU}), arena.addTypePack({keyOfU}))); @@ -1768,7 +1808,6 @@ TEST_CASE_FIXTURE(SubtypeFixture, "free_types_might_be_subtypes") TEST_CASE_FIXTURE(Fixture, "variadic_any_pack_should_suppress_errors_during_overload_resolution") { - ScopedFastFlag sff{FFlag::LuauVariadicAnyPackShouldBeErrorSuppressing, true}; auto res = check(R"( type ActionCallback = (string) -> ...any diff --git a/tests/ToString.test.cpp b/tests/ToString.test.cpp index f4184f38..637b29e7 100644 --- a/tests/ToString.test.cpp +++ b/tests/ToString.test.cpp @@ -14,10 +14,8 @@ using namespace Luau; LUAU_FASTFLAG(LuauRecursiveTypeParameterRestriction) LUAU_FASTFLAG(LuauSolverV2) -LUAU_FASTFLAG(LuauSolverAgnosticStringification) LUAU_FASTFLAG(LuauExplicitSkipBoundTypes) LUAU_FASTFLAG(LuauReduceSetTypeStackPressure) -LUAU_FASTFLAG(LuauSolverAgnosticSetType) TEST_SUITE_BEGIN("ToString"); @@ -49,7 +47,6 @@ TEST_CASE_FIXTURE(Fixture, "bound_types") TEST_CASE_FIXTURE(Fixture, "free_types") { - ScopedFastFlag sff{FFlag::LuauSolverAgnosticStringification, true}; DOES_NOT_PASS_NEW_SOLVER_GUARD(); CheckResult result = check("local a"); @@ -60,7 +57,6 @@ TEST_CASE_FIXTURE(Fixture, "free_types") TEST_CASE_FIXTURE(Fixture, "free_types_stringify_the_same_regardless_of_solver") { - ScopedFastFlag sff{FFlag::LuauSolverAgnosticStringification, true}; TypeArena a; TypeId t = a.addType(FreeType{getFrontend().globals.globalScope.get(), getFrontend().builtinTypes->neverType, getFrontend().builtinTypes->unknownType}); @@ -70,7 +66,6 @@ TEST_CASE_FIXTURE(Fixture, "free_types_stringify_the_same_regardless_of_solver") TEST_CASE_FIXTURE(Fixture, "cyclic_table") { - ScopedFastFlag sff{FFlag::LuauSolverAgnosticStringification, true}; Type cyclicTable{TypeVariant(TableType())}; TableType* tableOne = getMutable(&cyclicTable); tableOne->props["self"] = {&cyclicTable}; @@ -89,7 +84,6 @@ TEST_CASE_FIXTURE(Fixture, "named_table") TEST_CASE_FIXTURE(Fixture, "empty_table") { - ScopedFastFlag sff{FFlag::LuauSolverAgnosticStringification, true}; CheckResult result = check(R"( local a: {} )"); @@ -104,7 +98,6 @@ TEST_CASE_FIXTURE(Fixture, "empty_table") TEST_CASE_FIXTURE(Fixture, "table_respects_use_line_break") { - ScopedFastFlag sff{FFlag::LuauSolverAgnosticStringification, true}; CheckResult result = check(R"( local a: { prop: string, anotherProp: number, thirdProp: boolean } )"); @@ -146,7 +139,6 @@ TEST_CASE_FIXTURE(Fixture, "long_disjunct_of_nil_is_nil_not_question_mark") TEST_CASE_FIXTURE(Fixture, "metatable") { - ScopedFastFlag sff{FFlag::LuauSolverAgnosticStringification, true}; Type table{TypeVariant(TableType())}; Type metatable{TypeVariant(TableType())}; Type mtv{TypeVariant(MetatableType{&table, &metatable})}; @@ -180,10 +172,6 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "named_metatable_toStringNamedFunction") TEST_CASE_FIXTURE(BuiltinsFixture, "exhaustive_toString_of_cyclic_table") { - ScopedFastFlag sff[] = { - {FFlag::LuauSolverAgnosticStringification, true}, - {FFlag::LuauSolverAgnosticSetType, true}, - }; CheckResult result = check(R"( --!strict local Vec3 = {} @@ -337,7 +325,6 @@ TEST_CASE_FIXTURE(Fixture, "complex_unions_printed_on_multiple_lines") TEST_CASE_FIXTURE(Fixture, "quit_stringifying_table_type_when_length_is_exceeded") { - ScopedFastFlag sff{FFlag::LuauSolverAgnosticStringification, true}; TableType ttv{}; for (char c : std::string("abcdefghijklmno")) ttv.props[std::string(1, c)] = {getBuiltins()->numberType}; @@ -352,7 +339,6 @@ TEST_CASE_FIXTURE(Fixture, "quit_stringifying_table_type_when_length_is_exceeded TEST_CASE_FIXTURE(Fixture, "stringifying_table_type_is_still_capped_when_exhaustive") { - ScopedFastFlag sff{FFlag::LuauSolverAgnosticStringification, true}; TableType ttv{}; for (char c : std::string("abcdefg")) ttv.props[std::string(1, c)] = {getBuiltins()->numberType}; @@ -367,7 +353,6 @@ TEST_CASE_FIXTURE(Fixture, "stringifying_table_type_is_still_capped_when_exhaust TEST_CASE_FIXTURE(Fixture, "quit_stringifying_type_when_length_is_exceeded") { - ScopedFastFlag sff{FFlag::LuauSolverAgnosticStringification, true}; CheckResult result = check(R"( function f0() end function f1(f) return f or f0 end @@ -437,7 +422,6 @@ TEST_CASE_FIXTURE(Fixture, "stringifying_type_is_still_capped_when_exhaustive") TEST_CASE_FIXTURE(Fixture, "stringifying_table_type_correctly_use_matching_table_state_braces") { - ScopedFastFlag sff{FFlag::LuauSolverAgnosticStringification, true}; TableType ttv{TableState::Sealed, TypeLevel{}}; for (char c : std::string("abcdefghij")) ttv.props[std::string(1, c)] = {getBuiltins()->numberType}; @@ -471,7 +455,6 @@ TEST_CASE_FIXTURE(Fixture, "stringifying_cyclic_intersection_type_bails_early") TEST_CASE_FIXTURE(Fixture, "stringifying_array_uses_array_syntax") { - ScopedFastFlag sff{FFlag::LuauSolverAgnosticStringification, true}; TableType ttv{TableState::Sealed, TypeLevel{}}; ttv.indexer = TableIndexer{getBuiltins()->numberType, getBuiltins()->stringType}; @@ -618,7 +601,6 @@ function foo(a, b) return a(b) end TEST_CASE_FIXTURE(Fixture, "toString_the_boundTo_table_type_contained_within_a_TypePack") { - ScopedFastFlag sff{FFlag::LuauSolverAgnosticStringification, true}; Type tv1{TableType{}}; TableType* ttv = getMutable(&tv1); ttv->state = TableState::Sealed; @@ -862,7 +844,6 @@ TEST_CASE_FIXTURE(Fixture, "tostring_unsee_ttv_if_array") TEST_CASE_FIXTURE(Fixture, "tostring_error_mismatch") { - ScopedFastFlag sff{FFlag::LuauSolverAgnosticStringification, true}; CheckResult result = check(R"( --!strict function f1(t: {a : number, b: string, c: {d: string}}) : {a : number, b : string, c : { d : number}} @@ -935,7 +916,6 @@ TEST_CASE_FIXTURE(Fixture, "read_only_properties") TEST_CASE_FIXTURE(Fixture, "cycle_rooted_in_a_pack") { - ScopedFastFlag sff{FFlag::LuauSolverAgnosticStringification, true}; TypeArena arena; TypePackId thePack = arena.addTypePack({getBuiltins()->numberType, getBuiltins()->numberType}); diff --git a/tests/TopoSort.test.cpp b/tests/TopoSort.test.cpp index 02ff6cbe..ad3959c9 100644 --- a/tests/TopoSort.test.cpp +++ b/tests/TopoSort.test.cpp @@ -1,6 +1,5 @@ // This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details #include "Luau/TopoSortStatements.h" -#include "Luau/Transpiler.h" #include "Fixture.h" diff --git a/tests/TypeFunction.test.cpp b/tests/TypeFunction.test.cpp index 230a4887..71399e89 100644 --- a/tests/TypeFunction.test.cpp +++ b/tests/TypeFunction.test.cpp @@ -15,10 +15,8 @@ using namespace Luau; LUAU_FASTFLAG(LuauSolverV2) LUAU_DYNAMIC_FASTINT(LuauTypeFamilyApplicationCartesianProductLimit) LUAU_FASTFLAG(DebugLuauAssertOnForcedConstraint) -LUAU_FASTFLAG(LuauRefineOccursCheckDirectRecursion) LUAU_FASTFLAG(LuauNoMoreComparisonTypeFunctions) -LUAU_FASTFLAG(LuauNameConstraintRestrictRecursiveTypes) -LUAU_FASTFLAG(LuauRawGetHandlesNil) +LUAU_FASTFLAG(LuauBuiltinTypeFunctionsArentGlobal) struct TypeFunctionFixture : Fixture { @@ -1328,8 +1326,6 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "rawget_type_function_works_w_union_type_inde if (!FFlag::LuauSolverV2) return; - ScopedFastFlag sff{FFlag::LuauRawGetHandlesNil, true}; - CheckResult result = check(R"( type MyObject = {a: string, b: number, c: boolean} type rawType = rawget @@ -1346,8 +1342,6 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "rawget_type_function_works_w_union_type_inde if (!FFlag::LuauSolverV2) return; - ScopedFastFlag sff{FFlag::LuauRawGetHandlesNil, true}; - CheckResult result = check(R"( type MyObject = {a: string, b: number, c: boolean} type MyObject2 = {a: number} @@ -1365,8 +1359,6 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "rawget_type_function_works_w_index_metatable if (!FFlag::LuauSolverV2) return; - ScopedFastFlag sff{FFlag::LuauRawGetHandlesNil, true}; - CheckResult result = check(R"( local exampleClass = { Foo = "text", Bar = true } local exampleClass2 = setmetatable({ Foo = 8 }, { __index = exampleClass }) @@ -1400,8 +1392,6 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "rawget_type_function_works_w_queried_key_abs if (!FFlag::LuauSolverV2) return; - ScopedFastFlag sff{FFlag::LuauRawGetHandlesNil, true}; - CheckResult result = check(R"( type MyObject = {a: string} type T = rawget @@ -1757,13 +1747,17 @@ struct TFFixture { TypeArena arena_; NotNull arena{&arena_}; - BuiltinTypes builtinTypes_; NotNull getBuiltins() { return NotNull{&builtinTypes_}; } + NotNull getBuiltinTypeFunctions() + { + return FFlag::LuauBuiltinTypeFunctionsArentGlobal ? NotNull{builtinTypes_.typeFunctions.get()} : NotNull{&builtinTypeFunctions}; + } + ScopePtr globalScope = std::make_shared(getBuiltins()->anyTypePack); InternalErrorReporter ice; @@ -1793,7 +1787,7 @@ TEST_CASE_FIXTURE(TFFixture, "refine") { TypeId g = arena->addType(GenericType{globalScope.get(), Polarity::Negative}); - TypeId refineTy = arena->addType(TypeFunctionInstanceType{builtinTypeFunctions.refineFunc, {g, getBuiltins()->truthyType}}); + TypeId refineTy = arena->addType(TypeFunctionInstanceType{getBuiltinTypeFunctions()->refineFunc, {g, getBuiltins()->truthyType}}); FunctionGraphReductionResult res = reduceTypeFunctions(refineTy, Location{}, tfc); @@ -1809,7 +1803,7 @@ TEST_CASE_FIXTURE(TFFixture, "or<'a, 'b>") TypeId aType = arena->freshType(getBuiltins(), globalScope.get()); TypeId bType = arena->freshType(getBuiltins(), globalScope.get()); - TypeId orType = arena->addType(TypeFunctionInstanceType{builtinTypeFunctions.orFunc, {aType, bType}}); + TypeId orType = arena->addType(TypeFunctionInstanceType{getBuiltinTypeFunctions()->orFunc, {aType, bType}}); FunctionGraphReductionResult res = reduceTypeFunctions(orType, Location{}, tfc); @@ -1821,7 +1815,7 @@ TEST_CASE_FIXTURE(TFFixture, "a_type_function_parameterized_on_generics_is_solve TypeId a = arena->addType(GenericType{"A"}); TypeId b = arena->addType(GenericType{"B"}); - TypeId addTy = arena->addType(TypeFunctionInstanceType{builtinTypeFunctions.addFunc, {a, b}}); + TypeId addTy = arena->addType(TypeFunctionInstanceType{getBuiltinTypeFunctions()->addFunc, {a, b}}); reduceTypeFunctions(addTy, Location{}, tfc); @@ -1836,9 +1830,9 @@ TEST_CASE_FIXTURE(TFFixture, "a_tf_parameterized_on_a_solved_tf_is_solved") TypeId a = arena->addType(GenericType{"A"}); TypeId b = arena->addType(GenericType{"B"}); - TypeId innerAddTy = arena->addType(TypeFunctionInstanceType{builtinTypeFunctions.addFunc, {a, b}}); + TypeId innerAddTy = arena->addType(TypeFunctionInstanceType{getBuiltinTypeFunctions()->addFunc, {a, b}}); - TypeId outerAddTy = arena->addType(TypeFunctionInstanceType{builtinTypeFunctions.addFunc, {builtinTypes_.numberType, innerAddTy}}); + TypeId outerAddTy = arena->addType(TypeFunctionInstanceType{getBuiltinTypeFunctions()->addFunc, {builtinTypes_.numberType, innerAddTy}}); reduceTypeFunctions(outerAddTy, Location{}, tfc); @@ -1850,9 +1844,9 @@ TEST_CASE_FIXTURE(TFFixture, "a_tf_parameterized_on_a_solved_tf_is_solved") TEST_CASE_FIXTURE(TFFixture, "a_tf_parameterized_on_a_stuck_tf_is_stuck") { - TypeId innerAddTy = arena->addType(TypeFunctionInstanceType{builtinTypeFunctions.addFunc, {builtinTypes_.bufferType, builtinTypes_.booleanType}}); + TypeId innerAddTy = arena->addType(TypeFunctionInstanceType{getBuiltinTypeFunctions()->addFunc, {builtinTypes_.bufferType, builtinTypes_.booleanType}}); - TypeId outerAddTy = arena->addType(TypeFunctionInstanceType{builtinTypeFunctions.addFunc, {builtinTypes_.numberType, innerAddTy}}); + TypeId outerAddTy = arena->addType(TypeFunctionInstanceType{getBuiltinTypeFunctions()->addFunc, {builtinTypes_.numberType, innerAddTy}}); reduceTypeFunctions(outerAddTy, Location{}, tfc); @@ -1865,15 +1859,12 @@ TEST_CASE_FIXTURE(TFFixture, "a_tf_parameterized_on_a_stuck_tf_is_stuck") // We want to make sure that `t1 where t1 = refine` becomes `unknown`, not a cyclic type. TEST_CASE_FIXTURE(TFFixture, "reduce_degenerate_refinement") { - ScopedFastFlag sffs[] = { - {FFlag::LuauSolverV2, true}, - {FFlag::LuauRefineOccursCheckDirectRecursion, true}, - }; + ScopedFastFlag sff{FFlag::LuauSolverV2, true}; TypeId root = arena->addType(BlockedType{}); TypeId refinement = arena->addType( TypeFunctionInstanceType{ - builtinTypeFunctions.refineFunc, + getBuiltinTypeFunctions()->refineFunc, { root, builtinTypes_.unknownType, @@ -1888,10 +1879,7 @@ TEST_CASE_FIXTURE(TFFixture, "reduce_degenerate_refinement") TEST_CASE_FIXTURE(Fixture, "generic_type_functions_should_not_get_stuck_or") { - ScopedFastFlag sffs[] = { - {FFlag::LuauSolverV2, true}, - {FFlag::LuauNoMoreComparisonTypeFunctions, false} - }; + ScopedFastFlag sffs[] = {{FFlag::LuauSolverV2, true}, {FFlag::LuauNoMoreComparisonTypeFunctions, false}}; CheckResult result = check(R"( local function init(data) @@ -1904,8 +1892,6 @@ TEST_CASE_FIXTURE(Fixture, "generic_type_functions_should_not_get_stuck_or") TEST_CASE_FIXTURE(TypeFunctionFixture, "recursive_restraint_violation") { - ScopedFastFlag _ = {FFlag::LuauNameConstraintRestrictRecursiveTypes, true}; - CheckResult result = check(R"( type a = {a<{T}>} )"); @@ -1916,8 +1902,6 @@ TEST_CASE_FIXTURE(TypeFunctionFixture, "recursive_restraint_violation") TEST_CASE_FIXTURE(TypeFunctionFixture, "recursive_restraint_violation1") { - ScopedFastFlag _ = {FFlag::LuauNameConstraintRestrictRecursiveTypes, true}; - CheckResult result = check(R"( type b = {b} )"); @@ -1928,8 +1912,6 @@ TEST_CASE_FIXTURE(TypeFunctionFixture, "recursive_restraint_violation1") TEST_CASE_FIXTURE(TypeFunctionFixture, "recursive_restraint_violation2") { - ScopedFastFlag _ = {FFlag::LuauNameConstraintRestrictRecursiveTypes, true}; - CheckResult result = check(R"( type c = {c} )"); @@ -1940,8 +1922,6 @@ TEST_CASE_FIXTURE(TypeFunctionFixture, "recursive_restraint_violation2") TEST_CASE_FIXTURE(TypeFunctionFixture, "recursive_restraint_violation3") { - ScopedFastFlag _ = {FFlag::LuauNameConstraintRestrictRecursiveTypes, true}; - CheckResult result = check(R"( type d = (d) -> () )"); diff --git a/tests/TypeFunction.user.test.cpp b/tests/TypeFunction.user.test.cpp index f875477e..9d20f364 100644 --- a/tests/TypeFunction.user.test.cpp +++ b/tests/TypeFunction.user.test.cpp @@ -9,7 +9,6 @@ using namespace Luau; LUAU_FASTFLAG(LuauSolverV2) LUAU_FASTFLAG(DebugLuauEqSatSimplification) -LUAU_FASTFLAG(LuauTypeFunNoScopeMapRef) LUAU_FASTFLAG(LuauInstantiateResolvedTypeFunctions) TEST_SUITE_BEGIN("UserDefinedTypeFunctionTests"); @@ -2447,7 +2446,6 @@ end TEST_CASE_FIXTURE(BuiltinsFixture, "udtf_fuzz_environment_scope_crash") { ScopedFastFlag newSolver{FFlag::LuauSolverV2, true}; - ScopedFastFlag luauTypeFunNoScopeMapRef{FFlag::LuauTypeFunNoScopeMapRef, true}; CheckResult result = check(R"( local _, running = ... diff --git a/tests/TypeInfer.aliases.test.cpp b/tests/TypeInfer.aliases.test.cpp index e184a106..5a5eaf74 100644 --- a/tests/TypeInfer.aliases.test.cpp +++ b/tests/TypeInfer.aliases.test.cpp @@ -10,7 +10,6 @@ using namespace Luau; LUAU_FASTFLAG(LuauSolverV2) -LUAU_FASTFLAG(LuauSolverAgnosticStringification) LUAU_FASTFLAG(LuauInitializeDefaultGenericParamsAtProgramPoint) LUAU_FASTFLAG(LuauAddErrorCaseForIncompatibleTypePacks) @@ -309,7 +308,6 @@ TEST_CASE_FIXTURE(Fixture, "dont_stop_typechecking_after_reporting_duplicate_typ TEST_CASE_FIXTURE(Fixture, "stringify_type_alias_of_recursive_template_table_type") { - ScopedFastFlag sff{FFlag::LuauSolverAgnosticStringification, true}; CheckResult result = check(R"( type Table = { a: T } @@ -327,7 +325,6 @@ TEST_CASE_FIXTURE(Fixture, "stringify_type_alias_of_recursive_template_table_typ TEST_CASE_FIXTURE(Fixture, "stringify_type_alias_of_recursive_template_table_type2") { - ScopedFastFlag sff{FFlag::LuauSolverAgnosticStringification, true}; CheckResult result = check(R"( type Table = { a: T } @@ -343,10 +340,9 @@ TEST_CASE_FIXTURE(Fixture, "stringify_type_alias_of_recursive_template_table_typ CHECK_EQ(getBuiltins()->numberType, tm->givenType); } -#if 0 // CLI-169898: temporarily disabled for stack overflow in unoptimized build -// Check that recursive intersection type doesn't generate an OOM TEST_CASE_FIXTURE(Fixture, "cli_38393_recursive_intersection_oom") { + // Check that recursive intersection type doesn't generate an OOM CheckResult result = check(R"( function _(l0:(t0)&((t0)&(((t0)&((t0)->()))->(typeof(_),typeof(# _)))),l39,...):any end @@ -354,7 +350,6 @@ TEST_CASE_FIXTURE(Fixture, "cli_38393_recursive_intersection_oom") _(_) )"); } -#endif TEST_CASE_FIXTURE(Fixture, "type_alias_fwd_declaration_is_precise") { @@ -591,7 +586,6 @@ type Cool = typeof(c) TEST_CASE_FIXTURE(BuiltinsFixture, "type_alias_of_an_imported_recursive_type") { - ScopedFastFlag sff{FFlag::LuauSolverAgnosticStringification, true}; fileResolver.source["game/A"] = R"( export type X = { a: number, b: X? } @@ -618,7 +612,6 @@ type X = Import.X TEST_CASE_FIXTURE(BuiltinsFixture, "type_alias_of_an_imported_recursive_generic_type") { - ScopedFastFlag sff{FFlag::LuauSolverAgnosticStringification, true}; fileResolver.source["game/A"] = R"( export type X = { a: T, b: U, C: X? } @@ -840,7 +833,6 @@ TEST_CASE_FIXTURE(Fixture, "generic_typevars_are_not_considered_to_escape_their_ */ TEST_CASE_FIXTURE(Fixture, "forward_declared_alias_is_not_clobbered_by_prior_unification_with_any") { - ScopedFastFlag sff{FFlag::LuauSolverAgnosticStringification, true}; CheckResult result = check(R"( local function x() diff --git a/tests/TypeInfer.annotations.test.cpp b/tests/TypeInfer.annotations.test.cpp index f3ec6d76..3b81b3d0 100644 --- a/tests/TypeInfer.annotations.test.cpp +++ b/tests/TypeInfer.annotations.test.cpp @@ -885,12 +885,13 @@ TEST_CASE_FIXTURE(Fixture, "instantiate_type_fun_should_not_trip_rbxassert") LUAU_REQUIRE_NO_ERRORS(result); } -#if 0 // This is because, after visiting all nodes in a block, we check if each type alias still points to a FreeType. // Doing it that way is wrong, but I also tried to make typeof(x) return a BoundType, with no luck. // Not important enough to fix today. TEST_CASE_FIXTURE(Fixture, "pulling_a_type_from_value_dont_falsely_create_occurs_check_failed") { + ScopedFastFlag _{FFlag::LuauSolverV2, true}; + CheckResult result = check(R"( function f(x) type T = typeof(x) @@ -899,7 +900,6 @@ TEST_CASE_FIXTURE(Fixture, "pulling_a_type_from_value_dont_falsely_create_occurs LUAU_REQUIRE_NO_ERRORS(result); } -#endif TEST_CASE_FIXTURE(Fixture, "occurs_check_on_cyclic_union_type") { diff --git a/tests/TypeInfer.builtins.test.cpp b/tests/TypeInfer.builtins.test.cpp index 576b860f..696b8dde 100644 --- a/tests/TypeInfer.builtins.test.cpp +++ b/tests/TypeInfer.builtins.test.cpp @@ -12,8 +12,11 @@ using namespace Luau; LUAU_FASTFLAG(LuauSolverV2) LUAU_FASTFLAG(LuauTableCloneClonesType3) LUAU_FASTFLAG(LuauNoScopeShallNotSubsumeAll) -LUAU_FASTFLAG(LuauSolverAgnosticStringification) -LUAU_FASTFLAG(LuauSolverAgnosticSetType) +LUAU_FASTFLAG(LuauSubtypingPrimitiveAndGenericTableTypes) +LUAU_FASTFLAG(LuauSubtypingReportGenericBoundMismatches2) +LUAU_FASTFLAG(LuauVectorLerp) +LUAU_FASTFLAG(LuauCompileVectorLerp) +LUAU_FASTFLAG(LuauTypeCheckerVectorLerp2) TEST_SUITE_BEGIN("BuiltinTests"); @@ -372,7 +375,6 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "setmetatable_unpacks_arg_types_correctly") TEST_CASE_FIXTURE(BuiltinsFixture, "setmetatable_on_union_of_tables") { - ScopedFastFlag sff{FFlag::LuauSolverAgnosticStringification, true}; CheckResult result = check(R"( type A = {tag: "A", x: number} type B = {tag: "B", y: string} @@ -417,7 +419,6 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "table_insert_correctly_infers_type_of_array_ TEST_CASE_FIXTURE(BuiltinsFixture, "table_pack") { - ScopedFastFlag sff{FFlag::LuauSolverAgnosticStringification, true}; CheckResult result = check(R"( local t = table.pack(1, "foo", true) )"); @@ -428,7 +429,6 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "table_pack") TEST_CASE_FIXTURE(BuiltinsFixture, "table_pack_variadic") { - ScopedFastFlag sff{FFlag::LuauSolverAgnosticStringification, true}; CheckResult result = check(R"( --!strict function f(): (string, ...number) @@ -444,7 +444,6 @@ local t = table.pack(f()) TEST_CASE_FIXTURE(BuiltinsFixture, "table_pack_reduce") { - ScopedFastFlag sff{FFlag::LuauSolverAgnosticStringification, true}; CheckResult result = check(R"( local t = table.pack(1, 2, true) )"); @@ -1106,7 +1105,6 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "assert_returns_false_and_string_iff_it_knows TEST_CASE_FIXTURE(BuiltinsFixture, "table_freeze_is_generic") { - ScopedFastFlag sff{FFlag::LuauSolverAgnosticStringification, true}; CheckResult result = check(R"( local t1: {a: number} = {a = 42} local t2: {b: string} = {b = "hello"} @@ -1587,9 +1585,7 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "string_find_should_not_crash") TEST_CASE_FIXTURE(BuiltinsFixture, "table_dot_clone_type_states") { - ScopedFastFlag sff[] = { - {FFlag::LuauSolverAgnosticStringification, true}, {FFlag::LuauSolverAgnosticSetType, true}, {FFlag::LuauTableCloneClonesType3, true} - }; + ScopedFastFlag sff{FFlag::LuauTableCloneClonesType3, true}; CheckResult result = check(R"( local t1 = {} t1.x = 5 @@ -1729,7 +1725,7 @@ table.insert(1::any, 2::any) TEST_CASE_FIXTURE(BuiltinsFixture, "table_insert_requires_all_fields") { - ScopedFastFlag _{FFlag::LuauNoScopeShallNotSubsumeAll, true}; + ScopedFastFlag _[] = {{FFlag::LuauNoScopeShallNotSubsumeAll, true}, {FFlag::LuauSubtypingReportGenericBoundMismatches2, true}}; CheckResult result = check(R"( local function huh(): { { x: number, y: string } } @@ -1780,4 +1776,63 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "read_refinements_on_persistent_tables_known_ CHECK_EQ("\"lol\"", toString(requireTypeAtPosition(Position{4, 23}))); } +TEST_CASE_FIXTURE(BuiltinsFixture, "next_with_refined_any") +{ + ScopedFastFlag lsv2{FFlag::LuauSolverV2, true}; + ScopedFastFlag sff{FFlag::LuauSubtypingPrimitiveAndGenericTableTypes, true}; + + CheckResult result = check(R"( + --!strict + local t: any = {"hello", "world"} + if type(t) == "table" and next(t) then + local foo, bar = next(t) + local _ = foo + local _ = bar + end + )"); + + LUAU_REQUIRE_NO_ERRORS(result); + + CHECK_EQ(toString(requireTypeAtPosition(Position{5, 23})), "unknown?"); + CHECK_EQ(toString(requireTypeAtPosition(Position{6, 23})), "unknown"); +} + +TEST_CASE_FIXTURE(BuiltinsFixture, "pairs_with_refined_any") +{ + ScopedFastFlag lsv2{FFlag::LuauSolverV2, true}; + ScopedFastFlag sff{FFlag::LuauSubtypingPrimitiveAndGenericTableTypes, true}; + + CheckResult result = check(R"( + --!strict + local t: any = {"hello", "world"} + if type(t) == "table" and pairs(t) then + local foo, bar, lorem = pairs(t) + local _ = foo + local _ = bar + local _ = lorem + end + )"); + + LUAU_REQUIRE_NO_ERRORS(result); + + CHECK_EQ(toString(requireTypeAtPosition(Position{5, 23})), "({+ [unknown]: unknown +}, unknown?) -> (unknown?, unknown)"); + CHECK_EQ(toString(requireTypeAtPosition(Position{6, 23})), "{+ [unknown]: unknown +}"); + CHECK_EQ(toString(requireTypeAtPosition(Position{7, 23})), "nil"); +} + +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) + end + )")); +} + TEST_SUITE_END(); diff --git a/tests/TypeInfer.classes.test.cpp b/tests/TypeInfer.classes.test.cpp index 40f6b2bd..bb4fd860 100644 --- a/tests/TypeInfer.classes.test.cpp +++ b/tests/TypeInfer.classes.test.cpp @@ -15,8 +15,8 @@ using namespace Luau; using std::nullopt; LUAU_FASTFLAG(LuauSolverV2) -LUAU_FASTFLAG(LuauMorePreciseExternTableRelation) -LUAU_FASTFLAG(LuauPushTypeConstraint) +LUAU_FASTFLAG(LuauPushTypeConstraint2) +LUAU_FASTFLAG(LuauExternTableIndexersIntersect) TEST_SUITE_BEGIN("TypeInferExternTypes"); @@ -920,10 +920,7 @@ end TEST_CASE_FIXTURE(Fixture, "extern_type_check_missing_key") { - ScopedFastFlag sffs[] = { - {FFlag::LuauSolverV2, true}, - {FFlag::LuauMorePreciseExternTableRelation, true}, - }; + ScopedFastFlag sff{FFlag::LuauSolverV2, true}; loadDefinition(R"( declare extern type Foobar with @@ -956,10 +953,7 @@ TEST_CASE_FIXTURE(Fixture, "extern_type_check_missing_key") TEST_CASE_FIXTURE(Fixture, "extern_type_check_present_key_in_superclass") { - ScopedFastFlag sffs[] = { - {FFlag::LuauSolverV2, true}, - {FFlag::LuauMorePreciseExternTableRelation, true}, - }; + ScopedFastFlag sff{FFlag::LuauSolverV2, true}; loadDefinition(R"( declare extern type FoobarParent with @@ -992,10 +986,7 @@ TEST_CASE_FIXTURE(Fixture, "extern_type_check_present_key_in_superclass") TEST_CASE_FIXTURE(BuiltinsFixture, "extern_type_check_key_becomes_never") { - ScopedFastFlag sffs[] = { - {FFlag::LuauSolverV2, true}, - {FFlag::LuauMorePreciseExternTableRelation, true}, - }; + ScopedFastFlag sff{FFlag::LuauSolverV2, true}; loadDefinition(R"( declare extern type Foobar with @@ -1020,10 +1011,7 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "extern_type_check_key_becomes_never") TEST_CASE_FIXTURE(BuiltinsFixture, "extern_type_check_key_becomes_intersection") { - ScopedFastFlag sffs[] = { - {FFlag::LuauSolverV2, true}, - {FFlag::LuauMorePreciseExternTableRelation, true}, - }; + ScopedFastFlag sff{FFlag::LuauSolverV2, true}; loadDefinition(R"( declare extern type Foobar with @@ -1046,8 +1034,7 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "extern_type_check_key_superset") { ScopedFastFlag sffs[] = { {FFlag::LuauSolverV2, true}, - {FFlag::LuauPushTypeConstraint, true}, - {FFlag::LuauMorePreciseExternTableRelation, true}, + {FFlag::LuauPushTypeConstraint2, true}, }; loadDefinition(R"( @@ -1069,10 +1056,7 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "extern_type_check_key_superset") TEST_CASE_FIXTURE(BuiltinsFixture, "extern_type_check_key_idempotent") { - ScopedFastFlag sffs[] = { - {FFlag::LuauSolverV2, true}, - {FFlag::LuauMorePreciseExternTableRelation, true}, - }; + ScopedFastFlag sff{FFlag::LuauSolverV2, true}; loadDefinition(R"( declare extern type Foobar with @@ -1091,4 +1075,45 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "extern_type_check_key_idempotent") CHECK_EQ("(Foobar) -> Foobar", toString(requireType("update"))); } +TEST_CASE_FIXTURE(BuiltinsFixture, "extern_type_intersect_with_table_indexer") +{ + ScopedFastFlag sffs[] = { + {FFlag::LuauSolverV2, true}, + {FFlag::LuauExternTableIndexersIntersect, true}, + }; + + LUAU_REQUIRE_NO_ERRORS(check(R"( + local function f(obj: { [any]: any }, functionName: string) + if typeof(obj) == "userdata" then + local _ = obj[functionName] + end + end + )")); + + CHECK_EQ("userdata & { [any]: any }", toString(requireTypeAtPosition({3, 28}))); +} + +TEST_CASE_FIXTURE(BuiltinsFixture, "extern_type_with_indexer_intersect_table") +{ + ScopedFastFlag sffs[] = { + {FFlag::LuauSolverV2, true}, + {FFlag::LuauExternTableIndexersIntersect, true}, + }; + + loadDefinition(R"( + declare extern type Foobar with + [string]: unknown + end + )"); + + LUAU_REQUIRE_NO_ERRORS(check(R"( + local function update(obj: Foobar) + assert(typeof(obj.Baz) == "number") + return obj + end + )")); + + CHECK_EQ("(Foobar) -> Foobar & { read Baz: number }", toString(requireType("update"))); +} + TEST_SUITE_END(); diff --git a/tests/TypeInfer.definitions.test.cpp b/tests/TypeInfer.definitions.test.cpp index 22a0d2ed..eb24d994 100644 --- a/tests/TypeInfer.definitions.test.cpp +++ b/tests/TypeInfer.definitions.test.cpp @@ -12,6 +12,7 @@ using namespace Luau; LUAU_FASTFLAG(LuauNoMoreComparisonTypeFunctions) LUAU_FASTINT(LuauTypeInferRecursionLimit) +LUAU_FASTFLAG(LuauNoOrderingTypeFunctions) TEST_SUITE_BEGIN("DefinitionTests"); @@ -575,6 +576,7 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "cli_142285_reduce_minted_union_func") ScopedFastFlag sff[] = { {FFlag::LuauSolverV2, true}, {FFlag::LuauNoMoreComparisonTypeFunctions, true}, + {FFlag::LuauNoOrderingTypeFunctions, true}, }; CheckResult result = check(R"( @@ -596,10 +598,10 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "cli_142285_reduce_minted_union_func") end )"); LUAU_REQUIRE_ERROR_COUNT(1, result); - // There are three errors in the above snippet, but they should all be where - // clause needed errors. - for (const auto& e : result.errors) - CHECK(get(e)); + auto err1 = get(result.errors[0]); + REQUIRE(err1); + CHECK_EQ(err1->suggestedToAnnotate, "item"); + CHECK_EQ(err1->op, AstExprBinary::Op::CompareLe); } TEST_CASE_FIXTURE(Fixture, "vector3_overflow") diff --git a/tests/TypeInfer.functions.test.cpp b/tests/TypeInfer.functions.test.cpp index 96f76a53..f7204b46 100644 --- a/tests/TypeInfer.functions.test.cpp +++ b/tests/TypeInfer.functions.test.cpp @@ -6,7 +6,6 @@ #include "Luau/Scope.h" #include "Luau/TypeInfer.h" #include "Luau/Type.h" -#include "Luau/VisitType.h" #include "ClassFixture.h" #include "Fixture.h" @@ -24,14 +23,14 @@ LUAU_FASTINT(LuauTarjanChildLimit) LUAU_FASTFLAG(DebugLuauEqSatSimplification) LUAU_FASTFLAG(LuauCollapseShouldNotCrash) LUAU_FASTFLAG(LuauFormatUseLastPosition) -LUAU_FASTFLAG(LuauSolverAgnosticStringification) -LUAU_FASTFLAG(LuauSubtypingGenericsDoesntUseVariance) -LUAU_FASTFLAG(LuauUnifyShortcircuitSomeIntersectionsAndUnions) LUAU_FASTFLAG(LuauSubtypingReportGenericBoundMismatches2) -LUAU_FASTFLAG(LuauSolverAgnosticStringification) -LUAU_FASTFLAG(LuauSubtypingGenericPacksDoesntUseVariance) -LUAU_FASTFLAG(LuauReturnMappedGenericPacksFromSubtyping2) LUAU_FASTFLAG(LuauFixNilRightPad) +LUAU_FASTFLAG(LuauNoScopeShallNotSubsumeAll) +LUAU_FASTFLAG(LuauNoOrderingTypeFunctions) +LUAU_FASTFLAG(LuauPushTypeConstraint2) +LUAU_FASTFLAG(LuauPushTypeConstraintIntersection) +LUAU_FASTFLAG(LuauPushTypeConstraintSingleton) +LUAU_FASTFLAG(LuauPushTypeConstraintLambdas) TEST_SUITE_BEGIN("TypeInferFunctions"); @@ -271,7 +270,10 @@ TEST_CASE_FIXTURE(Fixture, "list_only_alternative_overloads_that_match_argument_ REQUIRE(ei); if (FFlag::LuauSolverV2) - CHECK("Available overloads: (number) -> number; (number) -> string; and (number, number) -> number" == ei->message); + { + // TODO CLI-170535: Improve message so we show overloads with matching and non-matching arities + CHECK("Available overloads: (number) -> number; and (number) -> string" == ei->message); + } else CHECK_EQ("Other overloads are also not viable: (number) -> string", ei->message); } @@ -1375,7 +1377,6 @@ f(function(x) return x * 2 end) TEST_CASE_FIXTURE(BuiltinsFixture, "infer_generic_function_function_argument") { - ScopedFastFlag sff{FFlag::LuauSolverAgnosticStringification, true}; // FIXME: CLI-116133 bidirectional type inference needs to push expected types in for higher-order function calls DOES_NOT_PASS_NEW_SOLVER_GUARD(); @@ -1439,6 +1440,9 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "infer_generic_lib_function_function_argument { ScopedFastFlag sffs[] = { {FFlag::LuauSolverV2, true}, + {FFlag::LuauSubtypingReportGenericBoundMismatches2, true}, + {FFlag::LuauNoOrderingTypeFunctions, true}, + {FFlag::LuauNoScopeShallNotSubsumeAll, true}, }; CheckResult result = check(R"( @@ -1446,14 +1450,10 @@ local a = {{x=4}, {x=7}, {x=1}} table.sort(a, function(x, y) return x.x < y.x end) )"); - if (FFlag::LuauSubtypingReportGenericBoundMismatches2) - { - // FIXME CLI-161355 - LUAU_REQUIRE_ERROR_COUNT(1, result); - CHECK(get(result.errors[0])); - } - else - LUAU_REQUIRE_NO_ERRORS(result); + // FIXME CLI-161355 + LUAU_REQUIRE_ERROR_COUNT(2, result); + CHECK(get(result.errors[0])); + CHECK(get(result.errors[1])); } TEST_CASE_FIXTURE(Fixture, "variadic_any_is_compatible_with_a_generic_TypePack") @@ -1958,8 +1958,8 @@ TEST_CASE_FIXTURE(Fixture, "dont_infer_parameter_types_for_functions_from_their_ if (FFlag::LuauSolverV2) { LUAU_CHECK_NO_ERRORS(result); - if (!FFlag::LuauSubtypingGenericsDoesntUseVariance) // FIXME CLI-162439, the below fails on Linux with the flag on - CHECK("({ read p: { read q: a } }) -> (a & ~(false?))?" == toString(requireType("g"))); + // FIXME CLI-162439, the below fails on Linux with the flag on + // CHECK("({ read p: { read q: a } }) -> (a & ~(false?))?" == toString(requireType("g"))); } else { @@ -2241,7 +2241,6 @@ TEST_CASE_FIXTURE(Fixture, "inner_frees_become_generic_in_dcr") TEST_CASE_FIXTURE(Fixture, "function_exprs_are_generalized_at_signature_scope_not_enclosing") { - ScopedFastFlag sff{FFlag::LuauSolverAgnosticStringification, true}; CheckResult result = check(R"( local foo local bar @@ -2265,7 +2264,6 @@ TEST_CASE_FIXTURE(Fixture, "function_exprs_are_generalized_at_signature_scope_no TEST_CASE_FIXTURE(BuiltinsFixture, "param_1_and_2_both_takes_the_same_generic_but_their_arguments_are_incompatible") { - ScopedFastFlag sff{FFlag::LuauSolverAgnosticStringification, true}; CheckResult result = check(R"( local function foo(x: a, y: a?) return x @@ -2358,7 +2356,6 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "param_1_and_2_both_takes_the_same_generic_bu TEST_CASE_FIXTURE(Fixture, "attempt_to_call_an_intersection_of_tables") { - ScopedFastFlag sff{FFlag::LuauSolverAgnosticStringification, true}; CheckResult result = check(R"( local function f(t: { x: number } & { y: string }) t() @@ -2390,11 +2387,7 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "attempt_to_call_an_intersection_of_tables_wi TEST_CASE_FIXTURE(Fixture, "generic_packs_are_not_variadic") { - ScopedFastFlag sffs[] = { - {FFlag::LuauSolverV2, true}, - {FFlag::LuauSubtypingGenericPacksDoesntUseVariance, true}, - {FFlag::LuauReturnMappedGenericPacksFromSubtyping2, true}, - }; + ScopedFastFlag sff{FFlag::LuauSolverV2, true}; CheckResult result = check(R"( local function apply(f: (a, b...) -> c..., x: a) @@ -2609,19 +2602,22 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "tf_suggest_arg_type") if (!FFlag::LuauSolverV2) return; + ScopedFastFlag _{FFlag::LuauNoOrderingTypeFunctions, true}; + CheckResult result = check(R"( function fib(n, u) return (n or u) and (n < u and n + fib(n,u)) end )"); - LUAU_REQUIRE_ERROR_COUNT(1, result); - auto err = get(result.errors.back()); - LUAU_ASSERT(err); - CHECK("number" == toString(err->recommendedReturn)); - REQUIRE(err->recommendedArgs.size() == 2); - CHECK("number" == toString(err->recommendedArgs[0].second)); - CHECK("number" == toString(err->recommendedArgs[1].second)); + LUAU_REQUIRE_ERROR_COUNT(2, result); + CHECK(get(result.errors[0])); + auto err2 = get(result.errors[1]); + LUAU_ASSERT(err2); + CHECK("number" == toString(err2->recommendedReturn)); + REQUIRE(err2->recommendedArgs.size() == 2); + CHECK("number" == toString(err2->recommendedArgs[0].second)); + CHECK("number" == toString(err2->recommendedArgs[1].second)); } TEST_CASE_FIXTURE(BuiltinsFixture, "tf_suggest_arg_type_2") @@ -3271,8 +3267,6 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "function_calls_should_not_crash") TEST_CASE_FIXTURE(BuiltinsFixture, "unnecessary_nil_in_lower_bound_of_generic") { - ScopedFastFlag _{FFlag::LuauUnifyShortcircuitSomeIntersectionsAndUnions, true}; - CheckResult result = check( Mode::Nonstrict, R"( @@ -3324,4 +3318,62 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "oss_1640") )")); } +TEST_CASE_FIXTURE(BuiltinsFixture, "oss_1854") +{ + LUAU_REQUIRE_NO_ERRORS(check(R"( + --!strict + local function bug() + local counter = 1 + local work = buffer.create(64) + local function get_block() + buffer.writeu32(work, 48, counter) + counter = (counter + 1) % 0x100000000 + return work + end + end + )")); +} + +TEST_CASE_FIXTURE(Fixture, "cli_119545_pass_lambda_inside_table") +{ + ScopedFastFlag _{FFlag::LuauSolverV2, true}; + + LUAU_REQUIRE_NO_ERRORS(check(R"( + --!strict + type foo1 = { foo: (number) -> () } + type foo2 = { read foo: (number) -> () } + local function bar1(foo: foo1) end + local function bar2(foo: foo2) end + + local baz = { foo = function(number: number) end, } + bar1(baz) + bar2(baz) + )")); +} + +TEST_CASE_FIXTURE(Fixture, "oss_2065_bidirectional_inference_function_call") +{ + ScopedFastFlag sffs[] = { + {FFlag::LuauSolverV2, true}, + {FFlag::LuauPushTypeConstraint2, true}, + {FFlag::LuauPushTypeConstraintLambdas, true}, + {FFlag::LuauPushTypeConstraintIntersection, true}, + {FFlag::DebugLuauAssertOnForcedConstraint, true}, + }; + + LUAU_REQUIRE_NO_ERRORS(check(R"( + local function foo(callback: () -> (() -> ())?) + end + + local someCondition: boolean = true + + foo(function() + if someCondition then + return nil + end + return function() end + end) + )")); +} + TEST_SUITE_END(); diff --git a/tests/TypeInfer.generics.test.cpp b/tests/TypeInfer.generics.test.cpp index 91ddb335..59048485 100644 --- a/tests/TypeInfer.generics.test.cpp +++ b/tests/TypeInfer.generics.test.cpp @@ -9,14 +9,9 @@ LUAU_FASTFLAG(LuauInstantiateInSubtyping) LUAU_FASTFLAG(LuauSolverV2) -LUAU_FASTFLAG(LuauReturnMappedGenericPacksFromSubtyping2) LUAU_FASTFLAG(LuauIntersectNotNil) LUAU_FASTFLAG(DebugLuauAssertOnForcedConstraint) -LUAU_FASTFLAG(LuauContainsAnyGenericFollowBeforeChecking) -LUAU_FASTFLAG(LuauSubtypingGenericsDoesntUseVariance) LUAU_FASTFLAG(LuauSubtypingReportGenericBoundMismatches2) -LUAU_FASTFLAG(LuauSolverAgnosticStringification) -LUAU_FASTFLAG(LuauSubtypingGenericPacksDoesntUseVariance) LUAU_FASTFLAG(DebugLuauStringSingletonBasedOnQuotes) LUAU_FASTFLAG(LuauSubtypingUnionsAndIntersectionsInGenericBounds) @@ -68,7 +63,6 @@ TEST_CASE_FIXTURE(Fixture, "check_generic_local_function2") TEST_CASE_FIXTURE(Fixture, "unions_and_generics") { - ScopedFastFlag sff{FFlag::LuauSolverAgnosticStringification, true}; CheckResult result = check(R"( type foo = (T | {T}) -> T local foo = (nil :: any) :: foo @@ -903,7 +897,6 @@ end TEST_CASE_FIXTURE(Fixture, "generic_functions_should_be_memory_safe") { - ScopedFastFlag sff{FFlag::LuauSolverAgnosticStringification, true}; CheckResult result = check(R"( --!strict -- At one point this produced a UAF @@ -999,8 +992,6 @@ local TheDispatcher: Dispatcher = { TEST_CASE_FIXTURE(Fixture, "generic_argument_count_too_few") { - ScopedFastFlag _{FFlag::LuauReturnMappedGenericPacksFromSubtyping2, true}; - CheckResult result = check(R"( function test(a: number) return 1 @@ -1027,8 +1018,6 @@ wrapper(test) TEST_CASE_FIXTURE(Fixture, "generic_argument_count_too_many") { - ScopedFastFlag _{FFlag::LuauReturnMappedGenericPacksFromSubtyping2, true}; - CheckResult result = check(R"( function test2(a: number, b: string) return 1 @@ -1071,8 +1060,6 @@ wrapper(test2, 1, "") TEST_CASE_FIXTURE(Fixture, "generic_argument_pack_type_inferred_from_return") { - ScopedFastFlag _{FFlag::LuauReturnMappedGenericPacksFromSubtyping2, true}; - CheckResult result = check(R"( function test2(a: number) return "hello" @@ -1101,8 +1088,6 @@ wrapper(test2, 1) TEST_CASE_FIXTURE(Fixture, "generic_argument_pack_type_inferred_from_return_no_error") { - ScopedFastFlag _{FFlag::LuauReturnMappedGenericPacksFromSubtyping2, true}; - CheckResult result = check(R"( function test2(a: number) return "hello" @@ -1119,8 +1104,6 @@ wrapper(test2, "hello") TEST_CASE_FIXTURE(Fixture, "nested_generic_argument_type_packs") { - ScopedFastFlag _{FFlag::LuauReturnMappedGenericPacksFromSubtyping2, true}; - CheckResult result = check(R"( function test2(a: number) return 3 @@ -1451,7 +1434,6 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "infer_generic_function_function_argument_2") TEST_CASE_FIXTURE(BuiltinsFixture, "infer_generic_function_function_argument_3") { - ScopedFastFlag sff{FFlag::LuauSolverAgnosticStringification, true}; CheckResult result = check(R"( local function foldl(arr: {a}, init: b, f: (b, a) -> b) local r = init @@ -1485,10 +1467,7 @@ TEST_CASE_FIXTURE(Fixture, "infer_generic_function_function_argument_overloaded_ TEST_CASE_FIXTURE(Fixture, "infer_generic_function_function_overloaded_pt_2") { - ScopedFastFlag _[] = { - {FFlag::LuauSubtypingReportGenericBoundMismatches2, true}, - {FFlag::LuauSubtypingGenericsDoesntUseVariance, true}, - }; + ScopedFastFlag _{FFlag::LuauSubtypingReportGenericBoundMismatches2, true}; CheckResult result = check(R"( local g12: ((T, (T) -> T) -> T) & ((T, T, (T, T) -> T) -> T) @@ -1549,7 +1528,7 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "do_not_infer_generic_functions") TEST_CASE_FIXTURE(BuiltinsFixture, "do_not_infer_generic_functions_2") { - ScopedFastFlag _[] = {{FFlag::LuauSolverV2, true}, {FFlag::LuauSubtypingGenericsDoesntUseVariance, true}}; + ScopedFastFlag _{FFlag::LuauSolverV2, true}; CheckResult result = check(R"( type t = (a, a, (a, a) -> a) -> a @@ -1884,7 +1863,6 @@ end TEST_CASE_FIXTURE(BuiltinsFixture, "follow_bound_type_packs_in_generic_type_visitor") { - ScopedFastFlag _{FFlag::LuauContainsAnyGenericFollowBeforeChecking, true}; // Note: we just need this test not to crash check(R"( function (_(_,_,nil)) @@ -1899,8 +1877,6 @@ end TEST_CASE_FIXTURE(BuiltinsFixture, "generic_packs_in_contravariant_position") { - ScopedFastFlag sff{FFlag::LuauSubtypingGenericPacksDoesntUseVariance, true}; - CheckResult result = check(R"( function f(foo: (A) -> ()): () end function g(...: B...): () end @@ -1912,8 +1888,6 @@ f(g) TEST_CASE_FIXTURE(BuiltinsFixture, "generic_packs_in_contravariant_position_2") { - ScopedFastFlag sff{FFlag::LuauSubtypingGenericPacksDoesntUseVariance, true}; - CheckResult result = check(R"( function f(foo: (number) -> (number)): () end type T = (A...) -> A... @@ -1926,8 +1900,6 @@ f(t) TEST_CASE_FIXTURE(BuiltinsFixture, "generic_packs_in_contravariant_position_3") { - ScopedFastFlag sff{FFlag::LuauSubtypingGenericPacksDoesntUseVariance, true}; - CheckResult result = check(R"( function f(foo: (B...) -> B...): () end type T = (A...) -> A... @@ -1941,7 +1913,6 @@ f(t) TEST_CASE_FIXTURE(BuiltinsFixture, "generic_packs_in_contravariant_position_4") { ScopedFastFlag sff1{FFlag::LuauSolverV2, true}; - ScopedFastFlag sff2{FFlag::LuauSubtypingGenericPacksDoesntUseVariance, true}; CheckResult result = check(R"( function f(foo: (A...) -> A...): () end @@ -1955,8 +1926,6 @@ f(t) TEST_CASE_FIXTURE(BuiltinsFixture, "generic_packs_in_contravariant_position_5") { - ScopedFastFlag sff{FFlag::LuauSubtypingGenericPacksDoesntUseVariance, true}; - CheckResult result = check(R"( function f(foo: (number) -> number): () end type T = (A...) -> number @@ -1969,8 +1938,6 @@ f(t) TEST_CASE_FIXTURE(BuiltinsFixture, "generic_packs_in_contravariant_position_6") { - ScopedFastFlag sff{FFlag::LuauSubtypingGenericPacksDoesntUseVariance, true}; - CheckResult result = check(R"( function f(foo: (...number) -> number): () end type T = (A...) -> number @@ -1983,8 +1950,6 @@ f(t) TEST_CASE_FIXTURE(BuiltinsFixture, "generic_packs_in_contravariant_position_7") { - ScopedFastFlag sff{FFlag::LuauSubtypingGenericPacksDoesntUseVariance, true}; - CheckResult result = check(R"( function f(foo: () -> ()): () end type T = () -> A... @@ -1997,8 +1962,6 @@ f(t) TEST_CASE_FIXTURE(BuiltinsFixture, "generic_packs_in_contravariant_position_8") { - ScopedFastFlag sff{FFlag::LuauSubtypingGenericPacksDoesntUseVariance, true}; - CheckResult result = check(R"( function f(foo: () -> ()): () end type T = (A...) -> A... @@ -2012,7 +1975,6 @@ f(t) TEST_CASE_FIXTURE(BuiltinsFixture, "nested_generic_packs") { ScopedFastFlag sff{FFlag::LuauSolverV2, true}; - ScopedFastFlag sff1{FFlag::LuauSubtypingGenericPacksDoesntUseVariance, true}; CheckResult result = check(R"( type T = (A...) -> ((A...) -> ()) @@ -2026,7 +1988,7 @@ local u: U = t TEST_CASE_FIXTURE(Fixture, "ensure_that_invalid_generic_instantiations_error") { - ScopedFastFlag _[] = {{FFlag::LuauSubtypingGenericsDoesntUseVariance, true}, {FFlag::LuauSubtypingReportGenericBoundMismatches2, true}}; + ScopedFastFlag _{FFlag::LuauSubtypingReportGenericBoundMismatches2, true}; CheckResult res = check(R"( local func: (T, (T) -> ()) -> () = nil :: any local foobar: (number) -> () = nil :: any @@ -2042,7 +2004,7 @@ TEST_CASE_FIXTURE(Fixture, "ensure_that_invalid_generic_instantiations_error") TEST_CASE_FIXTURE(Fixture, "ensure_that_invalid_generic_instantiations_error_1") { - ScopedFastFlag _[] = {{FFlag::LuauSubtypingGenericsDoesntUseVariance, true}, {FFlag::LuauSubtypingReportGenericBoundMismatches2, true}}; + ScopedFastFlag _{FFlag::LuauSubtypingReportGenericBoundMismatches2, true}; CheckResult res = check(R"( --!strict @@ -2064,8 +2026,6 @@ TEST_CASE_FIXTURE(Fixture, "ensure_that_invalid_generic_instantiations_error_1") TEST_CASE_FIXTURE(BuiltinsFixture, "xpcall_should_work_with_generics") { - ScopedFastFlag _{FFlag::LuauSubtypingGenericPacksDoesntUseVariance, true}; - CheckResult result = check(R"( --!strict local v: (number) -> (number) = nil :: any @@ -2084,7 +2044,6 @@ TEST_CASE_FIXTURE(Fixture, "array_of_singletons_should_subtype_against_generic_a // These flags expose the issue {FFlag::LuauSubtypingReportGenericBoundMismatches2, true}, {FFlag::DebugLuauStringSingletonBasedOnQuotes, true}, - {FFlag::LuauSubtypingGenericsDoesntUseVariance, true}, // And this flag fixes it {FFlag::LuauSubtypingUnionsAndIntersectionsInGenericBounds, true} }; @@ -2097,4 +2056,40 @@ TEST_CASE_FIXTURE(Fixture, "array_of_singletons_should_subtype_against_generic_a LUAU_REQUIRE_NO_ERRORS(res); } +TEST_CASE_FIXTURE(BuiltinsFixture, "gh1985_array_of_union_for_generic") +{ + ScopedFastFlag _[] = { + {FFlag::LuauSubtypingReportGenericBoundMismatches2, true}, + {FFlag::LuauSubtypingUnionsAndIntersectionsInGenericBounds, true} + }; + + CheckResult res = check(R"( + local function clear(arr: { T }) table.clear(arr) end + local a: { true | false } + -- This obviously shouldn't error, '{ true | false }' should fit '{ T }' + -- TypeError: The generic type parameter Twas found to have invalid bounds. Its lower bounds were [true, false], and its upper bounds were [true]. + clear(a) + )"); + + LUAU_REQUIRE_NO_ERRORS(res); +} + +TEST_CASE_FIXTURE(BuiltinsFixture, "gh1985_array_of_union_for_generic_2") +{ + ScopedFastFlag _[] = { + {FFlag::LuauSubtypingReportGenericBoundMismatches2, true}, + {FFlag::LuauSubtypingUnionsAndIntersectionsInGenericBounds, true} + }; + + CheckResult res = check(R"( + local function id(arr: { T }): { T } return arr end + local a: { true | false } + local b = id(a) + )"); + + LUAU_REQUIRE_NO_ERRORS(res); +} + + + TEST_SUITE_END(); diff --git a/tests/TypeInfer.intersectionTypes.test.cpp b/tests/TypeInfer.intersectionTypes.test.cpp index 42b7865d..38d938ba 100644 --- a/tests/TypeInfer.intersectionTypes.test.cpp +++ b/tests/TypeInfer.intersectionTypes.test.cpp @@ -10,9 +10,6 @@ using namespace Luau; LUAU_FASTFLAG(LuauSolverV2) -LUAU_FASTFLAG(LuauReturnMappedGenericPacksFromSubtyping2) -LUAU_FASTFLAG(LuauSolverAgnosticStringification) -LUAU_FASTFLAG(LuauSubtypingGenericPacksDoesntUseVariance) TEST_SUITE_BEGIN("IntersectionTypes"); @@ -164,7 +161,6 @@ TEST_CASE_FIXTURE(Fixture, "propagates_name") TEST_CASE_FIXTURE(Fixture, "index_on_an_intersection_type_with_property_guaranteed_to_exist") { - ScopedFastFlag sff{FFlag::LuauSolverAgnosticStringification, true}; CheckResult result = check(R"( type A = {x: {y: number}} type B = {x: {y: number}} @@ -649,7 +645,6 @@ TEST_CASE_FIXTURE(Fixture, "union_saturate_overloaded_functions") TEST_CASE_FIXTURE(Fixture, "intersection_of_tables") { - ScopedFastFlag sff{FFlag::LuauSolverAgnosticStringification, true}; CheckResult result = check(R"( function f(x: { p : number?, q : string? } & { p : number?, q : number?, r : number? }) local y : { p : number?, q : nil, r : number? } = x -- OK @@ -682,7 +677,6 @@ TEST_CASE_FIXTURE(Fixture, "intersection_of_tables") TEST_CASE_FIXTURE(Fixture, "intersection_of_tables_with_top_properties") { - ScopedFastFlag sff{FFlag::LuauSolverAgnosticStringification, true}; CheckResult result = check(R"( function f(x : { p : number?, q : any } & { p : unknown, q : string? }) local y : { p : number?, q : string? } = x -- OK @@ -736,7 +730,6 @@ TEST_CASE_FIXTURE(Fixture, "intersection_of_tables_with_never_properties") TEST_CASE_FIXTURE(Fixture, "overloaded_functions_returning_intersections") { - ScopedFastFlag sff{FFlag::LuauSolverAgnosticStringification, true}; CheckResult result = check(R"( function f(x : ((number?) -> ({ p : number } & { q : number })) & ((string?) -> ({ p : number } & { r : number }))) local y : (nil) -> { p : number, q : number, r : number} = x -- OK @@ -856,8 +849,6 @@ could not be converted into TEST_CASE_FIXTURE(Fixture, "overloaded_functions_mentioning_generic_packs") { - ScopedFastFlag sff{FFlag::LuauSubtypingGenericPacksDoesntUseVariance, true}; - CheckResult result = check(R"( function f() function g(x : ((number?, a...) -> (number?, b...)) & ((string?, a...) -> (string?, b...))) @@ -1123,8 +1114,6 @@ TEST_CASE_FIXTURE(Fixture, "overloadeded_functions_with_weird_typepacks_1") TEST_CASE_FIXTURE(Fixture, "overloadeded_functions_with_weird_typepacks_2") { - ScopedFastFlag _{FFlag::LuauSubtypingGenericPacksDoesntUseVariance, true}; - CheckResult result = check(R"( function f() function g(x : ((a...) -> ()) & ((b...) -> ())) @@ -1154,9 +1143,6 @@ TEST_CASE_FIXTURE(Fixture, "overloadeded_functions_with_weird_typepacks_2") TEST_CASE_FIXTURE(Fixture, "overloadeded_functions_with_weird_typepacks_3") { - ScopedFastFlag sff1{FFlag::LuauSubtypingGenericPacksDoesntUseVariance, true}; - ScopedFastFlag sff2{FFlag::LuauReturnMappedGenericPacksFromSubtyping2, true}; - CheckResult result = check(R"( function f() function g(x : (() -> a...) & (() -> (number?,a...))) @@ -1187,9 +1173,6 @@ could not be converted into TEST_CASE_FIXTURE(Fixture, "overloadeded_functions_with_weird_typepacks_4") { - ScopedFastFlag _{FFlag::LuauReturnMappedGenericPacksFromSubtyping2, true}; - ScopedFastFlag sff{FFlag::LuauSubtypingGenericPacksDoesntUseVariance, true}; - CheckResult result = check(R"( function f() function g(x : ((a...) -> ()) & ((number,a...) -> number)) diff --git a/tests/TypeInfer.loops.test.cpp b/tests/TypeInfer.loops.test.cpp index f6ad24c1..a225c0eb 100644 --- a/tests/TypeInfer.loops.test.cpp +++ b/tests/TypeInfer.loops.test.cpp @@ -15,8 +15,8 @@ using namespace Luau; LUAU_FASTFLAG(LuauSolverV2) -LUAU_FASTFLAG(LuauSolverAgnosticStringification) LUAU_FASTFLAG(LuauNoScopeShallNotSubsumeAll) +LUAU_FASTFLAG(LuauIterableBindNotUnify) TEST_SUITE_BEGIN("TypeInferLoops"); @@ -256,14 +256,20 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "for_in_with_just_one_iterator_is_ok") TEST_CASE_FIXTURE(BuiltinsFixture, "for_in_loop_with_zero_iterators_dcr") { - ScopedFastFlag sff{FFlag::LuauSolverV2, true}; + ScopedFastFlag sffs[] = { + {FFlag::LuauSolverV2, true}, + {FFlag::LuauIterableBindNotUnify, true}, + }; CheckResult result = check(R"( function no_iter() end for key in no_iter() do end )"); - LUAU_REQUIRE_ERROR_COUNT(2, result); + LUAU_REQUIRE_ERROR_COUNT(1, result); + auto err = get(result.errors[0]); + REQUIRE(err); + CHECK_EQ("for..in loops require at least one value to iterate over. Got zero", err->message); } TEST_CASE_FIXTURE(BuiltinsFixture, "for_in_with_a_custom_iterator_should_type_check") @@ -947,7 +953,6 @@ TEST_CASE_FIXTURE(Fixture, "for_loop_lower_bound_is_string_3") TEST_CASE_FIXTURE(BuiltinsFixture, "cli_68448_iterators_need_not_accept_nil") { - ScopedFastFlag sff{FFlag::LuauSolverAgnosticStringification, true}; // CLI-116500 if (FFlag::LuauSolverV2) return; diff --git a/tests/TypeInfer.modules.test.cpp b/tests/TypeInfer.modules.test.cpp index 235fad48..26f24ab2 100644 --- a/tests/TypeInfer.modules.test.cpp +++ b/tests/TypeInfer.modules.test.cpp @@ -13,13 +13,8 @@ LUAU_FASTFLAG(LuauInstantiateInSubtyping) LUAU_FASTFLAG(LuauSolverV2) -LUAU_FASTFLAG(LuauReturnMappedGenericPacksFromSubtyping2) LUAU_FASTFLAG(DebugLuauMagicTypes) -LUAU_FASTFLAG(LuauLimitDynamicConstraintSolving3) LUAU_FASTINT(LuauSolverConstraintLimit) -LUAU_FASTFLAG(LuauNameConstraintRestrictRecursiveTypes) -LUAU_FASTFLAG(LuauSolverAgnosticStringification) -LUAU_FASTFLAG(LuauSolverAgnosticSetType) using namespace Luau; @@ -162,7 +157,6 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "require_a_variadic_function") TEST_CASE_FIXTURE(BuiltinsFixture, "cross_module_table_freeze") { - ScopedFastFlag sff = {FFlag::LuauSolverAgnosticStringification, true}; fileResolver.source["game/A"] = R"( --!strict return { @@ -270,7 +264,6 @@ a = tbl.abc.def TEST_CASE_FIXTURE(BuiltinsFixture, "general_require_type_mismatch") { - ScopedFastFlag sff{FFlag::LuauSolverAgnosticStringification, true}; fileResolver.source["game/A"] = R"( return { def = 4 } )"; @@ -822,7 +815,7 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "cycles_dont_make_everything_any") TEST_CASE_FIXTURE(BuiltinsFixture, "cross_module_function_mutation") { - ScopedFastFlag _[] = {{FFlag::LuauSolverV2, true}, {FFlag::LuauReturnMappedGenericPacksFromSubtyping2, true}}; + ScopedFastFlag _{FFlag::LuauSolverV2, true}; fileResolver.source["game/A"] = R"( function test2(a: number, b: string) @@ -851,7 +844,6 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "internal_types_are_scrubbed_from_module") ScopedFastFlag sffs[] = { {FFlag::LuauSolverV2, true}, {FFlag::DebugLuauMagicTypes, true}, - {FFlag::LuauLimitDynamicConstraintSolving3, true}, }; fileResolver.source["game/A"] = R"( @@ -870,7 +862,6 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "internal_type_errors_are_only_reported_once" ScopedFastFlag sffs[] = { {FFlag::LuauSolverV2, true}, {FFlag::DebugLuauMagicTypes, true}, - {FFlag::LuauLimitDynamicConstraintSolving3, true}, }; fileResolver.source["game/A"] = R"( @@ -886,7 +877,7 @@ return function(): { X: _luau_blocked_type, Y: _luau_blocked_type } return nil : TEST_CASE_FIXTURE(BuiltinsFixture, "scrub_unsealed_tables") { - ScopedFastFlag sffs[] = {{FFlag::LuauSolverV2, true}, {FFlag::LuauLimitDynamicConstraintSolving3, true}}; + ScopedFastFlag sff{FFlag::LuauSolverV2, true}; ScopedFastInt sfi{FInt::LuauSolverConstraintLimit, 5}; @@ -916,8 +907,6 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "scrub_unsealed_tables") TEST_CASE_FIXTURE(BuiltinsFixture, "invalid_local_alias_shouldnt_shadow_imported_type") { - ScopedFastFlag _{FFlag::LuauNameConstraintRestrictRecursiveTypes, true}; - fileResolver.source["game/A"] = R"( export type bad = {T} return {} @@ -944,8 +933,6 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "invalid_local_alias_shouldnt_shadow_imported TEST_CASE_FIXTURE(BuiltinsFixture, "invalid_alias_should_export_as_error_type") { - ScopedFastFlag _{FFlag::LuauNameConstraintRestrictRecursiveTypes, true}; - fileResolver.source["game/A"] = R"( export type bad = {bad<{T}>} return {} diff --git a/tests/TypeInfer.oop.test.cpp b/tests/TypeInfer.oop.test.cpp index 990f8c30..d84f54b8 100644 --- a/tests/TypeInfer.oop.test.cpp +++ b/tests/TypeInfer.oop.test.cpp @@ -2,6 +2,9 @@ #include "Luau/AstQuery.h" #include "Luau/BuiltinDefinitions.h" +#include "Luau/Common.h" +#include "Luau/Error.h" +#include "Luau/Frontend.h" #include "Luau/Type.h" #include "Luau/VisitType.h" @@ -14,7 +17,7 @@ using namespace Luau; LUAU_FASTFLAG(LuauSolverV2) LUAU_FASTFLAG(LuauTrackFreeInteriorTypePacks) -LUAU_FASTFLAG(LuauSolverAgnosticStringification) +LUAU_FASTFLAG(LuauIndexInMetatableSubtyping) TEST_SUITE_BEGIN("TypeInferOOP"); @@ -332,7 +335,6 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "flag_when_index_metamethod_returns_0_values" TEST_CASE_FIXTURE(BuiltinsFixture, "augmenting_an_unsealed_table_with_a_metatable") { - ScopedFastFlag sff{FFlag::LuauSolverAgnosticStringification, true}; CheckResult result = check(R"( local A = {number = 8} @@ -622,4 +624,127 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "textbook_class_pattern_2") LUAU_REQUIRE_NO_ERRORS(result); } +TEST_CASE_FIXTURE(BuiltinsFixture, "oop_invoke_with_inferred_self_type") +{ + ScopedFastFlag _{FFlag::LuauIndexInMetatableSubtyping, true}; + + LUAU_REQUIRE_NO_ERRORS(check(R"( + local ItemContainer = {} + ItemContainer.__index = ItemContainer + + function ItemContainer.new() + local self = {} + setmetatable(self, ItemContainer) + return self + end + + function ItemContainer:removeItem(itemId, itemType) + self:getItem(itemId, itemType) + end + + function ItemContainer:getItem(itemId, itemType): () + end + + local container = ItemContainer.new() + + container:removeItem(0, "magic") + )")); +} + +TEST_CASE_FIXTURE(BuiltinsFixture, "oop_invoke_with_inferred_self_and_property") +{ + ScopedFastFlag _{FFlag::LuauIndexInMetatableSubtyping, true}; + + LUAU_REQUIRE_NO_ERRORS(check(R"( + local ItemContainer = {} + ItemContainer.__index = ItemContainer + + function ItemContainer.new(name) + local self = {name = name} + setmetatable(self, ItemContainer) + return self + end + + function ItemContainer:removeItem(itemId, itemType) + print(self.name) + self:getItem(itemId, itemType) + end + + function ItemContainer:getItem(itemId, itemType): () + end + + local container = ItemContainer.new("library") + + container:removeItem(0, "magic") + )")); +} + +TEST_CASE_FIXTURE(BuiltinsFixture, "metatable_field_allows_upcast") +{ + ScopedFastFlag sffs[] = { + {FFlag::LuauSolverV2, true}, + {FFlag::LuauIndexInMetatableSubtyping, true}, + }; + + LUAU_REQUIRE_NO_ERRORS(check(R"( + local Foobar = {} + Foobar.__index = Foobar + Foobar.const = 42 + + local foobar = setmetatable({}, Foobar) + + local _: { read const: number } = foobar + )")); +} + +TEST_CASE_FIXTURE(BuiltinsFixture, "metatable_field_disallows_invalid_upcast") +{ + ScopedFastFlag sffs[] = { + {FFlag::LuauSolverV2, true}, + {FFlag::LuauIndexInMetatableSubtyping, true}, + }; + + CheckResult results = check(R"( + local Foobar = {} + Foobar.__index = Foobar + Foobar.const = 42 + + local foobar = setmetatable({}, Foobar) + + local _: { const: number } = foobar + )"); + + LUAU_REQUIRE_ERROR_COUNT(1, results); + auto err = get(results.errors[0]); + REQUIRE(err); + CHECK_EQ("{ const: number }", toString(err->wantedType)); + CHECK_EQ("{ @metatable t1, { } } where t1 = { __index: t1, const: number }", toString(err->givenType, {/* exhaustive */ true})); +} + +TEST_CASE_FIXTURE(BuiltinsFixture, "metatable_field_precedence_for_subtyping") +{ + ScopedFastFlag sffs[] = { + {FFlag::LuauSolverV2, true}, + {FFlag::LuauIndexInMetatableSubtyping, true}, + }; + + CheckResult results = check(R"( + local function foobar1(_: { read foo: number }) end + local function foobar2(_: { read bar: boolean }) end + local function foobar3(_: { read foo: string }) end + + local t = { foo = 4 } + setmetatable(t, { __index = { foo = "heh", bar = true }}) + foobar1(t) + foobar2(t) + foobar3(t) + )"); + + LUAU_REQUIRE_ERROR_COUNT(1, results); + auto err = get(results.errors[0]); + REQUIRE(err); + CHECK_EQ("{ read foo: string }", toString(err->wantedType, {/* exhaustive */ true})); + CHECK_EQ("{ @metatable { __index: { bar: boolean, foo: string } }, { foo: number } }", toString(err->givenType, { /* exhaustive */ true})); +} + TEST_SUITE_END(); diff --git a/tests/TypeInfer.operators.test.cpp b/tests/TypeInfer.operators.test.cpp index ef829e15..19d20149 100644 --- a/tests/TypeInfer.operators.test.cpp +++ b/tests/TypeInfer.operators.test.cpp @@ -22,6 +22,7 @@ LUAU_FASTFLAG(LuauNoScopeShallNotSubsumeAll) LUAU_FASTFLAG(LuauTrackUniqueness) LUAU_FASTFLAG(LuauNoMoreComparisonTypeFunctions) LUAU_FASTFLAG(LuauSolverAgnosticStringification) +LUAU_FASTFLAG(LuauNoOrderingTypeFunctions) TEST_SUITE_BEGIN("TypeInferOperators"); @@ -293,6 +294,8 @@ TEST_CASE_FIXTURE(Fixture, "compare_strings") TEST_CASE_FIXTURE(Fixture, "cannot_indirectly_compare_types_that_do_not_have_a_metatable") { + ScopedFastFlag _{FFlag::LuauNoOrderingTypeFunctions, true}; + CheckResult result = check(R"( local a = {} local b = {} @@ -303,9 +306,7 @@ TEST_CASE_FIXTURE(Fixture, "cannot_indirectly_compare_types_that_do_not_have_a_m if (FFlag::LuauSolverV2) { - UninhabitedTypeFunction* utf = get(result.errors[0]); - REQUIRE(utf); - REQUIRE_EQ(toString(utf->ty), "lt"); + REQUIRE(get(result.errors[0])); } else { @@ -317,6 +318,8 @@ TEST_CASE_FIXTURE(Fixture, "cannot_indirectly_compare_types_that_do_not_have_a_m TEST_CASE_FIXTURE(BuiltinsFixture, "cannot_indirectly_compare_types_that_do_not_offer_overloaded_ordering_operators") { + ScopedFastFlag _{FFlag::LuauNoOrderingTypeFunctions, true}; + CheckResult result = check(R"( local M = {} function M.new() @@ -333,9 +336,7 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "cannot_indirectly_compare_types_that_do_not_ if (FFlag::LuauSolverV2) { - UninhabitedTypeFunction* utf = get(result.errors[0]); - REQUIRE(utf); - REQUIRE_EQ(toString(utf->ty), "lt"); + REQUIRE(get(result.errors[0])); } else { @@ -865,6 +866,8 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "and_binexps_dont_unify") TEST_CASE_FIXTURE(Fixture, "error_on_invalid_operand_types_to_relational_operators") { + ScopedFastFlag _{FFlag::LuauNoOrderingTypeFunctions, true}; + CheckResult result = check(R"( local a: boolean = true local b: boolean = false @@ -875,9 +878,9 @@ TEST_CASE_FIXTURE(Fixture, "error_on_invalid_operand_types_to_relational_operato if (FFlag::LuauSolverV2) { - UninhabitedTypeFunction* utf = get(result.errors[0]); - REQUIRE(utf); - REQUIRE_EQ(toString(utf->ty), "lt"); + GenericError* ge = get(result.errors[0]); + REQUIRE(ge); + CHECK_EQ("Types 'boolean' and 'boolean' cannot be compared with relational operator <", ge->message); } else { @@ -889,6 +892,8 @@ TEST_CASE_FIXTURE(Fixture, "error_on_invalid_operand_types_to_relational_operato TEST_CASE_FIXTURE(Fixture, "error_on_invalid_operand_types_to_relational_operators2") { + ScopedFastFlag _{FFlag::LuauNoOrderingTypeFunctions, true}; + CheckResult result = check(R"( local a: number | string = "" local b: number | string = 1 @@ -906,9 +911,9 @@ TEST_CASE_FIXTURE(Fixture, "error_on_invalid_operand_types_to_relational_operato if (FFlag::LuauSolverV2) { - UninhabitedTypeFunction* utf = get(result.errors[0]); - REQUIRE(utf); - REQUIRE_EQ(toString(utf->ty), "lt"); + GenericError* ge = get(result.errors[0]); + REQUIRE(ge); + CHECK_EQ("Types 'number | string' and 'number | string' cannot be compared with relational operator <", ge->message); } else { diff --git a/tests/TypeInfer.provisional.test.cpp b/tests/TypeInfer.provisional.test.cpp index b6f174d7..2f6ef2c5 100644 --- a/tests/TypeInfer.provisional.test.cpp +++ b/tests/TypeInfer.provisional.test.cpp @@ -18,8 +18,8 @@ LUAU_FASTINT(LuauTarjanChildLimit) LUAU_FASTINT(LuauTypeInferIterationLimit) LUAU_FASTINT(LuauTypeInferRecursionLimit) LUAU_FASTINT(LuauTypeInferTypePackLoopLimit) -LUAU_FASTFLAG(LuauSolverAgnosticStringification) LUAU_FASTFLAG(LuauNoMoreComparisonTypeFunctions) +LUAU_FASTFLAG(LuauAddRefinementToAssertions) TEST_SUITE_BEGIN("ProvisionalTests"); @@ -59,12 +59,13 @@ TEST_CASE_FIXTURE(Fixture, "typeguard_inference_incomplete") end )"; - const std::string expectedWithNewSolver = R"( + const std::string expectedWithNewSolver = + R"( function f(a:{fn:()->(unknown,...unknown)}): () if type(a) == 'boolean' then local a1:{fn:()->(unknown,...unknown)}&boolean=a elseif a.fn() then - local a2:{fn:()->(unknown,...unknown)}&(class|function|nil|number|string|thread|buffer|table)=a + local a2:{fn:()->(unknown,...unknown)}&(userdata|function|nil|number|string|thread|buffer|table)=a end end )"; @@ -279,7 +280,6 @@ TEST_CASE_FIXTURE(Fixture, "lvalue_equals_another_lvalue_with_no_overlap") // Just needs to fully support equality refinement. Which is annoying without type states. TEST_CASE_FIXTURE(Fixture, "discriminate_from_x_not_equal_to_nil") { - ScopedFastFlag sff{FFlag::LuauSolverAgnosticStringification, true}; CheckResult result = check(R"( type T = {x: string, y: number} | {x: nil, y: nil} @@ -536,7 +536,6 @@ TEST_CASE_FIXTURE(Fixture, "dcr_can_partially_dispatch_a_constraint") TEST_CASE_FIXTURE(Fixture, "free_options_cannot_be_unified_together") { - ScopedFastFlag sff_stringification{FFlag::LuauSolverAgnosticStringification, true}; ScopedFastFlag sff{FFlag::LuauSolverV2, false}; TypeArena arena; @@ -840,7 +839,6 @@ TEST_CASE_FIXTURE(IsSubtypeFixture, "functions_with_mismatching_arity_but_any_is TEST_CASE_FIXTURE(Fixture, "assign_table_with_refined_property_with_a_similar_type_is_illegal") { - ScopedFastFlag sff{FFlag::LuauSolverAgnosticStringification, true}; CheckResult result = check(R"( local t: {x: number?} = {x = nil} @@ -966,7 +964,6 @@ TEST_CASE_FIXTURE(Fixture, "floating_generics_should_not_be_allowed") TEST_CASE_FIXTURE(Fixture, "free_options_can_be_unified_together") { - ScopedFastFlag sff_stringification{FFlag::LuauSolverAgnosticStringification, true}; ScopedFastFlag sff{FFlag::LuauSolverV2, false}; TypeArena arena; @@ -1380,4 +1377,91 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "refine_unknown_to_table_and_test_two_props") CHECK(Position{3, 61} == result.errors[0].location.end); } +TEST_CASE_FIXTURE(BuiltinsFixture, "function_indexer_satisfies_reading_property") +{ + ScopedFastFlag _{FFlag::LuauSolverV2, true}; + + // We would like this code to have _no_ errors, but it requires one of: + // (a) Being able to express read-only indexers, as that is the type of + // `__index` when it is a function. + // (b) Metatable aware semantic subtyping for tables. + CheckResult result = check(R"( + local t = setmetatable({}, { + __index = function (_, _prop: string): number + return 42 + end + }) + + local function readX(tbl: { read X: number }) + print(tbl.X) + end + + -- This should work as `__index` being a function should semantically + -- be the same as having an indexer. + readX(t) + )"); + + LUAU_REQUIRE_ERROR_COUNT(1, result); + auto err = get(result.errors[0]); + REQUIRE(err); + CHECK_EQ("{ @metatable { __index: (unknown, string) -> number }, { } }", toString(err->givenType, { /* exhaustive */ true})); + CHECK_EQ("{ read X: number }", toString(err->wantedType)); +} + +TEST_CASE_FIXTURE(Fixture, "unification_inferring_never_for_refined_param") +{ + ScopedFastFlag sff{FFlag::LuauSolverV2, true}; + + LUAU_REQUIRE_NO_ERRORS(check(R"( + local function __remove(__: number?) end + + function __removeItem(self, itemId: number) + local index = self.getItem(itemId) + if index then + __remove(index) + end + end + )")); + + // TODO CLI-168953: This is not correct. We should not be inferring `never` + // for the second return type of `getItem`. + CHECK_EQ("({ read getItem: (number) -> (never, ...unknown) }, number) -> ()", toString(requireType("__removeItem"))); +} + +TEST_CASE_FIXTURE(BuiltinsFixture, "assert_and_many_nested_typeof_contexts") +{ + ScopedFastFlag sffs[] = { + {FFlag::LuauSolverV2, true}, + {FFlag::LuauAddRefinementToAssertions, true}, + }; + + CheckResult result = check(R"( + local foo: unknown = nil :: any + assert(typeof(foo) == "table") + if typeof(typeof(foo.x)) == "string" then + end + )"); + + // TODO CLI-174351: We should expect a TypeError: Type 'table' does not have key 'x' error here. + LUAU_REQUIRE_NO_ERRORS(result); +} + +TEST_CASE_FIXTURE(BuiltinsFixture, "bidirectional_inference_variadic_type_pack_read_only_prop") +{ + ScopedFastFlag sff{FFlag::LuauSolverV2, true}; + + LUAU_REQUIRE_NO_ERRORS(check(R"( + local foo: { read bar: (...string) -> () } = { + bar = function (foobar) + print(foobar) + end + } + )")); + + // CLI-174314: This should be `string`: we need to flatten and *extend* + // the type packs for function arguments, so that variadic type packs + // fill in. + CHECK_EQ("unknown", toString(requireTypeAtPosition({3, 24}))); +} + TEST_SUITE_END(); diff --git a/tests/TypeInfer.refinements.test.cpp b/tests/TypeInfer.refinements.test.cpp index 99a42cce..6614e7a9 100644 --- a/tests/TypeInfer.refinements.test.cpp +++ b/tests/TypeInfer.refinements.test.cpp @@ -11,18 +11,18 @@ LUAU_FASTFLAG(LuauSolverV2) LUAU_FASTFLAG(DebugLuauEqSatSimplification) LUAU_FASTFLAG(LuauFunctionCallsAreNotNilable) -LUAU_FASTFLAG(LuauNormalizationReorderFreeTypeIntersect) LUAU_FASTFLAG(LuauRefineNoRefineAlways) LUAU_FASTFLAG(LuauRefineDistributesOverUnions) -LUAU_FASTFLAG(LuauUnifyShortcircuitSomeIntersectionsAndUnions) -LUAU_FASTFLAG(LuauNewNonStrictNoErrorsPassingNever) LUAU_FASTFLAG(LuauSubtypingReportGenericBoundMismatches2) -LUAU_FASTFLAG(LuauSolverAgnosticStringification) -LUAU_FASTFLAG(LuauSubtypingGenericsDoesntUseVariance) -LUAU_FASTFLAG(LuauReturnMappedGenericPacksFromSubtyping2) LUAU_FASTFLAG(LuauNoMoreComparisonTypeFunctions) LUAU_FASTFLAG(LuauNumericUnaryOpsDontProduceNegationRefinements) LUAU_FASTFLAG(LuauAddConditionalContextForTernary) +LUAU_FASTFLAG(LuauNoOrderingTypeFunctions) +LUAU_FASTFLAG(LuauConsiderErrorSuppressionInTypes) +LUAU_FASTFLAG(LuauAddRefinementToAssertions) +LUAU_FASTFLAG(LuauEnqueueUnionsOfDistributedTypeFunctions) +LUAU_FASTFLAG(DebugLuauAssertOnForcedConstraint) +LUAU_FASTFLAG(LuauNormalizationPreservesAny) using namespace Luau; @@ -522,13 +522,23 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "call_an_incompatible_function_after_using_ty end )"); - LUAU_REQUIRE_ERROR_COUNT(2, result); + if (FFlag::LuauSolverV2 && FFlag::LuauConsiderErrorSuppressionInTypes) + { + LUAU_REQUIRE_ERROR_COUNT(1, result); + + 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])); - CHECK(Location{{7, 18}, {7, 19}} == result.errors[0].location); + 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])); - CHECK(Location{{13, 18}, {13, 19}} == result.errors[1].location); + CHECK("Type 'string' could not be converted into 'number'" == toString(result.errors[1])); + CHECK(Location{{13, 18}, {13, 19}} == result.errors[1].location); + } } TEST_CASE_FIXTURE(BuiltinsFixture, "impossible_type_narrow_is_not_an_error") @@ -691,10 +701,6 @@ TEST_CASE_FIXTURE(Fixture, "lvalue_is_not_nil") TEST_CASE_FIXTURE(Fixture, "free_type_is_equal_to_an_lvalue") { - ScopedFastFlag sff[] = { - {FFlag::LuauNormalizationReorderFreeTypeIntersect, true}, - }; - CheckResult result = check(R"( local function f(a, b: string?) if a == b then @@ -724,7 +730,6 @@ TEST_CASE_FIXTURE(Fixture, "free_type_is_equal_to_an_lvalue") TEST_CASE_FIXTURE(Fixture, "unknown_lvalue_is_not_synonymous_with_other_on_not_equal") { - ScopedFastFlag sff{FFlag::LuauSolverAgnosticStringification, true}; CheckResult result = check(R"( local function f(a: any, b: {x: number}?) if a ~= b then @@ -853,7 +858,6 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "typeguard_not_to_be_string") TEST_CASE_FIXTURE(BuiltinsFixture, "typeguard_narrows_for_table") { - ScopedFastFlag sff{FFlag::LuauSolverAgnosticStringification, true}; CheckResult result = check(R"( local function f(x: string | {x: number} | {y: boolean}) if type(x) == "table" then @@ -890,7 +894,6 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "typeguard_narrows_for_functions") TEST_CASE_FIXTURE(BuiltinsFixture, "type_guard_can_filter_for_intersection_of_tables") { - ScopedFastFlag sff{FFlag::LuauSolverAgnosticStringification, true}; CheckResult result = check(R"( type XYCoord = {x: number} & {y: number} local function f(t: XYCoord?) @@ -1045,7 +1048,6 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "either_number_or_string") TEST_CASE_FIXTURE(Fixture, "not_t_or_some_prop_of_t") { - ScopedFastFlag sff{FFlag::LuauSolverAgnosticStringification, true}; CheckResult result = check(R"( local function f(t: {x: boolean}?) if not t or t.x then @@ -1298,7 +1300,6 @@ TEST_CASE_FIXTURE(Fixture, "apply_refinements_on_astexprindexexpr_whose_subscrip TEST_CASE_FIXTURE(Fixture, "discriminate_from_truthiness_of_x") { - ScopedFastFlag sff{FFlag::LuauSolverAgnosticStringification, true}; CheckResult result = check(R"( type T = {tag: "missing", x: nil} | {tag: "exists", x: string} @@ -1433,7 +1434,6 @@ TEST_CASE_FIXTURE(Fixture, "refine_a_property_not_to_be_nil_through_an_intersect TEST_CASE_FIXTURE(RefinementExternTypeFixture, "discriminate_from_isa_of_x") { - ScopedFastFlag sff{FFlag::LuauSolverAgnosticStringification, true}; CheckResult result = check(R"( type T = {tag: "Part", x: Part} | {tag: "Folder", x: Folder} @@ -1716,9 +1716,7 @@ TEST_CASE_FIXTURE(RefinementExternTypeFixture, "asserting_non_existent_propertie TEST_CASE_FIXTURE(RefinementExternTypeFixture, "x_is_not_instance_or_else_not_part") { - // CLI-117135 - RefinementTests.x_is_not_instance_or_else_not_part not correctly applying refinements to a function parameter - if (FFlag::LuauSolverV2) - return; + ScopedFastFlag sff{FFlag::LuauRefineDistributesOverUnions, true}; CheckResult result = check(R"( local function f(x: Part | Folder | string) @@ -2247,7 +2245,7 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "luau_polyfill_isindexkey_refine_conjunction" ScopedFastFlag sffs[] = { {FFlag::LuauSolverV2, true}, {FFlag::LuauNoMoreComparisonTypeFunctions, true}, - {FFlag::LuauNormalizationReorderFreeTypeIntersect, true}, + {FFlag::LuauNoOrderingTypeFunctions, true}, }; CheckResult result = check(R"( @@ -2259,19 +2257,30 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "luau_polyfill_isindexkey_refine_conjunction" end )"); - LUAU_CHECK_ERROR_COUNT(2, result); + LUAU_REQUIRE_NO_ERRORS(result); +} + +TEST_CASE_FIXTURE(BuiltinsFixture, "check_refinement_to_primitive_and_compare") +{ + ScopedFastFlag sffs[] = { + {FFlag::LuauSolverV2, true}, + {FFlag::LuauNoMoreComparisonTypeFunctions, true}, + {FFlag::LuauNoOrderingTypeFunctions, true}, + }; + + CheckResult result = check(R"( + local function comesAfterLuau(word) + return type(word) == "string" and word > "luau" + end + )"); - // For some reason we emit three error here. - for (const auto& e : result.errors) - CHECK(get(e)); + LUAU_REQUIRE_NO_ERRORS(result); + CHECK_EQ("(unknown) -> boolean", toString(requireType("comesAfterLuau"))); } TEST_CASE_FIXTURE(BuiltinsFixture, "luau_polyfill_isindexkey_refine_conjunction_variant") { - ScopedFastFlag _[] = { - {FFlag::LuauUnifyShortcircuitSomeIntersectionsAndUnions, true}, - {FFlag::LuauNormalizationReorderFreeTypeIntersect, true}, - }; + ScopedFastFlag _{FFlag::LuauNoOrderingTypeFunctions, true}; // FIXME CLI-141364: An underlying bug in normalization means the type of // `isIndexKey` is platform dependent. @@ -2283,15 +2292,8 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "luau_polyfill_isindexkey_refine_conjunction_ and math.floor(k) == k -- no float keys end )"); - if (FFlag::LuauSolverV2) - { - LUAU_REQUIRE_ERROR_COUNT(2, result); - // For some reason we emit two errors here. - for (const auto& e : result.errors) - CHECK(get(e)); - } - else - LUAU_REQUIRE_NO_ERRORS(result); + + LUAU_REQUIRE_NO_ERRORS(result); } TEST_CASE_FIXTURE(BuiltinsFixture, "ex") @@ -2344,7 +2346,6 @@ TEST_CASE_FIXTURE(RefinementExternTypeFixture, "mutate_prop_of_some_refined_symb TEST_CASE_FIXTURE(BuiltinsFixture, "ensure_t_after_return_references_all_reachable_points") { - ScopedFastFlag sff{FFlag::LuauSolverAgnosticStringification, true}; CheckResult result = check(R"( local t = {} @@ -2732,6 +2733,23 @@ TEST_CASE_FIXTURE(RefinementExternTypeFixture, "cannot_call_a_function_single") CHECK_EQ("The type function is not precise enough for us to determine the appropriate result type of this call.", toString(result.errors[0])); } +TEST_CASE_FIXTURE(RefinementExternTypeFixture, "cli_140033_refine_union_of_extern_types") +{ + LUAU_REQUIRE_NO_ERRORS(check(R"( + --!strict + local function getImageLabel(vars: { Instance }): Folder | Part | nil + for _, item in vars do + if item:IsA("Folder") or item:IsA("Part") then + return item + end + end + return nil + end + )")); + + CHECK_EQ("Folder | Part", toString(requireTypeAtPosition({5, 28}))); +} + TEST_CASE_FIXTURE(RefinementExternTypeFixture, "cannot_call_a_function_union") { ScopedFastFlag sff{FFlag::LuauSolverV2, true}; @@ -3011,4 +3029,181 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "inline_if_conditional_context") )")); } +TEST_CASE_FIXTURE(Fixture, "oss_1517_equality_doesnt_add_nil") +{ + LUAU_REQUIRE_NO_ERRORS(check(R"( + type MyType = { + data: any + } + + local function createMyType(): MyType + local obj = { data = {} } + return obj + end + + local function testTypeInference() + local a: MyType = createMyType() + local b: MyType = createMyType() + + if a == b then + local c: MyType = b + local value = b.data + end + end + )")); +} + +TEST_CASE_FIXTURE(BuiltinsFixture, "typeof_refinement_context") +{ + ScopedFastFlag sffs[] = { + {FFlag::LuauSolverV2, true}, + {FFlag::LuauAddRefinementToAssertions, true}, + }; + + LUAU_REQUIRE_NO_ERRORS(check(R"( + --!strict + + local x = {} :: unknown + + if typeof(x) == "table" then + if typeof(x.transform) == "function" then + local y = x.transform + end + end + )")); +} + +TEST_CASE_FIXTURE(BuiltinsFixture, "assert_and_typeof_refinement_context") +{ + ScopedFastFlag sffs[] = { + {FFlag::LuauSolverV2, true}, + {FFlag::LuauAddRefinementToAssertions, true}, + }; + + LUAU_REQUIRE_NO_ERRORS(check(R"( + --!strict + + local x = {} :: unknown + + if typeof(x) == "table" then + assert(typeof(x.transform) == "function") + end + )")); +} + +TEST_CASE_FIXTURE(BuiltinsFixture, "foo_call_should_not_refine") +{ + ScopedFastFlag sffs[] = { + {FFlag::LuauSolverV2, true}, + {FFlag::LuauAddRefinementToAssertions, true}, + }; + + CheckResult result = check(R"( + --!strict + + local x = {} :: unknown + local function foo(_: boolean) end + + if typeof(x) == "table" then + foo(typeof(x.transform) == "function") + end + )"); + + LUAU_REQUIRE_ERROR_COUNT(1, result); + CHECK_EQ("Type 'table' does not have key 'transform'", toString(result.errors[0])); +} + +TEST_CASE_FIXTURE(BuiltinsFixture, "assert_call_should_not_refine_despite_typeof") +{ + ScopedFastFlag sffs[] = { + {FFlag::LuauSolverV2, true}, + {FFlag::LuauAddRefinementToAssertions, true}, + }; + + CheckResult result = check(R"( + --!strict + local function foo(_: any) end + + local function f(x: unknown) + if typeof(x) == "table" then + assert(foo(typeof(x.bar))) + end + end + )"); + + LUAU_REQUIRE_ERROR_COUNT(1, result); + CHECK_EQ("Type 'table' does not have key 'bar'", toString(result.errors[0])); +} + +TEST_CASE_FIXTURE(BuiltinsFixture, "non_conditional_context_in_if_should_not_refine") +{ + ScopedFastFlag sffs[] = { + {FFlag::LuauSolverV2, true}, + {FFlag::LuauAddRefinementToAssertions, true}, + }; + + CheckResult result = check(R"( + local function bing(_: any) end + local function foobar(x: unknown) + assert(typeof(x) == "table") + if bing(x.foo) then + end + end + )"); + + LUAU_REQUIRE_ERROR_COUNT(1, result); + CHECK_EQ("Type 'table' does not have key 'foo'", toString(result.errors[0])); +} + +TEST_CASE_FIXTURE(Fixture, "type_function_reduction_with_union_type_application" * doctest::timeout(0.5)) +{ + ScopedFastFlag sffs[] = { + {FFlag::LuauSolverV2, true}, + {FFlag::DebugLuauAssertOnForcedConstraint, true}, + {FFlag::LuauEnqueueUnionsOfDistributedTypeFunctions, true}, + }; + + LUAU_REQUIRE_NO_ERRORS(check(R"( + local lastTick = 0 + local jumpAnimTime = 0 + local toolAnimTime = 0 + + function move(time, tool, animStringValueObject) + local deltaTime = time - lastTick + lastTick = time + + if jumpAnimTime > 0 then + jumpAnimTime = jumpAnimTime - deltaTime + end + + if animStringValueObject then + toolAnimTime = time + .3 + end + + if time > toolAnimTime then + end + end + )")); +} + +TEST_CASE_FIXTURE(BuiltinsFixture, "refine_any_and_unknown_should_still_be_any") +{ + ScopedFastFlag _{FFlag::LuauNormalizationPreservesAny, true}; + + LUAU_REQUIRE_NO_ERRORS(check(R"( + local REACT_FRAGMENT_TYPE = (nil :: any) + local function typeOf(object: any) + local __type = object.type + + if __type == REACT_FRAGMENT_TYPE then + return __type + else + return __type + and typeof(__type) == "table" + and __type["$$typeof"] + end + end + )")); +} + TEST_SUITE_END(); diff --git a/tests/TypeInfer.singletons.test.cpp b/tests/TypeInfer.singletons.test.cpp index 766888b1..fbb34bc2 100644 --- a/tests/TypeInfer.singletons.test.cpp +++ b/tests/TypeInfer.singletons.test.cpp @@ -8,8 +8,9 @@ using namespace Luau; LUAU_FASTFLAG(LuauSolverV2) -LUAU_FASTFLAG(LuauSolverAgnosticStringification) -LUAU_FASTFLAG(LuauPushTypeConstraint) +LUAU_FASTFLAG(LuauPushTypeConstraint2) +LUAU_FASTFLAG(LuauPushTypeConstraintSingleton) +LUAU_FASTFLAG(LuauPushTypeConstraintIndexer) TEST_SUITE_BEGIN("TypeSingletons"); @@ -292,7 +293,6 @@ TEST_CASE_FIXTURE(Fixture, "tagged_unions_immutable_tag") TEST_CASE_FIXTURE(Fixture, "table_has_a_boolean") { - ScopedFastFlag sff{FFlag::LuauSolverAgnosticStringification, true}; CheckResult result = check(R"( local t={a=1,b=false} )"); @@ -399,7 +399,7 @@ TEST_CASE_FIXTURE(Fixture, "table_properties_type_error_escapes") TEST_CASE_FIXTURE(Fixture, "error_detailed_tagged_union_mismatch_string") { - ScopedFastFlag _{FFlag::LuauPushTypeConstraint, true}; + ScopedFastFlag _{FFlag::LuauPushTypeConstraint2, true}; CheckResult result = check(R"( type Cat = { tag: 'cat', catfood: string } @@ -411,7 +411,10 @@ local a: Animal = { tag = 'cat', cafood = 'something' } LUAU_REQUIRE_ERROR_COUNT(1, result); if (FFlag::LuauSolverV2) - CHECK(R"(Table type '{ cafood: string, tag: "cat" }' not compatible with type 'Cat' because the former is missing field 'catfood')" == toString(result.errors[0])); + CHECK( + R"(Table type '{ cafood: string, tag: "cat" }' not compatible with type 'Cat' because the former is missing field 'catfood')" == + toString(result.errors[0]) + ); else { const std::string expected = R"(Type 'a' could not be converted into 'Cat | Dog' @@ -662,6 +665,8 @@ 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" } @@ -675,7 +680,7 @@ TEST_CASE_FIXTURE(Fixture, "tagged_union_in_ternary") TEST_CASE_FIXTURE(Fixture, "table_literal_with_singleton_union_values") { - ScopedFastFlag _{FFlag::LuauPushTypeConstraint, true}; + ScopedFastFlag _{FFlag::LuauPushTypeConstraint2, true}; CheckResult result = check(R"( local t1: {[string]: "a" | "b"} = { a = "a", b = "b" } @@ -688,7 +693,7 @@ TEST_CASE_FIXTURE(Fixture, "table_literal_with_singleton_union_values") TEST_CASE_FIXTURE(Fixture, "singleton_type_mismatch_via_variable") { - ScopedFastFlag _{FFlag::LuauPushTypeConstraint, true}; + ScopedFastFlag _{FFlag::LuauPushTypeConstraint2, true}; CheckResult result = check(R"( local c = "c" @@ -708,7 +713,7 @@ TEST_CASE_FIXTURE(Fixture, "singleton_type_mismatch_via_variable") TEST_CASE_FIXTURE(Fixture, "cli_163481_any_indexer_pushes_type") { - ScopedFastFlag _{FFlag::LuauPushTypeConstraint, true}; + ScopedFastFlag _{FFlag::LuauPushTypeConstraint2, true}; LUAU_REQUIRE_NO_ERRORS(check(R"( --!strict @@ -726,4 +731,90 @@ 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}, + }; + + LUAU_REQUIRE_NO_ERRORS(check(R"( + local function foo(my_enum: "" | T): T + return my_enum :: T + end + + local var = foo("meow") + )")); + + CHECK_EQ("\"meow\"", toString(requireType("var"))); +} + +TEST_CASE_FIXTURE(Fixture, "oss_1773") +{ + ScopedFastFlag sffs[] = { + {FFlag::LuauSolverV2, true}, + {FFlag::LuauPushTypeConstraint2, true}, + {FFlag::LuauPushTypeConstraintIndexer, true}, + }; + + LUAU_REQUIRE_NO_ERRORS(check(R"( + --!strict + + export type T = "foo" | "bar" | "toto" + + local object: T = "foo" + + local getOpposite: {[T]: T} = { + ["foo"] = "bar", + ["bar"] = "toto", + ["toto"] = "foo" + } + + local function hello() + local x = getOpposite[object] + + if x then + object = x + end + end + )")); +} + +TEST_CASE_FIXTURE(Fixture, "bidirectionally_infer_indexers_errored") +{ + ScopedFastFlag sffs[] = { + {FFlag::LuauSolverV2, true}, + {FFlag::LuauPushTypeConstraint2, true}, + {FFlag::LuauPushTypeConstraintIndexer, true}, + }; + + CheckResult result = check(R"( + --!strict + + export type T = "foo" | "bar" | "toto" + + local getOpposite: { [number]: T } = { + ["foo"] = "bar", + ["bar"] = "toto", + ["toto"] = "foo" + } + )"); + + LUAU_REQUIRE_ERROR_COUNT(3, result); + + for (const auto& e: result.errors) + CHECK(get(e)); +} + +TEST_CASE_FIXTURE(Fixture, "oss_2018") +{ + ScopedFastFlag _{FFlag::LuauPushTypeConstraint2, true}; + + LUAU_REQUIRE_NO_ERRORS(check(R"( + local rule: { rule: "AppendTextComment" } | { rule: "Other" } = { rule = "AppendTextComment" } + )")); +} + + TEST_SUITE_END(); diff --git a/tests/TypeInfer.tables.test.cpp b/tests/TypeInfer.tables.test.cpp index 3793fa9d..fb93ae60 100644 --- a/tests/TypeInfer.tables.test.cpp +++ b/tests/TypeInfer.tables.test.cpp @@ -23,18 +23,15 @@ LUAU_FASTFLAG(LuauInstantiateInSubtyping) LUAU_FASTFLAG(LuauFixIndexerSubtypingOrdering) LUAU_FASTFLAG(DebugLuauAssertOnForcedConstraint) LUAU_FASTINT(LuauPrimitiveInferenceInTableLimit) -LUAU_FASTFLAG(LuauSolverAgnosticStringification) -LUAU_FASTFLAG(LuauInferActualIfElseExprType2) LUAU_FASTFLAG(LuauNoScopeShallNotSubsumeAll) LUAU_FASTFLAG(LuauExtendSealedTableUpperBounds) -LUAU_FASTFLAG(LuauSubtypingGenericsDoesntUseVariance) -LUAU_FASTFLAG(LuauAllowMixedTables) -LUAU_FASTFLAG(LuauSolverAgnosticSetType) LUAU_FASTFLAG(LuauSubtypingReportGenericBoundMismatches2) -LUAU_FASTFLAG(LuauPushTypeConstraint) -LUAU_FASTFLAG(LuauUnifyShortcircuitSomeIntersectionsAndUnions) +LUAU_FASTFLAG(LuauPushTypeConstraint2) LUAU_FASTFLAG(LuauSimplifyIntersectionForLiteralSubtypeCheck) LUAU_FASTFLAG(LuauCacheDuplicateHasPropConstraints) +LUAU_FASTFLAG(LuauPushTypeConstraintIntersection) +LUAU_FASTFLAG(LuauPushTypeConstraintSingleton) +LUAU_FASTFLAG(LuauPushTypeConstraintLambdas) TEST_SUITE_BEGIN("TableTests"); @@ -100,7 +97,6 @@ TEST_CASE_FIXTURE(Fixture, "basic") TEST_CASE_FIXTURE(Fixture, "augment_table") { - ScopedFastFlag sff[] = {{FFlag::LuauSolverAgnosticSetType, true}, {FFlag::LuauSolverAgnosticStringification, true}}; CheckResult result = check(R"( local t = {} t.foo = 'bar' @@ -118,7 +114,6 @@ TEST_CASE_FIXTURE(Fixture, "augment_table") TEST_CASE_FIXTURE(Fixture, "augment_nested_table") { - ScopedFastFlag sff[] = {{FFlag::LuauSolverAgnosticSetType, true}, {FFlag::LuauSolverAgnosticStringification, true}}; CheckResult result = check(R"( local t = { p = {} } t.p.foo = 'bar' @@ -172,7 +167,6 @@ TEST_CASE_FIXTURE(Fixture, "index_expression_is_checked_against_the_indexer_type TEST_CASE_FIXTURE(Fixture, "cannot_augment_sealed_table") { - ScopedFastFlag sff{FFlag::LuauSolverAgnosticStringification, true}; CheckResult result = check(R"( function mkt() return {prop=999} @@ -782,7 +776,6 @@ TEST_CASE_FIXTURE(Fixture, "infer_indexer_from_array_like_table") TEST_CASE_FIXTURE(Fixture, "infer_indexer_from_value_property_in_literal") { - ScopedFastFlag sff{FFlag::LuauSolverAgnosticStringification, true}; CheckResult result = check(R"( function Symbol(n) return { __name=n } @@ -842,7 +835,6 @@ TEST_CASE_FIXTURE(Fixture, "infer_indexer_from_its_variable_type_and_unifiable") TEST_CASE_FIXTURE(Fixture, "indexer_mismatch") { - ScopedFastFlag sff{FFlag::LuauSolverAgnosticStringification, true}; CheckResult result = check(R"( local t1: { [string]: string } = {} local t2: { [number]: number } = {} @@ -1668,7 +1660,6 @@ TEST_CASE_FIXTURE(Fixture, "right_table_missing_key") // Could be flaky if the fix has regressed. TEST_CASE_FIXTURE(Fixture, "right_table_missing_key2") { - ScopedFastFlag sff{FFlag::LuauSolverAgnosticStringification, true}; // CLI-114792 We don't report MissingProperties DOES_NOT_PASS_NEW_SOLVER_GUARD(); @@ -1692,7 +1683,6 @@ TEST_CASE_FIXTURE(Fixture, "right_table_missing_key2") TEST_CASE_FIXTURE(Fixture, "casting_unsealed_tables_with_props_into_table_with_indexer") { - ScopedFastFlag sff{FFlag::LuauSolverAgnosticStringification, true}; CheckResult result = check(R"( type StringToStringMap = { [string]: string } local rt: StringToStringMap = { ["foo"] = 1 } @@ -1722,7 +1712,6 @@ TEST_CASE_FIXTURE(Fixture, "casting_unsealed_tables_with_props_into_table_with_i TEST_CASE_FIXTURE(Fixture, "casting_sealed_tables_with_props_into_table_with_indexer") { - ScopedFastFlag sff{FFlag::LuauSolverAgnosticStringification, true}; CheckResult result = check(R"( type StringToStringMap = { [string]: string } function mkrt() return { ["foo"] = 1 } end @@ -1750,7 +1739,6 @@ TEST_CASE_FIXTURE(Fixture, "casting_tables_with_props_into_table_with_indexer2") TEST_CASE_FIXTURE(Fixture, "casting_tables_with_props_into_table_with_indexer3") { - ScopedFastFlag sff{FFlag::LuauSolverAgnosticStringification, true}; CheckResult result = check(R"( local function foo(a: {[string]: number, a: string}) end foo({ a = 1 }) @@ -1789,7 +1777,6 @@ TEST_CASE_FIXTURE(Fixture, "casting_tables_with_props_into_table_with_indexer4") TEST_CASE_FIXTURE(Fixture, "table_subtyping_with_missing_props_dont_report_multiple_errors") { - ScopedFastFlag sff{FFlag::LuauSolverAgnosticStringification, true}; CheckResult result = check(R"( function f(vec1: {x: number}): {x: number, y: number, z: number} return vec1 @@ -1847,8 +1834,6 @@ TEST_CASE_FIXTURE(Fixture, "table_subtyping_with_missing_props_dont_report_multi TEST_CASE_FIXTURE(Fixture, "table_subtyping_with_extra_props_dont_report_multiple_errors") { - ScopedFastFlag sff{FFlag::LuauSolverAgnosticStringification, true}; - CheckResult result = check(R"( function mkvec3() return {x = 1, y = 2, z = 3} end function mkvec1() return {x = 1} end @@ -1883,7 +1868,6 @@ TEST_CASE_FIXTURE(Fixture, "table_subtyping_with_extra_props_is_ok") TEST_CASE_FIXTURE(Fixture, "type_mismatch_on_massive_table_is_cut_short") { ScopedFastInt sfis{FInt::LuauTableTypeMaximumStringifierLength, 40}; - ScopedFastFlag sff{FFlag::LuauSolverAgnosticStringification, true}; CheckResult result = check(R"( @@ -2100,7 +2084,6 @@ TEST_CASE_FIXTURE(Fixture, "only_ascribe_synthetic_names_at_module_scope") TEST_CASE_FIXTURE(Fixture, "hide_table_error_properties") { - ScopedFastFlag sff{FFlag::LuauSolverAgnosticStringification, true}; CheckResult result = check(R"( --!strict @@ -2329,7 +2312,6 @@ TEST_CASE_FIXTURE(Fixture, "invariant_table_properties_means_instantiating_table // Old Solver Bug: We have to turn off InstantiateInSubtyping in the old solver as we don't invariantly // compare functions inside of table properties ScopedFastFlag sff[] = { - {FFlag::LuauSubtypingGenericsDoesntUseVariance, true}, {FFlag::LuauSubtypingReportGenericBoundMismatches2, true}, {FFlag::LuauInstantiateInSubtyping, FFlag::LuauSolverV2}, }; @@ -2370,6 +2352,10 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "table_insert_should_cope_with_optional_prope TEST_CASE_FIXTURE(BuiltinsFixture, "table_insert_should_cope_with_optional_properties_in_strict") { + ScopedFastFlag sffs[] = { + {FFlag::LuauSolverV2, true}, {FFlag::LuauNoScopeShallNotSubsumeAll, true}, {FFlag::LuauSubtypingReportGenericBoundMismatches2, true} + }; + CheckResult result = check(R"( --!strict local buttons = {} @@ -2378,11 +2364,9 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "table_insert_should_cope_with_optional_prope table.insert(buttons, { a = 3 }) )"); - // FIXME(CLI-165431): fixing subtyping revealed an overload selection problems - if (FFlag::LuauSolverV2 && FFlag::LuauNoScopeShallNotSubsumeAll) - LUAU_REQUIRE_ERROR_COUNT(4, result); - else - LUAU_REQUIRE_NO_ERRORS(result); + // FIXME(CLI-169950): fixing subtyping revealed an overload selection problem. + // fixing the overload selection problem revealed another subtyping problem + LUAU_REQUIRE_ERROR_COUNT(2, result); } TEST_CASE_FIXTURE(Fixture, "error_detailed_prop") @@ -2454,7 +2438,6 @@ Type 'number' could not be converted into 'string' in an invariant context)"; TEST_CASE_FIXTURE(BuiltinsFixture, "error_detailed_metatable_prop") { ScopedFastFlag sff[] = { - {FFlag::LuauSolverAgnosticStringification, true}, {FFlag::LuauInstantiateInSubtyping, true}, }; CheckResult result = check(R"( @@ -2627,7 +2610,6 @@ a.p = { x = 9 } TEST_CASE_FIXTURE(Fixture, "explicitly_typed_table_error") { - ScopedFastFlag sff{FFlag::LuauSolverAgnosticStringification, true}; CheckResult result = check(R"( --!strict type Super = { x : number } @@ -2676,7 +2658,6 @@ TEST_CASE_FIXTURE(Fixture, "explicitly_typed_table_with_indexer") TEST_CASE_FIXTURE(BuiltinsFixture, "recursive_metatable_type_call") { - ScopedFastFlag sff{FFlag::LuauSolverAgnosticStringification, true}; // CLI-114782 DOES_NOT_PASS_NEW_SOLVER_GUARD(); @@ -2801,6 +2782,11 @@ 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}, + }; + CheckResult result = check(R"( type T = {} & {p: number | string} local function f(t: T) @@ -2810,14 +2796,7 @@ TEST_CASE_FIXTURE(Fixture, "confusing_indexing") local foo = f({p = "string"}) )"); - if (FFlag::LuauSolverV2) - { - // CLI-114781 Bidirectional checking can't see through the intersection - LUAU_REQUIRE_ERROR_COUNT(1, result); - } - else - LUAU_REQUIRE_NO_ERRORS(result); - + LUAU_REQUIRE_NO_ERRORS(result); CHECK_EQ("number | string", toString(requireType("foo"))); } @@ -3422,7 +3401,6 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "table_simple_call") TEST_CASE_FIXTURE(BuiltinsFixture, "access_index_metamethod_that_returns_variadic") { - ScopedFastFlag sff{FFlag::LuauSolverAgnosticStringification, true}; CheckResult result = check(R"( type Foo = {x: string} local t = {} @@ -3481,7 +3459,6 @@ TEST_CASE_FIXTURE(Fixture, "dont_invalidate_the_properties_iterator_of_free_tabl TEST_CASE_FIXTURE(Fixture, "checked_prop_too_early") { - ScopedFastFlag sff{FFlag::LuauSolverAgnosticStringification, true}; CheckResult result = check(R"( local t: {x: number?}? = {x = nil} local u = t.x and t or 5 @@ -3503,7 +3480,6 @@ TEST_CASE_FIXTURE(Fixture, "checked_prop_too_early") TEST_CASE_FIXTURE(Fixture, "accidentally_checked_prop_in_opposite_branch") { - ScopedFastFlag sff{FFlag::LuauSolverAgnosticStringification, true}; CheckResult result = check(R"( local t: {x: number?}? = {x = nil} local u = t and t.x == 5 or t.x == 31337 @@ -3664,7 +3640,6 @@ TEST_CASE_FIXTURE(Fixture, "prop_access_on_key_whose_types_mismatches") TEST_CASE_FIXTURE(Fixture, "prop_access_on_unions_of_indexers_where_key_whose_types_mismatches") { - ScopedFastFlag sff{FFlag::LuauSolverAgnosticStringification, true}; CheckResult result = check(R"( local t: { [number]: number } | { [boolean]: number } = {} local u = t.x @@ -3862,7 +3837,6 @@ TEST_CASE_FIXTURE(Fixture, "invariant_table_properties_means_instantiating_table { ScopedFastFlag sff[] = { {FFlag::LuauInstantiateInSubtyping, true}, - {FFlag::LuauSolverAgnosticStringification, true}, }; CheckResult result = check(R"( @@ -3957,7 +3931,6 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "setmetatable_has_a_side_effect") TEST_CASE_FIXTURE(BuiltinsFixture, "tables_should_be_fully_populated") { - ScopedFastFlag sff{FFlag::LuauSolverAgnosticStringification, true}; CheckResult result = check(R"( local t = { x = 5 :: NonexistingTypeWhichEndsUpReturningAnErrorType, @@ -4292,7 +4265,6 @@ TEST_CASE_FIXTURE(Fixture, "cli_84607_missing_prop_in_array_or_dict") TEST_CASE_FIXTURE(Fixture, "simple_method_definition") { - ScopedFastFlag sff[] = {{FFlag::LuauSolverAgnosticStringification, true}}; CheckResult result = check(R"( local T = {} @@ -4421,7 +4393,6 @@ TEST_CASE_FIXTURE(Fixture, "new_solver_supports_read_write_properties") TEST_CASE_FIXTURE(Fixture, "table_subtyping_error_suppression") { - ScopedFastFlag sff{FFlag::LuauSolverAgnosticStringification, true}; CheckResult result = check(R"( function one(tbl: {x: any}) end function two(tbl: {x: string}) one(tbl) end -- ok, string <: any and any <: string @@ -4834,7 +4805,6 @@ TEST_CASE_FIXTURE(Fixture, "insert_a_and_f_of_a_into_table_res_in_a_loop") TEST_CASE_FIXTURE(BuiltinsFixture, "ipairs_adds_an_unbounded_indexer") { - ScopedFastFlag sff{FFlag::LuauSolverAgnosticStringification, true}; CheckResult result = check(R"( --!strict @@ -5266,7 +5236,6 @@ TEST_CASE_FIXTURE(Fixture, "empty_union_container_overflow") TEST_CASE_FIXTURE(Fixture, "inference_in_constructor") { - ScopedFastFlag sff{FFlag::LuauSolverAgnosticStringification, true}; LUAU_CHECK_NO_ERRORS(check(R"( local function new(y) local t: { x: number } = { x = y } @@ -5450,7 +5419,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::LuauPushTypeConstraint, true}}; + ScopedFastFlag sffs[] = {{FFlag::LuauSolverV2, true}, {FFlag::LuauPushTypeConstraint2, true}}; auto result = check(R"( type File = { @@ -5671,20 +5640,15 @@ TEST_CASE_FIXTURE(Fixture, "large_table_inference_does_not_bleed") CHECK(err.location.begin.line == 2); } - -#if 0 - -TEST_CASE_FIXTURE(Fixture, "extremely_large_table" * doctest::timeout(2.0)) +TEST_CASE_FIXTURE(Fixture, "extremely_large_table" * doctest::timeout(1.0)) { ScopedFastFlag _{FFlag::LuauSolverV2, true}; - const std::string source = "local res = {\n" + rep("\"foo\",\n", 100'000) + "}"; + const std::string source = "local res = {\n" + rep("\"foo\",\n", 10'000) + "}"; LUAU_REQUIRE_NO_ERRORS(check(source)); CHECK_EQ("{string}", toString(requireType("res"), {true})); } -#endif - TEST_CASE_FIXTURE(Fixture, "oss_1838") { LUAU_REQUIRE_NO_ERRORS(check(R"( @@ -5837,8 +5801,7 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "oss_1450") { ScopedFastFlag sffs[] = { {FFlag::LuauSolverV2, true}, - {FFlag::LuauUnifyShortcircuitSomeIntersectionsAndUnions, true}, - {FFlag::LuauPushTypeConstraint, true}, + {FFlag::LuauPushTypeConstraint2, true}, }; CheckResult results = check(R"( @@ -5998,8 +5961,6 @@ TEST_CASE_FIXTURE(Fixture, "free_types_with_sealed_table_upper_bounds_can_still_ TEST_CASE_FIXTURE(Fixture, "mixed_tables_are_ok_when_explicit") { - ScopedFastFlag _{FFlag::LuauAllowMixedTables, true}; - LUAU_REQUIRE_NO_ERRORS(check(R"( local foo: { [number | string]: unknown } = { Key = "sorry", @@ -6011,8 +5972,6 @@ TEST_CASE_FIXTURE(Fixture, "mixed_tables_are_ok_when_explicit") TEST_CASE_FIXTURE(Fixture, "mixed_tables_are_ok_for_any_key") { - ScopedFastFlag _{FFlag::LuauAllowMixedTables, true}; - LUAU_REQUIRE_NO_ERRORS(check(R"( local foo: { [any]: unknown } = { Key = "sorry", @@ -6051,7 +6010,7 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "oss_1935") TEST_CASE_FIXTURE(Fixture, "result_like_tagged_union") { - ScopedFastFlag _{FFlag::LuauPushTypeConstraint, true}; + ScopedFastFlag _{FFlag::LuauPushTypeConstraint2, true}; LUAU_REQUIRE_NO_ERRORS(check(R"( --!strict @@ -6073,7 +6032,8 @@ TEST_CASE_FIXTURE(Fixture, "oss_1924") { ScopedFastFlag sffs[] = { {FFlag::LuauSolverV2, true}, - {FFlag::LuauPushTypeConstraint, true}, + {FFlag::LuauPushTypeConstraint2, true}, + {FFlag::LuauPushTypeConstraintSingleton, true}, }; CheckResult result = check(R"( @@ -6086,12 +6046,12 @@ TEST_CASE_FIXTURE(Fixture, "oss_1924") auto err = get(result.errors[0]); REQUIRE(err); CHECK_EQ("\"s\"", toString(err->wantedType)); - CHECK_EQ("string", toString(err->givenType)); + CHECK_EQ("\"t\"", toString(err->givenType)); } TEST_CASE_FIXTURE(BuiltinsFixture, "cli_167052") { - ScopedFastFlag _{FFlag::LuauPushTypeConstraint, true}; + ScopedFastFlag _{FFlag::LuauPushTypeConstraint2, true}; LUAU_REQUIRE_NO_ERRORS(check(R"( local Children = newproxy() @@ -6121,4 +6081,267 @@ end LUAU_REQUIRE_NO_ERRORS(result); } +TEST_CASE_FIXTURE(BuiltinsFixture, "table_insert_any_and_true") +{ + CheckResult result = check(R"( + table.insert({} :: any, true) + )"); + + LUAU_REQUIRE_NO_ERRORS(result); +} + +TEST_CASE_FIXTURE(BuiltinsFixture, "table_insert_array_of_any") +{ + CheckResult result = check(R"( + table.insert({} :: { any }, 42) + )"); + + LUAU_REQUIRE_NO_ERRORS(result); +} + +TEST_CASE_FIXTURE(BuiltinsFixture, "bad_insert_type_mismatch") +{ + ScopedFastFlag sffs[] = { + {FFlag::LuauSolverV2, true}, {FFlag::LuauSubtypingReportGenericBoundMismatches2, true}, {FFlag::LuauNoScopeShallNotSubsumeAll, true} + }; + + CheckResult result = check(R"( + local function doInsert(t: { string }) + table.insert(t, true) + end + )"); + + LUAU_REQUIRE_ERROR_COUNT(1, result); + LUAU_REQUIRE_ERROR(result, GenericBoundsMismatch); +} + + +TEST_CASE_FIXTURE(Fixture, "string_indexer_satisfies_read_only_property") +{ + ScopedFastFlag _{FFlag::LuauSolverV2, true}; + + // NOTE: Unclear if this should be allowed, but for the type solver's + // current state I think it's reasonable. + LUAU_REQUIRE_NO_ERRORS(check(R"( + local function foo(t: { [string]: number }): { read X: number } + return t + end + )")); +} + +TEST_CASE_FIXTURE(Fixture, "bidirectional_inference_works_through_intersections") +{ + ScopedFastFlag sffs[] = { + {FFlag::LuauSolverV2, true}, + {FFlag::LuauPushTypeConstraint2, true}, + {FFlag::LuauPushTypeConstraintIntersection, true}, + }; + + LUAU_REQUIRE_NO_ERRORS(check(R"( + type x = {} & ({ state: "1" } | { state: "2" }) + local x: x = { state = "2" } + )")); +} + +TEST_CASE_FIXTURE(Fixture, "bidirectional_inference_intersection_other_intersection_example") +{ + ScopedFastFlag sffs[] = { + {FFlag::LuauSolverV2, true}, + {FFlag::LuauPushTypeConstraint2, true}, + {FFlag::LuauPushTypeConstraintIntersection, true}, + }; + + LUAU_REQUIRE_NO_ERRORS(check(R"( + type A = { foo: "a" } + type B = { bar: "b" } + type AB = A & B + local t: AB = { + foo = "a", + bar = "b", + } + )")); +} + +TEST_CASE_FIXTURE(Fixture, "do_not_force_on_simple_bidirectional_inference") +{ + ScopedFastFlag _{FFlag::DebugLuauAssertOnForcedConstraint, true}; + + LUAU_REQUIRE_NO_ERRORS(check(R"( + type Role = "Citizen" + + local function getRoles(): { Role } + return { 'Citizen' } + end + )")); +} + +TEST_CASE_FIXTURE(Fixture, "oss_2017") +{ + ScopedFastFlag _{FFlag::DebugLuauAssertOnForcedConstraint, true}; + + LUAU_REQUIRE_NO_ERRORS(check(R"( + --!strict + local class = {} + class.__index = class + + type Role = string + local ROLES_PLAYERS_REQUIRED: {[Role]: number} = {} + + function class.distributeRoles() + for _, role: Role in class.getAllRoles() do + local _ = ROLES_PLAYERS_REQUIRED[role] + end + end + + function class.getAllRoles(): { Role } + return { 'Citizen', 'Mafia', 'Detective', 'Bodyguard', 'Jester' } + end + + return class + )")); +} + +TEST_CASE_FIXTURE(Fixture, "oss_1953") +{ + ScopedFastFlag sff{FFlag::LuauSolverV2, true}; + + CheckResult result = check(R"( + type A = { kind: "a" } + type B = { kind: "b" } + + local function foo(fn: () -> A | B | T) + local v = fn() + return v and v.kind + end + )"); + + LUAU_REQUIRE_ERROR_COUNT(1, result); + auto err = get(result.errors[0]); + CHECK_EQ("kind", err->key); +} + +TEST_CASE_FIXTURE(Fixture, "array_of_callbacks_bidirectionally_inferred") +{ + ScopedFastFlag sffs[] = { + {FFlag::LuauSolverV2, true}, + {FFlag::LuauPushTypeConstraint2, true}, + {FFlag::LuauPushTypeConstraintLambdas, true}, + {FFlag::DebugLuauAssertOnForcedConstraint, true}, + }; + + LUAU_REQUIRE_NO_ERRORS(check(R"( + local Actions : { [string]: (string?) -> number? } = { + Foo = function (input) + if input then + return 42 + else + return nil + end + end + } + )")); + + CHECK_EQ("string?", toString(requireTypeAtPosition({3, 21}))); +} + +TEST_CASE_FIXTURE(BuiltinsFixture, "oss_1483") +{ + ScopedFastFlag sffs[] = { + {FFlag::LuauSolverV2, true}, + {FFlag::LuauPushTypeConstraint2, true}, + {FFlag::LuauPushTypeConstraintLambdas, true}, + {FFlag::DebugLuauAssertOnForcedConstraint, true}, + }; + + LUAU_REQUIRE_NO_ERRORS(check(R"( + type Form = "do-not-register" | (() -> ()) + + local function observer(register: () -> Form) end + + observer(function() + if math.random() > 0.5 then + return "do-not-register" + end + return function() end + end) + )")); +} + +TEST_CASE_FIXTURE(Fixture, "oss_1910") +{ + ScopedFastFlag sffs[] = { + {FFlag::LuauSolverV2, true}, + {FFlag::LuauPushTypeConstraint2, true}, + {FFlag::LuauPushTypeConstraintLambdas, true}, + {FFlag::DebugLuauAssertOnForcedConstraint, true}, + }; + + LUAU_REQUIRE_NO_ERRORS(check(R"( + type Implementation = { + on_thing: (something: boolean) -> (), + } + + local a: Implementation = { + on_thing = function(something) + local _ = something + end, + } + )")); + CHECK_EQ("boolean", toString(requireTypeAtPosition({7, 29}))); +} + +TEST_CASE_FIXTURE(BuiltinsFixture, "bidirectional_inference_variadic_type_pack") +{ + ScopedFastFlag sffs[] = { + {FFlag::LuauSolverV2, true}, + {FFlag::LuauPushTypeConstraint2, true}, + {FFlag::LuauPushTypeConstraintLambdas, true}, + {FFlag::DebugLuauAssertOnForcedConstraint, true}, + }; + + // As it turns out, you don't strictly need bidirectional inference in + // this specific case: subtyping is enough to constain `foobar` to be + // `string <: 'a <: string`, and generalization takes care of the rest, + // but you need to order the constraints correctly, otherwise we + // generalize the lambda too early. + LUAU_REQUIRE_NO_ERRORS(check(R"( + local foo: { (...string) -> () } = { + function (foobar) + print(foobar) + end + } + )")); + + CHECK_EQ("string", toString(requireTypeAtPosition({3, 24}))); +} + +TEST_CASE_FIXTURE(Fixture, "table_with_intersection_containing_lambda") +{ + ScopedFastFlag sffs[] = { + {FFlag::LuauSolverV2, true}, + {FFlag::LuauPushTypeConstraint2, true}, + {FFlag::LuauPushTypeConstraintLambdas, true}, + {FFlag::LuauPushTypeConstraintIntersection, true}, + {FFlag::DebugLuauAssertOnForcedConstraint, true}, + }; + + LUAU_REQUIRE_NO_ERRORS(check(R"( + type Arg = { foo: string } + + type TypeA = { foo: string, method: (arg: Arg) -> any } + + type TypeB = TypeA & {} + + local bar: TypeB = { + foo = "wow!", + method = function(arg) + local _ = arg + return nil + end, + } + )")); + + CHECK_EQ("{ foo: string }", toString(requireTypeAtPosition({10, 28}), { /* exhaustive */ true})); +} + TEST_SUITE_END(); diff --git a/tests/TypeInfer.test.cpp b/tests/TypeInfer.test.cpp index 327d53c4..f753ecdf 100644 --- a/tests/TypeInfer.test.cpp +++ b/tests/TypeInfer.test.cpp @@ -2,6 +2,7 @@ #include "Luau/AstQuery.h" #include "Luau/BuiltinDefinitions.h" +#include "Luau/Common.h" #include "Luau/Frontend.h" #include "Luau/Scope.h" #include "Luau/TypeInfer.h" @@ -14,6 +15,9 @@ #include +LUAU_DYNAMIC_FASTINT(LuauConstraintGeneratorRecursionLimit) +LUAU_DYNAMIC_FASTINT(LuauSubtypingRecursionLimit) + LUAU_FASTFLAG(LuauFixLocationSpanTableIndexExpr) LUAU_FASTFLAG(LuauSolverV2) LUAU_FASTFLAG(LuauInstantiateInSubtyping) @@ -23,15 +27,15 @@ LUAU_FASTINT(LuauRecursionLimit) LUAU_FASTINT(LuauTypeInferTypePackLoopLimit) LUAU_FASTINT(LuauTypeInferRecursionLimit) LUAU_FASTFLAG(LuauDfgAllowUpdatesInLoops) -LUAU_FASTFLAG(LuauInferActualIfElseExprType2) LUAU_FASTFLAG(DebugLuauMagicTypes) -LUAU_FASTFLAG(LuauReturnMappedGenericPacksFromSubtyping2) LUAU_FASTFLAG(LuauMissingFollowMappedGenericPacks) LUAU_FASTFLAG(LuauOccursCheckInCommit) LUAU_FASTFLAG(LuauEGFixGenericsList) -LUAU_FASTFLAG(LuauParametrizedAttributeSyntax) LUAU_FASTFLAG(LuauNoConstraintGenRecursionLimitIce) LUAU_FASTFLAG(LuauTryToOptimizeSetTypeUnification) +LUAU_FASTFLAG(LuauDontReferenceScopePtrFromHashTable) +LUAU_FASTFLAG(LuauConsiderErrorSuppressionInTypes) +LUAU_FASTFLAG(LuauMetatableAvoidSingletonUnion) using namespace Luau; @@ -294,18 +298,13 @@ TEST_CASE_FIXTURE(Fixture, "occurs_check_does_not_recurse_forever_if_asked_to_tr LUAU_REQUIRE_NO_ERRORS(result); } -#if 0 -// CLI-29798 TEST_CASE_FIXTURE(Fixture, "crazy_complexity") { CheckResult result = check(R"( --!nonstrict A:A():A():A():A():A():A():A():A():A():A():A() )"); - - MESSAGE("OK! Allocated ", typeChecker.types.size(), " types"); } -#endif TEST_CASE_FIXTURE(Fixture, "type_errors_infer_types") { @@ -421,6 +420,8 @@ TEST_CASE_FIXTURE(Fixture, "check_block_recursion_limit") ScopedFastInt luauRecursionLimit{FInt::LuauRecursionLimit, limit + 100}; ScopedFastInt luauCheckRecursionLimit{FInt::LuauCheckRecursionLimit, limit - 100}; + ScopedFastInt luauConstraintGeneratorRecursionLimit{DFInt::LuauConstraintGeneratorRecursionLimit, limit - 100}; + ScopedFastInt luauSubtypingRecursionLimit{DFInt::LuauSubtypingRecursionLimit, limit - 100}; CheckResult result = check(rep("do ", limit) + "local a = 1" + rep(" end", limit)); @@ -439,6 +440,8 @@ TEST_CASE_FIXTURE(Fixture, "check_expr_recursion_limit") #endif ScopedFastInt luauRecursionLimit{FInt::LuauRecursionLimit, limit + 100}; ScopedFastInt luauCheckRecursionLimit{FInt::LuauCheckRecursionLimit, limit - 100}; + ScopedFastInt luauConstraintGeneratorRecursionLimit{DFInt::LuauConstraintGeneratorRecursionLimit, limit - 100}; + ScopedFastInt luauSubtypingRecursionLimit{DFInt::LuauSubtypingRecursionLimit, limit - 100}; CheckResult result = check(R"(("foo"))" + rep(":lower()", limit)); @@ -666,7 +669,6 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "tc_after_error_recovery_no_replacement_name_ TEST_CASE_FIXTURE(BuiltinsFixture, "invalide_deprecated_attribute_doesn't_chrash_checker") { - ScopedFastFlag sff{FFlag::LuauParametrizedAttributeSyntax, true}; CheckResult result = check(R"( @[deprecated{ reason = reasonString }] function hello(x: number, y: number): number @@ -2082,8 +2084,6 @@ TEST_CASE_FIXTURE(Fixture, "fuzz_generalize_one_remove_type_assert_2") LUAU_REQUIRE_NO_ERROR(result, ConstraintSolvingIncompleteError); } -#if 0 - TEST_CASE_FIXTURE(BuiltinsFixture, "fuzz_simplify_combinatorial_explosion") { ScopedFastFlag sffs[] = { @@ -2101,8 +2101,6 @@ local _ )")); } -#endif - TEST_CASE_FIXTURE(BuiltinsFixture, "fuzz_missing_follow_table_freeze") { LUAU_REQUIRE_ERRORS(check(R"( @@ -2427,10 +2425,7 @@ end then _._G else ... TEST_CASE_FIXTURE(Fixture, "oss_1815_verbatim") { - ScopedFastFlag sffs[] = { - {FFlag::LuauSolverV2, true}, - {FFlag::LuauInferActualIfElseExprType2, true}, - }; + ScopedFastFlag sff{FFlag::LuauSolverV2, true}; CheckResult results = check(R"( --!strict @@ -2459,10 +2454,7 @@ TEST_CASE_FIXTURE(Fixture, "oss_1815_verbatim") TEST_CASE_FIXTURE(Fixture, "if_then_else_bidirectional_inference") { - ScopedFastFlag sffs[] = { - {FFlag::LuauSolverV2, true}, - {FFlag::LuauInferActualIfElseExprType2, true}, - }; + ScopedFastFlag sff{FFlag::LuauSolverV2, true}; CheckResult results = check(R"( type foo = { @@ -2480,10 +2472,7 @@ TEST_CASE_FIXTURE(Fixture, "if_then_else_bidirectional_inference") TEST_CASE_FIXTURE(Fixture, "if_then_else_two_errors") { - ScopedFastFlag sffs[] = { - {FFlag::LuauSolverV2, true}, - {FFlag::LuauInferActualIfElseExprType2, true}, - }; + ScopedFastFlag sff{FFlag::LuauSolverV2, true}; CheckResult results = check(R"( type foo = { @@ -2539,10 +2528,7 @@ TEST_CASE_FIXTURE(Fixture, "non_standalone_constraint_solving_incomplete_is_hidd TEST_CASE_FIXTURE(BuiltinsFixture, "fuzzer_missing_type_pack_follow") { - ScopedFastFlag sffs[] = { - {FFlag::LuauSolverV2, true}, - {FFlag::LuauReturnMappedGenericPacksFromSubtyping2, true}, - }; + ScopedFastFlag sffs{FFlag::LuauSolverV2, true}; LUAU_REQUIRE_ERRORS(check(R"( local _ = {[0]=_,} @@ -2579,6 +2565,7 @@ do end )")); } +#if 0 // CLI-166473: re-enable after flakiness is resolved TEST_CASE_FIXTURE(Fixture, "txnlog_checks_for_occurrence_before_self_binding_a_type") { ScopedFastFlag sff[] = {{FFlag::LuauSolverV2, false}, {FFlag::LuauOccursCheckInCommit, true}}; @@ -2621,12 +2608,14 @@ TEST_CASE_FIXTURE(Fixture, "txnlog_checks_for_occurrence_before_self_binding_a_t return f4 )"); } +#endif TEST_CASE_FIXTURE(Fixture, "constraint_generation_recursion_limit") { ScopedFastFlag sffs[] = {{FFlag::LuauSolverV2, true}, {FFlag::LuauNoConstraintGenRecursionLimitIce, true}}; // Lowers the recursion limit for the constraint generator ScopedFastInt i{FInt::LuauCheckRecursionLimit, 5}; + ScopedFastInt luauConstraintGeneratorRecursionLimit{DFInt::LuauConstraintGeneratorRecursionLimit, 5}; // This shouldn't ICE CheckResult result = check(R"( @@ -2664,25 +2653,75 @@ TEST_CASE_FIXTURE(Fixture, "nested_functions_can_depend_on_outer_generics") CHECK("number" == toString(tm->givenType)); } -TEST_CASE_FIXTURE(Fixture, "avoid_unification_inferring_never_for_refined_param") +TEST_CASE_FIXTURE(BuiltinsFixture, "unterminated_function_body_causes_constraint_generator_crash") { - ScopedFastFlag sffs[] = { - {FFlag::LuauSolverV2, true}, - {FFlag::LuauTryToOptimizeSetTypeUnification, true}, - }; + ScopedFastFlag _{FFlag::LuauDontReferenceScopePtrFromHashTable, true}; + // This should not crash + CheckResult result = check(R"( +export type t = { + func : typeof( + function + ) +} - LUAU_REQUIRE_NO_ERRORS(check(R"( - local function __remove(__: number?) end +export type t1 = t12 - function __removeItem(self, itemId: number) - local index = self.getItem(itemId) - if index then - __remove(index) - end - end - )")); +export type t2 = {} - CHECK_EQ("({ read getItem: (number) -> (number?, ...unknown) }, number) -> ()", toString(requireType("__removeItem"))); +export type t3 = { + foo:number + bar:number +} + +export type t4 = "foobar" + +export type t5 = string + +export type t6 = number + +export type t7 = "foobar" + +export type t8 = "foobar" + +export type t9 = typeof(1) + +export type t10 = typeof(1) + +export type t11 = typeof(1) + +export type t12 = { + b:number + pb:number +} +)"); +} + +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 + +local t: {[any]: any} = {} + +for k in t do + f(k) +end +)"); + + LUAU_REQUIRE_NO_ERRORS(result); +} + +TEST_CASE_FIXTURE(BuiltinsFixture, "fuzz_avoid_singleton_union") +{ + ScopedFastFlag _{FFlag::LuauMetatableAvoidSingletonUnion, true}; + + LUAU_REQUIRE_ERRORS(check(R"( +_ = if true then _ else {},if (_) then _ elseif "" then {} elseif _ then {} elseif _ then _ else {} +for l0,l2 in setmetatable(_,_),l0,_ do +end + )")); } TEST_SUITE_END(); diff --git a/tests/TypeInfer.tryUnify.test.cpp b/tests/TypeInfer.tryUnify.test.cpp index 1f08273c..459b2c67 100644 --- a/tests/TypeInfer.tryUnify.test.cpp +++ b/tests/TypeInfer.tryUnify.test.cpp @@ -13,7 +13,6 @@ using namespace Luau; LUAU_FASTFLAG(LuauSolverV2); LUAU_FASTFLAG(LuauUnifierRecursionOnRestart); -LUAU_FASTFLAG(LuauSolverAgnosticStringification) struct TryUnifyFixture : Fixture { @@ -276,7 +275,6 @@ TEST_CASE_FIXTURE(Fixture, "variadics_should_use_reversed_properly") TEST_CASE_FIXTURE(BuiltinsFixture, "cli_41095_concat_log_in_sealed_table_unification") { - ScopedFastFlag sff{FFlag::LuauSolverAgnosticStringification, true}; CheckResult result = check(R"( --!strict diff --git a/tests/TypeInfer.typePacks.test.cpp b/tests/TypeInfer.typePacks.test.cpp index ffa680e9..fba8485b 100644 --- a/tests/TypeInfer.typePacks.test.cpp +++ b/tests/TypeInfer.typePacks.test.cpp @@ -12,8 +12,6 @@ using namespace Luau; LUAU_FASTFLAG(LuauSolverV2) LUAU_FASTFLAG(LuauInstantiateInSubtyping) -LUAU_FASTFLAG(LuauSolverAgnosticStringification) -LUAU_FASTFLAG(LuauRemoveGenericErrorForParams) LUAU_FASTFLAG(LuauAddErrorCaseForIncompatibleTypePacks) TEST_SUITE_BEGIN("TypePackTests"); @@ -260,7 +258,6 @@ TEST_CASE_FIXTURE(Fixture, "variadic_pack_syntax") CHECK_EQ(toString(requireType("foo")), "(...number) -> ()"); } -#if 0 TEST_CASE_FIXTURE(Fixture, "type_pack_hidden_free_tail_infinite_growth") { CheckResult result = check(R"( @@ -277,7 +274,6 @@ end LUAU_REQUIRE_ERRORS(result); } -#endif TEST_CASE_FIXTURE(Fixture, "variadic_argument_tail") { @@ -294,8 +290,6 @@ end TEST_CASE_FIXTURE(Fixture, "type_alias_type_packs") { - ScopedFastFlag sff{FFlag::LuauSolverAgnosticStringification, true}; - CheckResult result = check(R"( type Packed = (T...) -> T... local a: Packed<> @@ -360,8 +354,6 @@ local c: Packed TEST_CASE_FIXTURE(BuiltinsFixture, "type_alias_type_packs_import") { - ScopedFastFlag sff{FFlag::LuauSolverAgnosticStringification, true}; - fileResolver.source["game/A"] = R"( export type Packed = { a: T, b: (U...) -> () } return {} @@ -393,7 +385,6 @@ local d: { a: typeof(c) } TEST_CASE_FIXTURE(BuiltinsFixture, "type_pack_type_parameters") { - ScopedFastFlag sff{FFlag::LuauSolverAgnosticStringification, true}; fileResolver.source["game/A"] = R"( export type Packed = { a: T, b: (U...) -> () } return {} @@ -622,8 +613,6 @@ type Other = Packed TEST_CASE_FIXTURE(Fixture, "type_alias_instantiated_but_missing_parameter_list") { - ScopedFastFlag sff{FFlag::LuauRemoveGenericErrorForParams, true}; - CheckResult result = check(R"( type Packed = (T...) -> T... local a: Packed @@ -814,8 +803,6 @@ TEST_CASE_FIXTURE(Fixture, "type_alias_default_type_errors3") TEST_CASE_FIXTURE(Fixture, "type_alias_default_type_errors4") { - ScopedFastFlag sff{FFlag::LuauRemoveGenericErrorForParams, true}; - CheckResult result = check(R"( type Packed = (T) -> T local a: Packed @@ -921,7 +908,6 @@ type C = A TEST_CASE_FIXTURE(Fixture, "type_alias_defaults_recursive_type") { - ScopedFastFlag sff{FFlag::LuauSolverAgnosticStringification, true}; CheckResult result = check(R"( type F ()> = (K) -> V type R = { m: F } diff --git a/tests/TypeInfer.typestates.test.cpp b/tests/TypeInfer.typestates.test.cpp index 5046c5b3..6fd2a584 100644 --- a/tests/TypeInfer.typestates.test.cpp +++ b/tests/TypeInfer.typestates.test.cpp @@ -6,7 +6,6 @@ LUAU_FASTFLAG(LuauSolverV2) LUAU_FASTFLAG(LuauRefineDistributesOverUnions) LUAU_FASTFLAG(LuauReduceSetTypeStackPressure) -LUAU_FASTFLAG(LuauSolverAgnosticStringification) using namespace Luau; @@ -123,6 +122,7 @@ TEST_CASE_FIXTURE(TypeStateFixture, "refine_a_local_and_then_assign_it") LUAU_REQUIRE_NO_ERRORS(result); } +#endif TEST_CASE_FIXTURE(TypeStateFixture, "assign_a_local_and_then_refine_it") { @@ -139,7 +139,6 @@ 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])); } -#endif TEST_CASE_FIXTURE(TypeStateFixture, "recursive_local_function") { @@ -584,7 +583,6 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "oss_1547") TEST_CASE_FIXTURE(Fixture, "modify_captured_table_field") { - ScopedFastFlag sff{FFlag::LuauSolverAgnosticStringification, true}; LUAU_REQUIRE_NO_ERRORS(check(R"( local state = { x = 0 } function incr() diff --git a/tests/TypeInfer.unionTypes.test.cpp b/tests/TypeInfer.unionTypes.test.cpp index 7f160bfe..7e04ab76 100644 --- a/tests/TypeInfer.unionTypes.test.cpp +++ b/tests/TypeInfer.unionTypes.test.cpp @@ -10,8 +10,6 @@ using namespace Luau; LUAU_FASTFLAG(LuauSolverV2) -LUAU_FASTFLAG(LuauSolverAgnosticStringification) - TEST_SUITE_BEGIN("UnionTypes"); TEST_CASE_FIXTURE(Fixture, "fuzzer_union_with_one_part_assertion") @@ -403,7 +401,6 @@ TEST_CASE_FIXTURE(Fixture, "optional_assignment_errors") TEST_CASE_FIXTURE(Fixture, "optional_assignment_errors_2") { - ScopedFastFlag sff{FFlag::LuauSolverAgnosticStringification, true}; CheckResult result = check(R"( type A = { x: number } & { y: number } function f(a: A?) @@ -524,7 +521,6 @@ local oh : boolean = t.y TEST_CASE_FIXTURE(Fixture, "error_detailed_union_part") { - ScopedFastFlag sff{FFlag::LuauSolverAgnosticStringification, true}; CheckResult result = check(R"( type X = { x: number } type Y = { y: number } @@ -897,9 +893,7 @@ TEST_CASE_FIXTURE(Fixture, "less_greedy_unification_with_union_types") LUAU_REQUIRE_NO_ERRORS(result); - CHECK_EQ( - "(({ read x: a } & { x: number }) | ({ read x: a } & { x: string })) -> { x: number } | { x: string }", toString(requireType("f")) - ); + CHECK_EQ("(({ read x: a } & { x: number }) | ({ read x: a } & { x: string })) -> { x: number } | { x: string }", toString(requireType("f"))); } TEST_CASE_FIXTURE(Fixture, "less_greedy_unification_with_union_types_2") diff --git a/tests/TypeInfer.unknownnever.test.cpp b/tests/TypeInfer.unknownnever.test.cpp index 1bae4837..0ccb7930 100644 --- a/tests/TypeInfer.unknownnever.test.cpp +++ b/tests/TypeInfer.unknownnever.test.cpp @@ -7,6 +7,7 @@ using namespace Luau; LUAU_FASTFLAG(LuauSolverV2); +LUAU_FASTFLAG(LuauNoOrderingTypeFunctions) TEST_SUITE_BEGIN("TypeInferUnknownNever"); @@ -327,6 +328,8 @@ TEST_CASE_FIXTURE(Fixture, "length_of_never") TEST_CASE_FIXTURE(Fixture, "dont_unify_operands_if_one_of_the_operand_is_never_in_any_ordering_operators") { + ScopedFastFlag _{FFlag::LuauNoOrderingTypeFunctions, true}; + CheckResult result = check(R"( local function ord(x: nil, y) return x ~= nil and x > y @@ -334,17 +337,11 @@ TEST_CASE_FIXTURE(Fixture, "dont_unify_operands_if_one_of_the_operand_is_never_i )"); + LUAU_REQUIRE_NO_ERRORS(result); if (FFlag::LuauSolverV2) - { - LUAU_REQUIRE_ERROR_COUNT(1, result); - CHECK(get(result.errors[0])); - CHECK_EQ("(nil, a) -> false | le", toString(requireType("ord"))); - } + CHECK_EQ("(nil, nil & ~nil) -> boolean", toString(requireType("ord"))); else - { - LUAU_REQUIRE_NO_ERRORS(result); CHECK_EQ("(nil, a) -> boolean", toString(requireType("ord"))); - } } TEST_CASE_FIXTURE(Fixture, "math_operators_and_never") diff --git a/tests/TypePath.test.cpp b/tests/TypePath.test.cpp index 93623f8b..9ab1be0d 100644 --- a/tests/TypePath.test.cpp +++ b/tests/TypePath.test.cpp @@ -18,13 +18,9 @@ using namespace Luau::TypePath; LUAU_FASTFLAG(LuauSolverV2); LUAU_DYNAMIC_FASTINT(LuauTypePathMaximumTraverseSteps); -LUAU_FASTFLAG(LuauReturnMappedGenericPacksFromSubtyping2); -LUAU_FASTFLAG(LuauSubtypingGenericPacksDoesntUseVariance) - struct TypePathFixture : Fixture { ScopedFastFlag sff1{FFlag::LuauSolverV2, true}; - ScopedFastFlag sff2{FFlag::LuauSubtypingGenericPacksDoesntUseVariance, true}; TypeArena arena; const DenseHashMap emptyMap_DEPRECATED{nullptr}; }; @@ -32,7 +28,6 @@ struct TypePathFixture : Fixture struct TypePathBuiltinsFixture : BuiltinsFixture { ScopedFastFlag sff1{FFlag::LuauSolverV2, true}; - ScopedFastFlag sff2{FFlag::LuauSubtypingGenericPacksDoesntUseVariance, true}; TypeArena arena; const DenseHashMap emptyMap_DEPRECATED{nullptr}; }; @@ -131,7 +126,6 @@ TEST_CASE_FIXTURE(TypePathFixture, "table_property") TEST_CASE_FIXTURE(ExternTypeFixture, "class_property") { - ScopedFastFlag sff{FFlag::LuauSubtypingGenericPacksDoesntUseVariance, true}; // Force this here because vector2InstanceType won't get initialized until the frontend has been forced getFrontend(); TypeArena arena; @@ -231,8 +225,6 @@ TEST_CASE_FIXTURE(TypePathFixture, "index") TEST_CASE_FIXTURE(ExternTypeFixture, "metatables") { - ScopedFastFlag sff{FFlag::LuauSubtypingGenericPacksDoesntUseVariance, true}; - getFrontend(); TypeArena arena; @@ -470,8 +462,6 @@ TEST_CASE_FIXTURE(TypePathFixture, "tail") TEST_CASE_FIXTURE(TypePathFixture, "pack_slice_has_tail") { - ScopedFastFlag _{FFlag::LuauReturnMappedGenericPacksFromSubtyping2, true}; - TypeArena& arena = getFrontend().globals.globalTypes; unfreeze(arena); @@ -487,8 +477,6 @@ TEST_CASE_FIXTURE(TypePathFixture, "pack_slice_has_tail") TEST_CASE_FIXTURE(TypePathFixture, "pack_slice_finite_pack") { - ScopedFastFlag _{FFlag::LuauReturnMappedGenericPacksFromSubtyping2, true}; - TypeArena& arena = getFrontend().globals.globalTypes; unfreeze(arena); diff --git a/tests/TypeVar.test.cpp b/tests/TypeVar.test.cpp index 220dc749..c823bc72 100644 --- a/tests/TypeVar.test.cpp +++ b/tests/TypeVar.test.cpp @@ -11,7 +11,6 @@ using namespace Luau; -LUAU_FASTFLAG(LuauSolverAgnosticStringification) TEST_SUITE_BEGIN("TypeTests"); @@ -221,7 +220,6 @@ TEST_CASE_FIXTURE(Fixture, "UnionTypeIterator_with_only_cyclic_union") */ TEST_CASE_FIXTURE(Fixture, "substitution_skip_failure") { - ScopedFastFlag sff{FFlag::LuauSolverAgnosticStringification, true}; Type ftv11{FreeType{TypeLevel{}, getBuiltins()->neverType, getBuiltins()->unknownType}}; TypePackVar tp24{TypePack{{&ftv11}}}; diff --git a/tests/Unifier2.test.cpp b/tests/Unifier2.test.cpp index 2e3bf96c..a8d7fdfd 100644 --- a/tests/Unifier2.test.cpp +++ b/tests/Unifier2.test.cpp @@ -133,40 +133,31 @@ TEST_CASE_FIXTURE(Unifier2Fixture, "unify_binds_free_supertype_tail_pack") CHECK("(number <: 'a)" == toString(freeAndFree)); } -TEST_CASE_FIXTURE(Unifier2Fixture, "unify_avoid_free_type_intersection_in_ub_from_union") +TEST_CASE_FIXTURE(Unifier2Fixture, "unify_free_type_intersection_in_ub_from_union") { - ScopedFastFlag _{FFlag::LuauTryToOptimizeSetTypeUnification, true}; // 'a TypeId freeTy = arena.addType(FreeType{&scope, builtinTypes.neverType, builtinTypes.unknownType}); // 'a & ~(false?) - TypeId subTy = arena.addType(IntersectionType{{ freeTy, builtinTypes.truthyType}}); + TypeId subTy = arena.addType(IntersectionType{{freeTy, builtinTypes.truthyType}}); // number? TypeId superTy = arena.addType(UnionType{{builtinTypes.numberType, builtinTypes.nilType}}); u2.unify(subTy, superTy); - CHECK("('a <: number?)" == toString(freeTy)); + // TODO CLI-168953: This is not correct. We should not be unifying to `never` here. + CHECK("('a <: never)" == toString(freeTy)); } -TEST_CASE_FIXTURE(Unifier2Fixture, "unify_unfortunate_free_type_lb_from_intersection") +TEST_CASE_FIXTURE(Unifier2Fixture, "unify_free_type_lb_from_intersection") { - ScopedFastFlag _{FFlag::LuauTryToOptimizeSetTypeUnification, true}; // 'a TypeId freeTy = arena.addType(FreeType{&scope, builtinTypes.neverType, builtinTypes.unknownType}); // 'a? - TypeId superTy = arena.addType(UnionType{{ freeTy, builtinTypes.nilType}}); + TypeId superTy = arena.addType(UnionType{{freeTy, builtinTypes.nilType}}); // string & ~"foo" - TypeId subTy = arena.addType(IntersectionType{{builtinTypes.stringType, arena.addType( - NegationType{ - arena.addType(SingletonType{StringSingleton{"foo"}}) - } - )}}); + TypeId subTy = + arena.addType(IntersectionType{{builtinTypes.stringType, arena.addType(NegationType{arena.addType(SingletonType{StringSingleton{"foo"}})})}}); u2.unify(subTy, superTy); - - // TODO CLI-168953: This is not correct. The lower bound should be - // string & ~"foo", but we're making this tradeoff for now to avoid - // the more common case where the upper bound becomes `never` (see - // previous test case). - CHECK("(string | ~\"foo\" <: 'a)" == toString(freeTy)); + CHECK("(string & ~\"foo\" <: 'a)" == toString(freeTy)); } TEST_SUITE_END(); diff --git a/tests/conformance/apicalls.luau b/tests/conformance/apicalls.luau index 8db62d96..3982e5a8 100644 --- a/tests/conformance/apicalls.luau +++ b/tests/conformance/apicalls.luau @@ -30,4 +30,8 @@ function oops() return "oops" end +function getnresults(n) + return table.unpack(table.create(n, 1)) +end + return('OK') diff --git a/tests/conformance/cyield.luau b/tests/conformance/cyield.luau index 157d9176..15df83b3 100644 --- a/tests/conformance/cyield.luau +++ b/tests/conformance/cyield.luau @@ -132,4 +132,51 @@ passthroughcheckyield(passthroughCallArgReuse) passthroughcheckyield(passthroughCallVaradic) passthroughcheckyield(passthroughCallWithState) +-- yieldable C function that acts as a pass-through (regular, pcall to Luau) +local function nonyieldablepcall(x, y) + local status, result = pcall(nonyieldable, x, y) + assert(status) + return result +end + +local function passthroughcheckpcall(f) + local x = f(nonyieldablepcall, 1, 2) + assert(x == 0.5) +end + +passthroughcheckpcall(passthroughCall) +passthroughcheckpcall(passthroughCallMoreResults) +passthroughcheckpcall(passthroughCallArgReuse) +passthroughcheckpcall(passthroughCallVaradic) +passthroughcheckpcall(passthroughCallWithState) + +-- yieldable C function that acts as a pass-through (regular, pcall to C) +local function nonyieldablepcallc(x, y) + local status, result = pcall(assert, x / y) + assert(status) + return result +end + +local function passthroughcheckpcallc(f) + local x = f(nonyieldablepcallc, 1, 2) + assert(x == 0.5) +end + +passthroughcheckpcallc(passthroughCall) +passthroughcheckpcallc(passthroughCallMoreResults) +passthroughcheckpcallc(passthroughCallArgReuse) +passthroughcheckpcallc(passthroughCallVaradic) +passthroughcheckpcallc(passthroughCallWithState) + +-- yieldable C function that acts as a pass-through (regular, direct pcall) +local function passthroughcheckpcallcdirect(f) + local status, x = f(pcall, function(y) return 1 / y end, 2) + assert(status) + assert(x == 0.5) +end + +-- only variadic functions are tested here because pcall returns status as well +passthroughcheckpcallcdirect(passthroughCallVaradic) +passthroughcheckpcallcdirect(passthroughCallWithState) + return "OK" diff --git a/tests/conformance/errors.luau b/tests/conformance/errors.luau index 57d2b693..ef00e13a 100644 --- a/tests/conformance/errors.luau +++ b/tests/conformance/errors.luau @@ -139,7 +139,7 @@ X=2;assert(lineerror(p) == 1) lineerror = nil -if not limitedstack then +do C = 0 -- local l = debug.getinfo(1, "l").currentline function y () C=C+1; y() end @@ -264,7 +264,7 @@ local function f (x) end end -if not limitedstack then +do f(3) end diff --git a/tests/conformance/native.luau b/tests/conformance/native.luau index f0fe3ce6..59d90edb 100644 --- a/tests/conformance/native.luau +++ b/tests/conformance/native.luau @@ -248,6 +248,13 @@ end assert(pcall(fuzzfail24) == false) +local function fuzzfail25(...) + _,_.n0,_,_,_[_ // _],_[_ + _ % _],_,_,_,_,l255,_,_,_,_,_,_,_,_,_[_],_,_,_ = table.insert("") + _,_.n0,_,_,_[_ // _],_[_ + _ % _],_,_,_,_,l255,_,_,_,_,_,_,_,_,_[_],_,_,_ = table.insert("") +end + +assert(pcall(fuzzfail25) == false) + local function arraySizeInv1() local t = {1, 2, nil, nil, nil, nil, nil, nil, nil, true} @@ -674,4 +681,48 @@ end assert(splitStoreRestore(2).ad() == 15) +local function guard1(a) + local c = 0 + if type(a) == "number" then + c = a * 2 + end + return c +end + +assert(guard1(4) == 8) +assert(guard1({}) == 0) + +local function guard2(a) + local c = 0 + if type(a) ~= "number" then + c = 4 + end + return c +end + +assert(guard2(4) == 0) +assert(guard2({}) == 4) + +local function guard3(a) + local c = 0 + if typeof(a) == "number" then + c = a * 2 + end + return c +end + +assert(guard3(4) == 8) +assert(guard3({}) == 0) + +local function guard4(a) + local c = 0 + if typeof(a) ~= "number" then + c = 4 + end + return c +end + +assert(guard4(4) == 0) +assert(guard4({}) == 4) + return('OK') diff --git a/tests/conformance/pcall.luau b/tests/conformance/pcall.luau index 9e691e2e..16a0fb87 100644 --- a/tests/conformance/pcall.luau +++ b/tests/conformance/pcall.luau @@ -56,14 +56,14 @@ checkresults({ true, 42 }, pcall(setmetatable({}, { __call = function(self, arg) checkerror(pcall(function() local a = nil / 5 end)) checkerror(pcall(function() select(-100) end)) -if not limitedstack then +do -- complex error tests - stack overflow, and stack overflow through pcall function stackinfinite() return stackinfinite() end checkerror(pcall(stackinfinite)) function stackover() return pcall(stackover) end local res = {pcall(stackover)} - assert(#res == 200) -- stack limit (MAXCCALLS) is 200 + assert(#res == 10000) -- stack limit (LUAI_MAXCALLS is 20000 and we use 2 calls per recursion) end -- yield tests @@ -292,7 +292,26 @@ if not limitedstack then coroutine.wrap(foo)() -- call another coroutine end checkerror(pcall(foo)) -- triggers C stack overflow - assert(count + 1 == 200) -- stack limit (MAXCCALLS) is 200, -1 for first pcall + assert(count == 200) -- stack limit (MAXCCALLS) is 200 +end + +-- protected and regular calls without C stack limit +do + local function regularadd(depth: number, a: number, b: number): number + if depth == 0 then return 1 end + + return a + regularadd(depth - 1, b, a + b) + end + + local function protectedadd(depth: number, a: number, b: number): number + if depth == 0 then return 1 end + + local result, s = pcall(protectedadd, depth - 1, b, a + b) + assert(result) + return a + s + end + + assert(regularadd(2000, 0, 1) == protectedadd(2000, 0, 1)) end return 'OK' diff --git a/tests/conformance/stringinterp.luau b/tests/conformance/stringinterp.luau index efb25bae..7e2b5495 100644 --- a/tests/conformance/stringinterp.luau +++ b/tests/conformance/stringinterp.luau @@ -56,4 +56,6 @@ assertEq(shadowsString(1), "Value is 1") assertEq(`\u{0041}\t`, "A\t") +assertEq(`{"5"} + {7} = 12`, "5 + 7 = 12") + return "OK" diff --git a/tests/conformance/tmerror.luau b/tests/conformance/tmerror.luau index fef077ea..0af0dead 100644 --- a/tests/conformance/tmerror.luau +++ b/tests/conformance/tmerror.luau @@ -1,41 +1,78 @@ -- This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details --- This file is based on Lua 5.x tests -- https://github.com/lua/lua/tree/master/testes --- Generate an error (i.e. throw an exception) inside a tag method which is indirectly --- called via pcall. +-- Generate an error (i.e. throw an exception) inside a tag method which is indirectly called via pcall. -- This test is meant to detect a regression in handling errors inside a tag method +local status, result + local testtable = {} -setmetatable(testtable, { __index = function() error("Error") end }) +setmetatable(testtable, { + __index = function() + error("Error") + end +}) -pcall(function() - testtable.missingmethod() +status, result = pcall(function() + testtable.missingmethod() end) +assert(status == false) +assert(string.find(result, ":11: Error") ~= nil) -- local testtable2 = {} -setmetatable(testtable2, { __index = function() pcall(function() error("Error") end) end }) +setmetatable(testtable2, { + __index = function() + local status, result = pcall(function() + error("Error") + end) + assert(status == false) + assert(string.find(result, ":26: Error") ~= nil) + return nil + end +}) local m2 = testtable2.missingmethod -pcall(function() - testtable2.missingmethod() +status, result = pcall(function() + testtable2.missingmethod() end) +assert(status == false) +assert(string.find(result, ":37: attempt to call a nil value") ~= nil) -- local testtable3 = {} -setmetatable(testtable3, { __index = function() pcall(error, "Error") end }) +setmetatable(testtable3, { __index = function() + local status, result = pcall(error, "Error") + assert(status == false) + assert(result == "Error") + return nil +end }) local m3 = testtable3.missingmethod -pcall(function() - testtable3.missingmethod() +status, result = pcall(function() + testtable3.missingmethod() end) +assert(status == false) +assert(string.find(result, ":54: attempt to call a nil value") ~= nil) -- local testtable4 = {} -setmetatable(testtable4, { __index = function() pcall(error) end }) +setmetatable(testtable4, { + __index = function() + local status, result = pcall(error) + assert(status == false) + assert(result == nil) + return nil + end +}) local m4 = testtable4.missingmember +status, result = pcall(function() + testtable4.missingmethod() +end) +assert(status == false) +assert(string.find(result, ":73: attempt to call a nil value") ~= nil) + return('OK') diff --git a/tests/conformance/vector_library.luau b/tests/conformance/vector_library.luau index faafb60d..3e3f7688 100644 --- a/tests/conformance/vector_library.luau +++ b/tests/conformance/vector_library.luau @@ -182,6 +182,7 @@ assert(vector.lerp(vector.one, vector.zero, 0.5) == vector.create(0.5, 0.5, 0.5) assert(vector.lerp(vector.one, vector.zero, 0.5) == vector.create(0.5, 0.5, 0.5)) assert(vector.lerp(vector.create(1, 2, 3), vector.create(3, 2, 1), 0.5) == vector.create(2, 2, 2)) assert(vector.lerp(vector.create(-1, -1, -3), vector.zero, 0.5) == vector.create(-0.5, -0.5, -1.5)) +assert(vector.lerp(vector.create(10, 8, 3), vector.create(-3, 8, 3), 0.43) == vector.create(4.41, 8, 3)) assert(ecall(function() return vector.lerp(vector.zero, vector.one, vector.one) end) == "invalid argument #3 to 'lerp' (number expected, got vector)") -- validate component access diff --git a/tests/main.cpp b/tests/main.cpp index d661b5bf..1f908b61 100644 --- a/tests/main.cpp +++ b/tests/main.cpp @@ -59,9 +59,6 @@ static bool skipFastFlag(const char* flagName) if (strncmp(flagName, "Debug", 5) == 0) return true; - if (strcmp(flagName, "StudioReportLuauAny2") == 0) - return true; - return false; } diff --git a/tests/require/config_tests/README.md b/tests/require/config_tests/README.md new file mode 100644 index 00000000..02689969 --- /dev/null +++ b/tests/require/config_tests/README.md @@ -0,0 +1 @@ +The `with_config` and `with_config_luau` directories should be identical, apart from the configuration file type they use. diff --git a/tests/require/config_tests/config_ambiguity/.config.luau b/tests/require/config_tests/config_ambiguity/.config.luau new file mode 100644 index 00000000..27af9236 --- /dev/null +++ b/tests/require/config_tests/config_ambiguity/.config.luau @@ -0,0 +1,7 @@ +return { + luau = { + aliases = { + dep = "./dependency" + } + } +} diff --git a/tests/require/config_tests/config_ambiguity/.luaurc b/tests/require/config_tests/config_ambiguity/.luaurc new file mode 100644 index 00000000..90fe77c2 --- /dev/null +++ b/tests/require/config_tests/config_ambiguity/.luaurc @@ -0,0 +1,5 @@ +{ + "aliases": { + "dep": "./dependency" + } +} diff --git a/tests/require/with_config/src/dependency.luau b/tests/require/config_tests/config_ambiguity/dependency.luau similarity index 100% rename from tests/require/with_config/src/dependency.luau rename to tests/require/config_tests/config_ambiguity/dependency.luau diff --git a/tests/require/with_config/src/alias_requirer.luau b/tests/require/config_tests/config_ambiguity/requirer.luau similarity index 100% rename from tests/require/with_config/src/alias_requirer.luau rename to tests/require/config_tests/config_ambiguity/requirer.luau diff --git a/tests/require/config_tests/config_cannot_be_required/.config.luau b/tests/require/config_tests/config_cannot_be_required/.config.luau new file mode 100644 index 00000000..a3cb3505 --- /dev/null +++ b/tests/require/config_tests/config_cannot_be_required/.config.luau @@ -0,0 +1 @@ +return {"result from .config.luau"} diff --git a/tests/require/config_tests/config_cannot_be_required/requirer.luau b/tests/require/config_tests/config_cannot_be_required/requirer.luau new file mode 100644 index 00000000..38c00431 --- /dev/null +++ b/tests/require/config_tests/config_cannot_be_required/requirer.luau @@ -0,0 +1 @@ +return require("./.config") diff --git a/tests/require/config_tests/config_luau_timeout/.config.luau b/tests/require/config_tests/config_luau_timeout/.config.luau new file mode 100644 index 00000000..9c128d7e --- /dev/null +++ b/tests/require/config_tests/config_luau_timeout/.config.luau @@ -0,0 +1,2 @@ +-- Infinite loop +while true do end diff --git a/tests/require/with_config/src/submodule/init.luau b/tests/require/config_tests/config_luau_timeout/requirer.luau similarity index 100% rename from tests/require/with_config/src/submodule/init.luau rename to tests/require/config_tests/config_luau_timeout/requirer.luau diff --git a/tests/require/with_config/.luaurc b/tests/require/config_tests/with_config/.luaurc similarity index 100% rename from tests/require/with_config/.luaurc rename to tests/require/config_tests/with_config/.luaurc diff --git a/tests/require/config_tests/with_config/parent_ambiguity/.luaurc b/tests/require/config_tests/with_config/parent_ambiguity/.luaurc new file mode 100644 index 00000000..24f8e9ff --- /dev/null +++ b/tests/require/config_tests/with_config/parent_ambiguity/.luaurc @@ -0,0 +1,5 @@ +{ + "aliases": { + "foo": "./foo", + } +} diff --git a/tests/require/config_tests/with_config/parent_ambiguity/folder.luau b/tests/require/config_tests/with_config/parent_ambiguity/folder.luau new file mode 100644 index 00000000..e69de29b diff --git a/tests/require/config_tests/with_config/parent_ambiguity/folder/requirer.luau b/tests/require/config_tests/with_config/parent_ambiguity/folder/requirer.luau new file mode 100644 index 00000000..2f613057 --- /dev/null +++ b/tests/require/config_tests/with_config/parent_ambiguity/folder/requirer.luau @@ -0,0 +1 @@ +return require("@foo") diff --git a/tests/require/config_tests/with_config/parent_ambiguity/foo.luau b/tests/require/config_tests/with_config/parent_ambiguity/foo.luau new file mode 100644 index 00000000..aa00aca1 --- /dev/null +++ b/tests/require/config_tests/with_config/parent_ambiguity/foo.luau @@ -0,0 +1 @@ +return { "result from foo" } diff --git a/tests/require/with_config/src/.luaurc b/tests/require/config_tests/with_config/src/.luaurc similarity index 100% rename from tests/require/with_config/src/.luaurc rename to tests/require/config_tests/with_config/src/.luaurc diff --git a/tests/require/config_tests/with_config/src/alias_requirer.luau b/tests/require/config_tests/with_config/src/alias_requirer.luau new file mode 100644 index 00000000..4375a783 --- /dev/null +++ b/tests/require/config_tests/with_config/src/alias_requirer.luau @@ -0,0 +1 @@ +return require("@dep") diff --git a/tests/require/config_tests/with_config/src/dependency.luau b/tests/require/config_tests/with_config/src/dependency.luau new file mode 100644 index 00000000..07466f42 --- /dev/null +++ b/tests/require/config_tests/with_config/src/dependency.luau @@ -0,0 +1 @@ +return {"result from dependency"} diff --git a/tests/require/with_config/src/directory_alias_requirer.luau b/tests/require/config_tests/with_config/src/directory_alias_requirer.luau similarity index 100% rename from tests/require/with_config/src/directory_alias_requirer.luau rename to tests/require/config_tests/with_config/src/directory_alias_requirer.luau diff --git a/tests/require/with_config/src/other_dependency.luau b/tests/require/config_tests/with_config/src/other_dependency.luau similarity index 100% rename from tests/require/with_config/src/other_dependency.luau rename to tests/require/config_tests/with_config/src/other_dependency.luau diff --git a/tests/require/with_config/src/parent_alias_requirer.luau b/tests/require/config_tests/with_config/src/parent_alias_requirer.luau similarity index 100% rename from tests/require/with_config/src/parent_alias_requirer.luau rename to tests/require/config_tests/with_config/src/parent_alias_requirer.luau diff --git a/tests/require/with_config/src/subdirectory/subdirectory_dependency.luau b/tests/require/config_tests/with_config/src/subdirectory/subdirectory_dependency.luau similarity index 100% rename from tests/require/with_config/src/subdirectory/subdirectory_dependency.luau rename to tests/require/config_tests/with_config/src/subdirectory/subdirectory_dependency.luau diff --git a/tests/require/with_config/src/submodule/.luaurc b/tests/require/config_tests/with_config/src/submodule/.luaurc similarity index 100% rename from tests/require/with_config/src/submodule/.luaurc rename to tests/require/config_tests/with_config/src/submodule/.luaurc diff --git a/tests/require/config_tests/with_config/src/submodule/init.luau b/tests/require/config_tests/with_config/src/submodule/init.luau new file mode 100644 index 00000000..4375a783 --- /dev/null +++ b/tests/require/config_tests/with_config/src/submodule/init.luau @@ -0,0 +1 @@ +return require("@dep") diff --git a/tests/require/config_tests/with_config_luau/.config.luau b/tests/require/config_tests/with_config_luau/.config.luau new file mode 100644 index 00000000..b64979de --- /dev/null +++ b/tests/require/config_tests/with_config_luau/.config.luau @@ -0,0 +1,8 @@ +return { + luau = { + aliases = { + dep = "./this_should_be_overwritten_by_child_luaurc", + otherdep = "./src/other_dependency" + } + } +} diff --git a/tests/require/config_tests/with_config_luau/parent_ambiguity/.config.luau b/tests/require/config_tests/with_config_luau/parent_ambiguity/.config.luau new file mode 100644 index 00000000..bc1ec7f0 --- /dev/null +++ b/tests/require/config_tests/with_config_luau/parent_ambiguity/.config.luau @@ -0,0 +1,7 @@ +return { + luau = { + aliases = { + foo = "./foo", + } + } +} diff --git a/tests/require/config_tests/with_config_luau/parent_ambiguity/folder.luau b/tests/require/config_tests/with_config_luau/parent_ambiguity/folder.luau new file mode 100644 index 00000000..e69de29b diff --git a/tests/require/config_tests/with_config_luau/parent_ambiguity/folder/requirer.luau b/tests/require/config_tests/with_config_luau/parent_ambiguity/folder/requirer.luau new file mode 100644 index 00000000..2f613057 --- /dev/null +++ b/tests/require/config_tests/with_config_luau/parent_ambiguity/folder/requirer.luau @@ -0,0 +1 @@ +return require("@foo") diff --git a/tests/require/config_tests/with_config_luau/parent_ambiguity/foo.luau b/tests/require/config_tests/with_config_luau/parent_ambiguity/foo.luau new file mode 100644 index 00000000..aa00aca1 --- /dev/null +++ b/tests/require/config_tests/with_config_luau/parent_ambiguity/foo.luau @@ -0,0 +1 @@ +return { "result from foo" } diff --git a/tests/require/config_tests/with_config_luau/src/.config.luau b/tests/require/config_tests/with_config_luau/src/.config.luau new file mode 100644 index 00000000..63d3bbc8 --- /dev/null +++ b/tests/require/config_tests/with_config_luau/src/.config.luau @@ -0,0 +1,8 @@ +return { + luau = { + aliases = { + dep = "./dependency", + subdir = "./subdirectory" + } + } +} diff --git a/tests/require/config_tests/with_config_luau/src/alias_requirer.luau b/tests/require/config_tests/with_config_luau/src/alias_requirer.luau new file mode 100644 index 00000000..4375a783 --- /dev/null +++ b/tests/require/config_tests/with_config_luau/src/alias_requirer.luau @@ -0,0 +1 @@ +return require("@dep") diff --git a/tests/require/config_tests/with_config_luau/src/dependency.luau b/tests/require/config_tests/with_config_luau/src/dependency.luau new file mode 100644 index 00000000..07466f42 --- /dev/null +++ b/tests/require/config_tests/with_config_luau/src/dependency.luau @@ -0,0 +1 @@ +return {"result from dependency"} diff --git a/tests/require/config_tests/with_config_luau/src/directory_alias_requirer.luau b/tests/require/config_tests/with_config_luau/src/directory_alias_requirer.luau new file mode 100644 index 00000000..3b19d4ff --- /dev/null +++ b/tests/require/config_tests/with_config_luau/src/directory_alias_requirer.luau @@ -0,0 +1 @@ +return(require("@subdir/subdirectory_dependency")) diff --git a/tests/require/config_tests/with_config_luau/src/other_dependency.luau b/tests/require/config_tests/with_config_luau/src/other_dependency.luau new file mode 100644 index 00000000..8c582dc2 --- /dev/null +++ b/tests/require/config_tests/with_config_luau/src/other_dependency.luau @@ -0,0 +1 @@ +return {"result from other_dependency"} diff --git a/tests/require/config_tests/with_config_luau/src/parent_alias_requirer.luau b/tests/require/config_tests/with_config_luau/src/parent_alias_requirer.luau new file mode 100644 index 00000000..a8e8de09 --- /dev/null +++ b/tests/require/config_tests/with_config_luau/src/parent_alias_requirer.luau @@ -0,0 +1 @@ +return require("@otherdep") diff --git a/tests/require/config_tests/with_config_luau/src/subdirectory/subdirectory_dependency.luau b/tests/require/config_tests/with_config_luau/src/subdirectory/subdirectory_dependency.luau new file mode 100644 index 00000000..8bbd0beb --- /dev/null +++ b/tests/require/config_tests/with_config_luau/src/subdirectory/subdirectory_dependency.luau @@ -0,0 +1 @@ +return {"result from subdirectory_dependency"} diff --git a/tests/require/config_tests/with_config_luau/src/submodule/.config.luau b/tests/require/config_tests/with_config_luau/src/submodule/.config.luau new file mode 100644 index 00000000..eb309fd1 --- /dev/null +++ b/tests/require/config_tests/with_config_luau/src/submodule/.config.luau @@ -0,0 +1,7 @@ +return { + luau = { + aliases = { + dep = "./this_should_not_be_read_by_init_luau", + } + } +} diff --git a/tests/require/config_tests/with_config_luau/src/submodule/init.luau b/tests/require/config_tests/with_config_luau/src/submodule/init.luau new file mode 100644 index 00000000..4375a783 --- /dev/null +++ b/tests/require/config_tests/with_config_luau/src/submodule/init.luau @@ -0,0 +1 @@ +return require("@dep")