From 5f42e63a735a2352862be88431f84d29c1849512 Mon Sep 17 00:00:00 2001 From: Varun Saini <61795485+vrn-sn@users.noreply.github.com> Date: Fri, 21 Mar 2025 14:43:00 -0700 Subject: [PATCH] Sync to upstream/release/666 (#1747) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Another week, another release. Happy spring! 🌷 ## New Type Solver - Add typechecking and autocomplete support for user-defined type functions! - Improve the display of type paths, making type mismatch errors far more human-readable. - Enhance various aspects of the `index` type function: support function type metamethods, fix crashes involving cyclic metatables, and forward `any` types through the type function. - Fix incorrect subtyping results involving the `buffer` type. - Fix crashes related to typechecking anonymous functions in nonstrict mode. ## AST - Retain source information for type packs, functions, and type functions. - Introduce `AstTypeOptional` to differentiate `T?` from `T | nil` in the AST. - Prevent the transpiler from advancing before tokens when the AST has errors. ## Autocomplete - Introduce demand-based cloning and better module isolation for fragment autocomplete, leading to a substantial speedup in performance. - Guard against recursive unions in `autocompleteProps`. ## Miscellaneous - #1720 (thank you!) ## Internal Contributors Co-authored-by: Andy Friesen Co-authored-by: Ariel Weiss Co-authored-by: Hunter Goldstein Co-authored-by: Talha Pathan Co-authored-by: Vighnesh Vijay Co-authored-by: Vyacheslav Egorov --- Analysis/include/Luau/BuiltinDefinitions.h | 1 + Analysis/include/Luau/ConstraintGenerator.h | 4 + Analysis/include/Luau/Frontend.h | 2 + Analysis/include/Luau/Generalization.h | 3 +- Analysis/include/Luau/GlobalTypes.h | 2 + Analysis/include/Luau/Scope.h | 5 +- Analysis/include/Luau/Type.h | 1 - Analysis/include/Luau/TypeChecker2.h | 21 +- Analysis/include/Luau/TypeFunction.h | 3 + Analysis/include/Luau/TypeFunctionRuntime.h | 2 - .../include/Luau/TypeFunctionRuntimeBuilder.h | 8 +- Analysis/include/Luau/TypePath.h | 13 + Analysis/src/AstJsonEncoder.cpp | 5 + Analysis/src/AutocompleteCore.cpp | 16 + Analysis/src/BuiltinDefinitions.cpp | 94 +++- Analysis/src/Clone.cpp | 35 +- Analysis/src/ConstraintGenerator.cpp | 180 +++++-- Analysis/src/DataFlowGraph.cpp | 2 + Analysis/src/EmbeddedBuiltinDefinitions.cpp | 79 +++ Analysis/src/Error.cpp | 18 +- Analysis/src/FragmentAutocomplete.cpp | 300 ++++++++++- Analysis/src/Frontend.cpp | 5 + Analysis/src/Generalization.cpp | 14 +- Analysis/src/GlobalTypes.cpp | 1 + Analysis/src/NonStrictTypeChecker.cpp | 13 +- Analysis/src/Normalize.cpp | 8 +- Analysis/src/RequireTracer.cpp | 119 ++--- Analysis/src/Scope.cpp | 34 ++ Analysis/src/Subtyping.cpp | 16 +- Analysis/src/TableLiteralInference.cpp | 36 +- Analysis/src/Transpiler.cpp | 197 ++++++- Analysis/src/TypeAttach.cpp | 5 +- Analysis/src/TypeChecker2.cpp | 69 ++- Analysis/src/TypeFunction.cpp | 411 ++++++--------- Analysis/src/TypeFunctionRuntime.cpp | 19 +- Analysis/src/TypeFunctionRuntimeBuilder.cpp | 31 +- Analysis/src/TypeInfer.cpp | 4 + Analysis/src/TypePath.cpp | 244 ++++++++- Analysis/src/Unifier2.cpp | 12 +- Ast/include/Luau/Ast.h | 14 + Ast/include/Luau/Cst.h | 73 +++ Ast/include/Luau/Parser.h | 17 +- Ast/src/Ast.cpp | 10 + Ast/src/Cst.cpp | 47 ++ Ast/src/Lexer.cpp | 4 +- Ast/src/Parser.cpp | 256 +++++++-- Compiler/src/Types.cpp | 4 + tests/AnyTypeSummary.test.cpp | 192 +++---- tests/Autocomplete.test.cpp | 80 +++ tests/ClassFixture.cpp | 3 +- tests/ClassFixture.h | 2 +- tests/ConstraintGeneratorFixture.cpp | 1 + tests/Fixture.cpp | 1 + tests/FragmentAutocomplete.test.cpp | 41 ++ tests/Frontend.test.cpp | 13 +- tests/Lexer.test.cpp | 8 +- tests/NonStrictTypeChecker.test.cpp | 32 ++ tests/Normalize.test.cpp | 2 - tests/Parser.test.cpp | 6 +- tests/RequireTracer.test.cpp | 8 - tests/Subtyping.test.cpp | 17 +- tests/ToString.test.cpp | 29 +- tests/Transpiler.test.cpp | 283 +++++++++- tests/TypeFunction.test.cpp | 98 ++++ tests/TypeFunction.user.test.cpp | 179 ++++++- tests/TypeInfer.aliases.test.cpp | 13 +- tests/TypeInfer.builtins.test.cpp | 30 +- tests/TypeInfer.classes.test.cpp | 11 +- tests/TypeInfer.definitions.test.cpp | 5 - tests/TypeInfer.functions.test.cpp | 135 ++++- tests/TypeInfer.generics.test.cpp | 13 +- tests/TypeInfer.intersectionTypes.test.cpp | 496 +++++++++++++++++- tests/TypeInfer.modules.test.cpp | 19 +- tests/TypeInfer.negations.test.cpp | 1 + tests/TypeInfer.operators.test.cpp | 3 - tests/TypeInfer.provisional.test.cpp | 27 +- tests/TypeInfer.singletons.test.cpp | 29 +- tests/TypeInfer.tables.test.cpp | 279 ++++++++-- tests/TypeInfer.test.cpp | 38 +- tests/TypeInfer.typePacks.test.cpp | 37 +- tests/TypeInfer.unionTypes.test.cpp | 143 ++++- tests/TypePath.test.cpp | 8 + 82 files changed, 3871 insertions(+), 868 deletions(-) diff --git a/Analysis/include/Luau/BuiltinDefinitions.h b/Analysis/include/Luau/BuiltinDefinitions.h index 7dc38835..89a3a929 100644 --- a/Analysis/include/Luau/BuiltinDefinitions.h +++ b/Analysis/include/Luau/BuiltinDefinitions.h @@ -70,6 +70,7 @@ Property makeProperty(TypeId ty, std::optional documentationSymbol void assignPropDocumentationSymbols(TableType::Props& props, const std::string& baseName); std::string getBuiltinDefinitionSource(); +std::string getTypeFunctionDefinitionSource(); void addGlobalBinding(GlobalTypes& globals, const std::string& name, TypeId ty, const std::string& packageName); void addGlobalBinding(GlobalTypes& globals, const std::string& name, Binding binding); diff --git a/Analysis/include/Luau/ConstraintGenerator.h b/Analysis/include/Luau/ConstraintGenerator.h index 8a072e82..40c1263e 100644 --- a/Analysis/include/Luau/ConstraintGenerator.h +++ b/Analysis/include/Luau/ConstraintGenerator.h @@ -117,12 +117,15 @@ struct ConstraintGenerator // Needed to register all available type functions for execution at later stages. NotNull typeFunctionRuntime; + DenseHashMap astTypeFunctionEnvironmentScopes{nullptr}; + // Needed to resolve modules to make 'require' import types properly. NotNull moduleResolver; // Occasionally constraint generation needs to produce an ICE. const NotNull ice; ScopePtr globalScope; + ScopePtr typeFunctionScope; std::function prepareModuleScope; std::vector requireCycles; @@ -140,6 +143,7 @@ struct ConstraintGenerator NotNull builtinTypes, NotNull ice, const ScopePtr& globalScope, + const ScopePtr& typeFunctionScope, std::function prepareModuleScope, DcrLogger* logger, NotNull dfg, diff --git a/Analysis/include/Luau/Frontend.h b/Analysis/include/Luau/Frontend.h index 91e94e4a..918019e2 100644 --- a/Analysis/include/Luau/Frontend.h +++ b/Analysis/include/Luau/Frontend.h @@ -305,6 +305,7 @@ ModulePtr check( NotNull moduleResolver, NotNull fileResolver, const ScopePtr& globalScope, + const ScopePtr& typeFunctionScope, std::function prepareModuleScope, FrontendOptions options, TypeCheckLimits limits @@ -319,6 +320,7 @@ ModulePtr check( NotNull moduleResolver, NotNull fileResolver, const ScopePtr& globalScope, + const ScopePtr& typeFunctionScope, std::function prepareModuleScope, FrontendOptions options, TypeCheckLimits limits, diff --git a/Analysis/include/Luau/Generalization.h b/Analysis/include/Luau/Generalization.h index 18d5b678..22ebf171 100644 --- a/Analysis/include/Luau/Generalization.h +++ b/Analysis/include/Luau/Generalization.h @@ -12,8 +12,9 @@ std::optional generalize( NotNull arena, NotNull builtinTypes, NotNull scope, - NotNull> bakedTypes, + NotNull> cachedTypes, TypeId ty, /* avoid sealing tables*/ bool avoidSealingTables = false ); + } diff --git a/Analysis/include/Luau/GlobalTypes.h b/Analysis/include/Luau/GlobalTypes.h index 55a6d6c7..c9079edd 100644 --- a/Analysis/include/Luau/GlobalTypes.h +++ b/Analysis/include/Luau/GlobalTypes.h @@ -19,7 +19,9 @@ struct GlobalTypes TypeArena globalTypes; SourceModule globalNames; // names for symbols entered into globalScope + ScopePtr globalScope; // shared by all modules + ScopePtr globalTypeFunctionScope; // shared by all modules }; } // namespace Luau diff --git a/Analysis/include/Luau/Scope.h b/Analysis/include/Luau/Scope.h index 4604a2e1..6c3e15df 100644 --- a/Analysis/include/Luau/Scope.h +++ b/Analysis/include/Luau/Scope.h @@ -35,7 +35,7 @@ struct Scope explicit Scope(TypePackId returnType); // root scope explicit Scope(const ScopePtr& parent, int subLevel = 0); // child scope. Parent must not be nullptr. - const ScopePtr parent; // null for the root + ScopePtr parent; // null for the root // All the children of this scope. std::vector> children; @@ -59,6 +59,8 @@ struct Scope std::optional lookup(Symbol sym) const; std::optional lookupUnrefinedType(DefId def) const; + + std::optional lookupRValueRefinementType(DefId def) const; std::optional lookup(DefId def) const; std::optional> lookupEx(DefId def); std::optional> lookupEx(Symbol sym); @@ -71,6 +73,7 @@ struct Scope // WARNING: This function linearly scans for a string key of equal value! It is thus O(n**2) std::optional linearSearchForBinding(const std::string& name, bool traverseScopeChain = true) const; + std::optional> linearSearchForBindingPair(const std::string& name, bool traverseScopeChain) const; RefinementMap refinements; diff --git a/Analysis/include/Luau/Type.h b/Analysis/include/Luau/Type.h index 890c7078..ced5ed83 100644 --- a/Analysis/include/Luau/Type.h +++ b/Analysis/include/Luau/Type.h @@ -622,7 +622,6 @@ struct UserDefinedFunctionData AstStatTypeFunction* definition = nullptr; DenseHashMap> environment{""}; - DenseHashMap environment_DEPRECATED{""}; }; /** diff --git a/Analysis/include/Luau/TypeChecker2.h b/Analysis/include/Luau/TypeChecker2.h index 0c52b1f1..2f88f345 100644 --- a/Analysis/include/Luau/TypeChecker2.h +++ b/Analysis/include/Luau/TypeChecker2.h @@ -13,6 +13,8 @@ #include "Luau/TypeOrPack.h" #include "Luau/TypeUtils.h" +LUAU_FASTFLAG(LuauImproveTypePathsInErrors) + namespace Luau { @@ -38,18 +40,29 @@ struct Reasonings std::string toString() { + if (FFlag::LuauImproveTypePathsInErrors && reasons.empty()) + return ""; + // DenseHashSet ordering is entirely undefined, so we want to // sort the reasons here to achieve a stable error // stringification. std::sort(reasons.begin(), reasons.end()); - std::string allReasons; + std::string allReasons = FFlag::LuauImproveTypePathsInErrors ? "\nthis is because " : ""; bool first = true; for (const std::string& reason : reasons) { - if (first) - first = false; + if (FFlag::LuauImproveTypePathsInErrors) + { + if (reasons.size() > 1) + allReasons += "\n\t * "; + } else - allReasons += "\n\t"; + { + if (first) + first = false; + else + allReasons += "\n\t"; + } allReasons += reason; } diff --git a/Analysis/include/Luau/TypeFunction.h b/Analysis/include/Luau/TypeFunction.h index 1c97550f..bde00461 100644 --- a/Analysis/include/Luau/TypeFunction.h +++ b/Analysis/include/Luau/TypeFunction.h @@ -48,6 +48,9 @@ struct TypeFunctionRuntime // Evaluation of type functions should only be performed in the absence of parse errors in the source module bool allowEvaluation = true; + // Root scope in which the type function operates in, set up by ConstraintGenerator + ScopePtr rootScope; + // Output created by 'print' function std::vector messages; diff --git a/Analysis/include/Luau/TypeFunctionRuntime.h b/Analysis/include/Luau/TypeFunctionRuntime.h index e6cc4d26..a7a26958 100644 --- a/Analysis/include/Luau/TypeFunctionRuntime.h +++ b/Analysis/include/Luau/TypeFunctionRuntime.h @@ -223,8 +223,6 @@ struct TypeFunctionClassType std::optional writeParent; TypeId classTy; - - std::string name_DEPRECATED; }; struct TypeFunctionGenericType diff --git a/Analysis/include/Luau/TypeFunctionRuntimeBuilder.h b/Analysis/include/Luau/TypeFunctionRuntimeBuilder.h index 040a3092..191bcf18 100644 --- a/Analysis/include/Luau/TypeFunctionRuntimeBuilder.h +++ b/Analysis/include/Luau/TypeFunctionRuntimeBuilder.h @@ -28,14 +28,8 @@ struct TypeFunctionRuntimeBuilderState { NotNull ctx; - // Mapping of class name to ClassType - // Invariant: users can not create a new class types -> any class types that get deserialized must have been an argument to the type function - // Using this invariant, whenever a ClassType is serialized, we can put it into this map - // whenever a ClassType is deserialized, we can use this map to return the corresponding value - DenseHashMap classesSerialized_DEPRECATED{{}}; - // List of errors that occur during serialization/deserialization - // At every iteration of serialization/deserialzation, if this list.size() != 0, we halt the process + // At every iteration of serialization/deserialization, if this list.size() != 0, we halt the process std::vector errors{}; TypeFunctionRuntimeBuilderState(NotNull ctx) diff --git a/Analysis/include/Luau/TypePath.h b/Analysis/include/Luau/TypePath.h index 2af5185d..d783c662 100644 --- a/Analysis/include/Luau/TypePath.h +++ b/Analysis/include/Luau/TypePath.h @@ -42,9 +42,19 @@ struct Property /// element. struct Index { + enum class Variant + { + Pack, + Union, + Intersection + }; + /// The 0-based index to use for the lookup. size_t index; + /// The sort of thing we're indexing from, this is used in stringifying the type path for errors. + Variant variant; + bool operator==(const Index& other) const; }; @@ -205,6 +215,9 @@ using Path = TypePath::Path; /// terribly clear to end users of the Luau type system. 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); + std::optional traverse(TypeId root, const Path& path, NotNull builtinTypes); std::optional traverse(TypePackId root, const Path& path, NotNull builtinTypes); diff --git a/Analysis/src/AstJsonEncoder.cpp b/Analysis/src/AstJsonEncoder.cpp index c0a6c254..881bc85b 100644 --- a/Analysis/src/AstJsonEncoder.cpp +++ b/Analysis/src/AstJsonEncoder.cpp @@ -1065,6 +1065,11 @@ struct AstJsonEncoder : public AstVisitor ); } + void write(class AstTypeOptional* node) + { + writeNode(node, "AstTypeOptional", [&]() {}); + } + void write(class AstTypeUnion* node) { writeNode( diff --git a/Analysis/src/AutocompleteCore.cpp b/Analysis/src/AutocompleteCore.cpp index 46369612..03e5c31e 100644 --- a/Analysis/src/AutocompleteCore.cpp +++ b/Analysis/src/AutocompleteCore.cpp @@ -30,6 +30,7 @@ LUAU_FASTFLAG(LuauExposeRequireByStringAutocomplete) LUAU_FASTFLAGVARIABLE(LuauAutocompleteRefactorsForIncrementalAutocomplete) LUAU_FASTFLAGVARIABLE(LuauAutocompleteUsesModuleForTypeCompatibility) +LUAU_FASTFLAGVARIABLE(LuauAutocompleteUnionCopyPreviousSeen) static const std::unordered_set kStatementStartingKeywords = {"while", "if", "local", "repeat", "function", "do", "for", "return", "break", "continue", "type", "export"}; @@ -484,6 +485,21 @@ static void autocompleteProps( AutocompleteEntryMap inner; std::unordered_set innerSeen; + // If we don't do this, and we have the misfortune of receiving a + // recursive union like: + // + // t1 where t1 = t1 | Class + // + // Then we are on a one way journey to a stack overflow. + if (FFlag::LuauAutocompleteUnionCopyPreviousSeen) + { + for (auto ty: seen) + { + if (is(ty)) + innerSeen.insert(ty); + } + } + if (isNil(*iter)) { ++iter; diff --git a/Analysis/src/BuiltinDefinitions.cpp b/Analysis/src/BuiltinDefinitions.cpp index 2a93195f..1f5e44a1 100644 --- a/Analysis/src/BuiltinDefinitions.cpp +++ b/Analysis/src/BuiltinDefinitions.cpp @@ -32,8 +32,8 @@ LUAU_FASTFLAG(LuauSolverV2) LUAU_FASTFLAGVARIABLE(LuauStringFormatErrorSuppression) LUAU_FASTFLAGVARIABLE(LuauTableCloneClonesType3) LUAU_FASTFLAG(LuauTrackInteriorFreeTypesOnScope) -LUAU_FASTFLAGVARIABLE(LuauFreezeIgnorePersistent) LUAU_FASTFLAGVARIABLE(LuauFollowTableFreeze) +LUAU_FASTFLAGVARIABLE(LuauUserTypeFunTypecheck) namespace Luau { @@ -288,6 +288,22 @@ void assignPropDocumentationSymbols(TableType::Props& props, const std::string& } } +static void finalizeGlobalBindings(ScopePtr scope) +{ + LUAU_ASSERT(FFlag::LuauUserTypeFunTypecheck); + + for (const auto& pair : scope->bindings) + { + persist(pair.second.typeId); + + if (TableType* ttv = getMutable(pair.second.typeId)) + { + if (!ttv->name) + ttv->name = "typeof(" + toString(pair.first) + ")"; + } + } +} + void registerBuiltinGlobals(Frontend& frontend, GlobalTypes& globals, bool typeCheckForAutocomplete) { LUAU_ASSERT(!globals.globalTypes.types.isFrozen()); @@ -399,14 +415,21 @@ void registerBuiltinGlobals(Frontend& frontend, GlobalTypes& globals, bool typeC // clang-format on } - for (const auto& pair : globals.globalScope->bindings) + if (FFlag::LuauUserTypeFunTypecheck) { - persist(pair.second.typeId); - - if (TableType* ttv = getMutable(pair.second.typeId)) + finalizeGlobalBindings(globals.globalScope); + } + else + { + for (const auto& pair : globals.globalScope->bindings) { - if (!ttv->name) - ttv->name = "typeof(" + toString(pair.first) + ")"; + persist(pair.second.typeId); + + if (TableType* ttv = getMutable(pair.second.typeId)) + { + if (!ttv->name) + ttv->name = "typeof(" + toString(pair.first) + ")"; + } } } @@ -467,6 +490,59 @@ void registerBuiltinGlobals(Frontend& frontend, GlobalTypes& globals, bool typeC TypeId requireTy = getGlobalBinding(globals, "require"); attachTag(requireTy, kRequireTagName); attachMagicFunction(requireTy, std::make_shared()); + + if (FFlag::LuauUserTypeFunTypecheck) + { + // Global scope cannot be the parent of the type checking environment because it can be changed by the embedder + globals.globalTypeFunctionScope->exportedTypeBindings = globals.globalScope->exportedTypeBindings; + globals.globalTypeFunctionScope->builtinTypeNames = globals.globalScope->builtinTypeNames; + + // Type function runtime also removes a few standard libraries and globals, so we will take only the ones that are defined + static const char* typeFunctionRuntimeBindings[] = { + // Libraries + "math", + "table", + "string", + "bit32", + "utf8", + "buffer", + + // Globals + "assert", + "error", + "print", + "next", + "ipairs", + "pairs", + "select", + "unpack", + "getmetatable", + "setmetatable", + "rawget", + "rawset", + "rawlen", + "rawequal", + "tonumber", + "tostring", + "type", + "typeof", + }; + + for (auto& name : typeFunctionRuntimeBindings) + { + AstName astName = globals.globalNames.names->get(name); + LUAU_ASSERT(astName.value); + + globals.globalTypeFunctionScope->bindings[astName] = globals.globalScope->bindings[astName]; + } + + LoadDefinitionFileResult typeFunctionLoadResult = frontend.loadDefinitionFile( + globals, globals.globalTypeFunctionScope, getTypeFunctionDefinitionSource(), "@luau", /* captureComments */ false, false + ); + LUAU_ASSERT(typeFunctionLoadResult.success); + + finalizeGlobalBindings(globals.globalTypeFunctionScope); + } } static std::vector parseFormatString(NotNull builtinTypes, const char* data, size_t size) @@ -1444,7 +1520,7 @@ bool MagicClone::infer(const MagicFunctionCallContext& context) return false; CloneState cloneState{context.solver->builtinTypes}; - TypeId resultType = shallowClone(inputType, *arena, cloneState, /* ignorePersistent */ FFlag::LuauFreezeIgnorePersistent); + TypeId resultType = shallowClone(inputType, *arena, cloneState, /* ignorePersistent */ true); if (auto tableType = getMutable(resultType)) { @@ -1481,7 +1557,7 @@ static std::optional freezeTable(TypeId inputType, const MagicFunctionCa { // Clone the input type, this will become our final result type after we mutate it. CloneState cloneState{context.solver->builtinTypes}; - TypeId resultType = shallowClone(inputType, *arena, cloneState, /* ignorePersistent */ FFlag::LuauFreezeIgnorePersistent); + TypeId resultType = shallowClone(inputType, *arena, cloneState, /* ignorePersistent */ true); auto tableTy = getMutable(resultType); // `clone` should not break this. LUAU_ASSERT(tableTy); diff --git a/Analysis/src/Clone.cpp b/Analysis/src/Clone.cpp index 5ad56e16..058564ef 100644 --- a/Analysis/src/Clone.cpp +++ b/Analysis/src/Clone.cpp @@ -9,13 +9,12 @@ #include "Luau/VisitType.h" LUAU_FASTFLAG(LuauSolverV2) -LUAU_FASTFLAG(LuauFreezeIgnorePersistent) // For each `Luau::clone` call, we will clone only up to N amount of types _and_ packs, as controlled by this limit. LUAU_FASTINTVARIABLE(LuauTypeCloneIterationLimit, 100'000) LUAU_FASTFLAGVARIABLE(LuauClonedTableAndFunctionTypesMustHaveScopes) LUAU_FASTFLAGVARIABLE(LuauDoNotClonePersistentBindings) - +LUAU_FASTFLAG(LuauIncrementalAutocompleteDemandBasedCloning) namespace Luau { @@ -134,7 +133,7 @@ protected: ty = follow(ty, FollowOption::DisableLazyTypeThunks); if (auto it = types->find(ty); it != types->end()) return it->second; - else if (ty->persistent && (!FFlag::LuauFreezeIgnorePersistent || ty != forceTy)) + else if (ty->persistent && ty != forceTy) return ty; return std::nullopt; } @@ -144,7 +143,7 @@ protected: tp = follow(tp); if (auto it = packs->find(tp); it != packs->end()) return it->second; - else if (tp->persistent && (!FFlag::LuauFreezeIgnorePersistent || tp != forceTp)) + else if (tp->persistent && tp != forceTp) return tp; return std::nullopt; } @@ -170,7 +169,7 @@ public: if (auto clone = find(ty)) return *clone; - else if (ty->persistent && (!FFlag::LuauFreezeIgnorePersistent || ty != forceTy)) + else if (ty->persistent && ty != forceTy) return ty; TypeId target = arena->addType(ty->ty); @@ -196,7 +195,7 @@ public: if (auto clone = find(tp)) return *clone; - else if (tp->persistent && (!FFlag::LuauFreezeIgnorePersistent || tp != forceTp)) + else if (tp->persistent && tp != forceTp) return tp; TypePackId target = arena->addTypePack(tp->ty); @@ -398,7 +397,7 @@ private: ty = shallowClone(ty); } - void cloneChildren(LazyType* t) + virtual void cloneChildren(LazyType* t) { if (auto unwrapped = t->unwrapped.load()) t->unwrapped.store(shallowClone(unwrapped)); @@ -505,7 +504,7 @@ public: if (auto clone = find(ty)) return *clone; - else if (ty->persistent && (!FFlag::LuauFreezeIgnorePersistent || ty != forceTy)) + else if (ty->persistent && ty != forceTy) return ty; TypeId target = arena->addType(ty->ty); @@ -539,7 +538,7 @@ public: if (auto clone = find(tp)) return *clone; - else if (tp->persistent && (!FFlag::LuauFreezeIgnorePersistent || tp != forceTp)) + else if (tp->persistent && tp != forceTp) return tp; TypePackId target = arena->addTypePack(tp->ty); @@ -553,6 +552,16 @@ public: queue.emplace_back(target); return target; } + + void cloneChildren(LazyType* t) override + { + // Do not clone lazy types + if (!FFlag::LuauIncrementalAutocompleteDemandBasedCloning) + { + if (auto unwrapped = t->unwrapped.load()) + t->unwrapped.store(shallowClone(unwrapped)); + } + } }; @@ -560,7 +569,7 @@ public: TypePackId shallowClone(TypePackId tp, TypeArena& dest, CloneState& cloneState, bool ignorePersistent) { - if (tp->persistent && (!FFlag::LuauFreezeIgnorePersistent || !ignorePersistent)) + if (tp->persistent && !ignorePersistent) return tp; TypeCloner cloner{ @@ -569,7 +578,7 @@ TypePackId shallowClone(TypePackId tp, TypeArena& dest, CloneState& cloneState, NotNull{&cloneState.seenTypes}, NotNull{&cloneState.seenTypePacks}, nullptr, - FFlag::LuauFreezeIgnorePersistent && ignorePersistent ? tp : nullptr + ignorePersistent ? tp : nullptr }; return cloner.shallowClone(tp); @@ -577,7 +586,7 @@ TypePackId shallowClone(TypePackId tp, TypeArena& dest, CloneState& cloneState, TypeId shallowClone(TypeId typeId, TypeArena& dest, CloneState& cloneState, bool ignorePersistent) { - if (typeId->persistent && (!FFlag::LuauFreezeIgnorePersistent || !ignorePersistent)) + if (typeId->persistent && !ignorePersistent) return typeId; TypeCloner cloner{ @@ -585,7 +594,7 @@ TypeId shallowClone(TypeId typeId, TypeArena& dest, CloneState& cloneState, bool cloneState.builtinTypes, NotNull{&cloneState.seenTypes}, NotNull{&cloneState.seenTypePacks}, - FFlag::LuauFreezeIgnorePersistent && ignorePersistent ? typeId : nullptr, + ignorePersistent ? typeId : nullptr, nullptr }; diff --git a/Analysis/src/ConstraintGenerator.cpp b/Analysis/src/ConstraintGenerator.cpp index e775c59b..1972d559 100644 --- a/Analysis/src/ConstraintGenerator.cpp +++ b/Analysis/src/ConstraintGenerator.cpp @@ -44,6 +44,7 @@ LUAU_FASTFLAG(LuauFreeTypesMustHaveBounds) LUAU_FASTFLAGVARIABLE(LuauInferLocalTypesInMultipleAssignments) LUAU_FASTFLAGVARIABLE(LuauDoNotLeakNilInRefinement) LUAU_FASTFLAGVARIABLE(LuauExtraFollows) +LUAU_FASTFLAG(LuauUserTypeFunTypecheck) namespace Luau { @@ -184,6 +185,7 @@ ConstraintGenerator::ConstraintGenerator( NotNull builtinTypes, NotNull ice, const ScopePtr& globalScope, + const ScopePtr& typeFunctionScope, std::function prepareModuleScope, DcrLogger* logger, NotNull dfg, @@ -200,6 +202,7 @@ ConstraintGenerator::ConstraintGenerator( , moduleResolver(moduleResolver) , ice(ice) , globalScope(globalScope) + , typeFunctionScope(typeFunctionScope) , prepareModuleScope(std::move(prepareModuleScope)) , requireCycles(std::move(requireCycles)) , logger(logger) @@ -221,6 +224,14 @@ void ConstraintGenerator::visitModuleRoot(AstStatBlock* block) rootScope->returnType = freshTypePack(scope); + if (FFlag::LuauUserTypeFunTypecheck) + { + // Create module-local scope for the type function environment + ScopePtr localTypeFunctionScope = std::make_shared(typeFunctionScope); + localTypeFunctionScope->location = block->location; + typeFunctionRuntime->rootScope = localTypeFunctionScope; + } + TypeId moduleFnTy = arena->addType(FunctionType{TypeLevel{}, rootScope, builtinTypes->anyTypePack, rootScope->returnType}); interiorTypes.emplace_back(); @@ -699,6 +710,9 @@ void ConstraintGenerator::checkAliases(const ScopePtr& scope, AstStatBlock* bloc std::unordered_map aliasDefinitionLocations; std::unordered_map classDefinitionLocations; + bool hasTypeFunction = false; + ScopePtr typeFunctionEnvScope; + // In order to enable mutually-recursive type aliases, we need to // populate the type bindings before we actually check any of the // alias statements. @@ -744,6 +758,9 @@ void ConstraintGenerator::checkAliases(const ScopePtr& scope, AstStatBlock* bloc } else if (auto function = stat->as()) { + if (FFlag::LuauUserTypeFunTypecheck) + hasTypeFunction = true; + // If a type function w/ same name has already been defined, error for having duplicates if (scope->exportedTypeBindings.count(function->name.value) || scope->privateTypeBindings.count(function->name.value)) { @@ -753,7 +770,8 @@ void ConstraintGenerator::checkAliases(const ScopePtr& scope, AstStatBlock* bloc continue; } - ScopePtr defnScope = childScope(function, scope); + // Variable becomes unused with the removal of FFlag::LuauUserTypeFunTypecheck + ScopePtr defnScope = FFlag::LuauUserTypeFunTypecheck ? nullptr : childScope(function, scope); // Create TypeFunctionInstanceType @@ -820,11 +838,22 @@ void ConstraintGenerator::checkAliases(const ScopePtr& scope, AstStatBlock* bloc } } + if (FFlag::LuauUserTypeFunTypecheck && hasTypeFunction) + typeFunctionEnvScope = std::make_shared(typeFunctionRuntime->rootScope); + // Additional pass for user-defined type functions to fill in their environments completely for (AstStat* stat : block->body) { if (auto function = stat->as()) { + if (FFlag::LuauUserTypeFunTypecheck) + { + // Similar to global pre-population, create a binding for each type function in the scope upfront + TypeId bt = arena->addType(BlockedType{}); + typeFunctionEnvScope->bindings[function->name] = Binding{bt, function->location}; + astTypeFunctionEnvironmentScopes[function] = typeFunctionEnvScope; + } + // Find the type function we have already created TypeFunctionInstanceType* mainTypeFun = nullptr; @@ -843,51 +872,60 @@ void ConstraintGenerator::checkAliases(const ScopePtr& scope, AstStatBlock* bloc UserDefinedFunctionData& userFuncData = mainTypeFun->userFuncData; size_t level = 0; - for (Scope* curr = scope.get(); curr; curr = curr->parent.get()) + if (FFlag::LuauUserTypeFunTypecheck) { - for (auto& [name, tf] : curr->privateTypeBindings) + auto addToEnvironment = [this](UserDefinedFunctionData& userFuncData, ScopePtr scope, const Name& name, TypeId type, size_t level) { if (userFuncData.environment.find(name)) - continue; + return; - if (auto ty = get(tf.type); ty && ty->userFuncData.definition) + if (auto ty = get(type); ty && ty->userFuncData.definition) + { userFuncData.environment[name] = std::make_pair(ty->userFuncData.definition, level); - } - for (auto& [name, tf] : curr->exportedTypeBindings) + if (auto it = astTypeFunctionEnvironmentScopes.find(ty->userFuncData.definition)) + { + if (auto existing = (*it)->linearSearchForBinding(name, /* traverseScopeChain */ false)) + scope->bindings[ty->userFuncData.definition->name] = + Binding{existing->typeId, ty->userFuncData.definition->location}; + } + } + }; + + for (Scope* curr = scope.get(); curr; curr = curr->parent.get()) { - if (userFuncData.environment.find(name)) - continue; + for (auto& [name, tf] : curr->privateTypeBindings) + addToEnvironment(userFuncData, typeFunctionEnvScope, name, tf.type, level); - if (auto ty = get(tf.type); ty && ty->userFuncData.definition) - userFuncData.environment[name] = std::make_pair(ty->userFuncData.definition, level); + for (auto& [name, tf] : curr->exportedTypeBindings) + addToEnvironment(userFuncData, typeFunctionEnvScope, name, tf.type, level); + + level++; } - - level++; } - } - else if (mainTypeFun) - { - UserDefinedFunctionData& userFuncData = mainTypeFun->userFuncData; - - for (Scope* curr = scope.get(); curr; curr = curr->parent.get()) + else { - for (auto& [name, tf] : curr->privateTypeBindings) + for (Scope* curr = scope.get(); curr; curr = curr->parent.get()) { - if (userFuncData.environment_DEPRECATED.find(name)) - continue; + for (auto& [name, tf] : curr->privateTypeBindings) + { + if (userFuncData.environment.find(name)) + continue; - if (auto ty = get(tf.type); ty && ty->userFuncData.definition) - userFuncData.environment_DEPRECATED[name] = ty->userFuncData.definition; - } + if (auto ty = get(tf.type); ty && ty->userFuncData.definition) + userFuncData.environment[name] = std::make_pair(ty->userFuncData.definition, level); + } - for (auto& [name, tf] : curr->exportedTypeBindings) - { - if (userFuncData.environment_DEPRECATED.find(name)) - continue; + for (auto& [name, tf] : curr->exportedTypeBindings) + { + if (userFuncData.environment.find(name)) + continue; - if (auto ty = get(tf.type); ty && ty->userFuncData.definition) - userFuncData.environment_DEPRECATED[name] = ty->userFuncData.definition; + if (auto ty = get(tf.type); ty && ty->userFuncData.definition) + userFuncData.environment[name] = std::make_pair(ty->userFuncData.definition, level); + } + + level++; } } } @@ -1689,6 +1727,64 @@ ControlFlow ConstraintGenerator::visit(const ScopePtr& scope, AstStatTypeAlias* ControlFlow ConstraintGenerator::visit(const ScopePtr& scope, AstStatTypeFunction* function) { + if (!FFlag::LuauUserTypeFunTypecheck) + return ControlFlow::None; + + auto scopePtr = astTypeFunctionEnvironmentScopes.find(function); + LUAU_ASSERT(scopePtr); + + 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 + scope->children.push_back(NotNull{sig.signatureScope.get()}); + + interiorTypes.push_back(std::vector{}); + 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, FFlag::LuauTrackInteriorFreeTypesOnScope ? std::vector{} : std::move(interiorTypes.back()) + } + ); + + if (FFlag::LuauTrackInteriorFreeTypesOnScope) + sig.signatureScope->interiorFreeTypes = std::move(interiorTypes.back()); + + getMutable(generalizedTy)->setOwner(gc); + interiorTypes.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) + constraint->dependencies.push_back(NotNull{previous}); + + previous = constraint.get(); + } + } + ); + + std::optional existingFunctionTy = (*scopePtr)->lookup(function->name); + + if (!existingFunctionTy) + ice->ice("checkAliases did not populate type function name", function->nameLocation); + + if (auto bt = get(*existingFunctionTy); bt && nullptr == bt->getOwner()) + emplaceType(asMutable(*existingFunctionTy), generalizedTy); + return ControlFlow::None; } @@ -3094,7 +3190,7 @@ ConstraintGenerator::FunctionSignature ConstraintGenerator::checkFunctionSignatu signatureScope = childScope(fn, parent); // We need to assign returnType before creating bodyScope so that the - // return type gets propogated to bodyScope. + // return type gets propagated to bodyScope. returnType = freshTypePack(signatureScope); signatureScope->returnType = returnType; @@ -3520,6 +3616,10 @@ TypeId ConstraintGenerator::resolveType(const ScopePtr& scope, AstType* ty, bool TypeId exprType = check(scope, tof->expr).ty; result = exprType; } + else if (ty->is()) + { + return builtinTypes->nilType; + } else if (auto unionAnnotation = ty->as()) { if (FFlag::LuauPreserveUnionIntersectionNodeForLeadingTokenSingleType) @@ -3882,9 +3982,18 @@ struct GlobalPrepopulator : AstVisitor void ConstraintGenerator::prepopulateGlobalScopeForFragmentTypecheck(const ScopePtr& globalScope, const ScopePtr& resumeScope, AstStatBlock* program) { FragmentTypeCheckGlobalPrepopulator gp{NotNull{globalScope.get()}, NotNull{resumeScope.get()}, dfg, arena}; + if (prepareModuleScope) prepareModuleScope(module->name, resumeScope); + program->visit(&gp); + + if (FFlag::LuauUserTypeFunTypecheck) + { + // Handle type function globals as well, without preparing a module scope since they have a separate environment + GlobalPrepopulator tfgp{NotNull{typeFunctionRuntime->rootScope.get()}, arena, dfg}; + program->visit(&tfgp); + } } void ConstraintGenerator::prepopulateGlobalScope(const ScopePtr& globalScope, AstStatBlock* program) @@ -3895,6 +4004,13 @@ void ConstraintGenerator::prepopulateGlobalScope(const ScopePtr& globalScope, As prepareModuleScope(module->name, globalScope); program->visit(&gp); + + if (FFlag::LuauUserTypeFunTypecheck) + { + // Handle type function globals as well, without preparing a module scope since they have a separate environment + GlobalPrepopulator tfgp{NotNull{typeFunctionRuntime->rootScope.get()}, arena, dfg}; + program->visit(&tfgp); + } } bool ConstraintGenerator::recordPropertyAssignment(TypeId ty) diff --git a/Analysis/src/DataFlowGraph.cpp b/Analysis/src/DataFlowGraph.cpp index f3fc5fbc..cf393612 100644 --- a/Analysis/src/DataFlowGraph.cpp +++ b/Analysis/src/DataFlowGraph.cpp @@ -1168,6 +1168,8 @@ void DataFlowGraphBuilder::visitType(AstType* t) return visitType(f); else if (auto tyof = t->as()) return visitType(tyof); + else if (auto o = t->as()) + return; else if (auto u = t->as()) return visitType(u); else if (auto i = t->as()) diff --git a/Analysis/src/EmbeddedBuiltinDefinitions.cpp b/Analysis/src/EmbeddedBuiltinDefinitions.cpp index ff2f02c0..d4693c9c 100644 --- a/Analysis/src/EmbeddedBuiltinDefinitions.cpp +++ b/Analysis/src/EmbeddedBuiltinDefinitions.cpp @@ -317,4 +317,83 @@ std::string getBuiltinDefinitionSource() return result; } +// TODO: split into separate tagged unions when the new solver can appropriately handle that. +static const std::string kBuiltinDefinitionTypesSrc = R"BUILTIN_SRC( + +export type type = { + tag: "nil" | "unknown" | "never" | "any" | "boolean" | "number" | "string" | "buffer" | "thread" | + "singleton" | "negation" | "union" | "intesection" | "table" | "function" | "class" | "generic", + + is: (self: type, arg: string) -> boolean, + + -- for singleton type + value: (self: type) -> (string | boolean | nil), + + -- for negation type + inner: (self: type) -> type, + + -- for union and intersection types + components: (self: type) -> {type}, + + -- for table type + setproperty: (self: type, key: type, value: type?) -> (), + setreadproperty: (self: type, key: type, value: type?) -> (), + setwriteproperty: (self: type, key: type, value: type?) -> (), + readproperty: (self: type, key: type) -> type?, + writeproperty: (self: type, key: type) -> type?, + properties: (self: type) -> { [type]: { read: type?, write: type? } }, + setindexer: (self: type, index: type, result: type) -> (), + setreadindexer: (self: type, index: type, result: type) -> (), + setwriteindexer: (self: type, index: type, result: type) -> (), + indexer: (self: type) -> { index: type, readresult: type, writeresult: type }?, + readindexer: (self: type) -> { index: type, result: type }?, + writeindexer: (self: type) -> { index: type, result: type }?, + setmetatable: (self: type, arg: type) -> (), + metatable: (self: type) -> type?, + + -- for function type + setparameters: (self: type, head: {type}?, tail: type?) -> (), + parameters: (self: type) -> { head: {type}?, tail: type? }, + setreturns: (self: type, head: {type}?, tail: type? ) -> (), + returns: (self: type) -> { head: {type}?, tail: type? }, + setgenerics: (self: type, {type}?) -> (), + generics: (self: type) -> {type}, + + -- for class type + -- 'properties', 'metatable', 'indexer', 'readindexer' and 'writeindexer' are shared with table type + readparent: (self: type) -> type?, + writeparent: (self: type) -> type?, + + -- for generic type + name: (self: type) -> string?, + ispack: (self: type) -> boolean, +} + +declare types: { + unknown: type, + never: type, + any: type, + boolean: type, + number: type, + string: type, + thread: type, + buffer: type, + + singleton: @checked (arg: string | boolean | nil) -> type, + generic: @checked (name: string, ispack: boolean?) -> type, + negationof: @checked (arg: type) -> type, + unionof: @checked (...type) -> type, + intersectionof: @checked (...type) -> type, + newtable: @checked (props: {[type]: type} | {[type]: { read: type, write: type } } | nil, indexer: { index: type, readresult: type, writeresult: type }?, metatable: type?) -> type, + newfunction: @checked (parameters: { head: {type}?, tail: type? }?, returns: { head: {type}?, tail: type? }?, generics: {type}?) -> type, + copy: @checked (arg: type) -> type, +} + +)BUILTIN_SRC"; + +std::string getTypeFunctionDefinitionSource() +{ + return kBuiltinDefinitionTypesSrc; +} + } // namespace Luau diff --git a/Analysis/src/Error.cpp b/Analysis/src/Error.cpp index 66b61d6b..1cd25c94 100644 --- a/Analysis/src/Error.cpp +++ b/Analysis/src/Error.cpp @@ -8,6 +8,7 @@ #include "Luau/StringUtils.h" #include "Luau/ToString.h" #include "Luau/Type.h" +#include "Luau/TypeChecker2.h" #include "Luau/TypeFunction.h" #include @@ -17,6 +18,7 @@ #include LUAU_FASTINTVARIABLE(LuauIndentTypeMismatchMaxTypeLength, 10) +LUAU_FASTFLAG(LuauNonStrictFuncDefErrorFix) static std::string wrongNumberOfArgsString( size_t expectedCount, @@ -116,7 +118,10 @@ struct ErrorConverter size_t luauIndentTypeMismatchMaxTypeLength = size_t(FInt::LuauIndentTypeMismatchMaxTypeLength); if (givenType.length() <= luauIndentTypeMismatchMaxTypeLength || wantedType.length() <= luauIndentTypeMismatchMaxTypeLength) return "Type " + given + " could not be converted into " + wanted; - return "Type\n " + given + "\ncould not be converted into\n " + wanted; + if (FFlag::LuauImproveTypePathsInErrors) + return "Type\n\t" + given + "\ncould not be converted into\n\t" + wanted; + else + return "Type\n " + given + "\ncould not be converted into\n " + wanted; }; if (givenTypeName == wantedTypeName) @@ -751,8 +756,15 @@ struct ErrorConverter std::string operator()(const NonStrictFunctionDefinitionError& e) const { - return "Argument " + e.argument + " with type '" + toString(e.argumentType) + "' in function '" + e.functionName + - "' is used in a way that will run time error"; + if (FFlag::LuauNonStrictFuncDefErrorFix && 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"; + } } std::string operator()(const PropertyAccessViolation& e) const diff --git a/Analysis/src/FragmentAutocomplete.cpp b/Analysis/src/FragmentAutocomplete.cpp index ef8cce21..edcab3a5 100644 --- a/Analysis/src/FragmentAutocomplete.cpp +++ b/Analysis/src/FragmentAutocomplete.cpp @@ -39,6 +39,9 @@ LUAU_FASTFLAGVARIABLE(LuauPersistConstraintGenerationScopes) LUAU_FASTFLAG(LuauModuleHoldsAstRoot) LUAU_FASTFLAGVARIABLE(LuauCloneTypeAliasBindings) LUAU_FASTFLAGVARIABLE(LuauCloneReturnTypePack) +LUAU_FASTFLAGVARIABLE(LuauIncrementalAutocompleteDemandBasedCloning) +LUAU_FASTFLAG(LuauUserTypeFunTypecheck) +LUAU_FASTFLAGVARIABLE(LuauFragmentNoTypeFunEval) namespace { @@ -84,8 +87,140 @@ void cloneModuleMap( } } +struct UsageFinder : public AstVisitor +{ + + explicit UsageFinder(NotNull dfg) + : dfg(dfg) + { + // We explicitly suggest that the usage finder propulate types for instance and enum by default + // These are common enough types that sticking them in the environment is a good idea + // and it lets magic functions work correctly too. + referencedBindings.emplace_back("Instance"); + referencedBindings.emplace_back("Enum"); + } + + bool visit(AstExprConstantString* expr) override + { + // Populating strings in the referenced bindings is nice too, because it means that magic functions that look + // up types by names will work correctly too. + // Only if the actual type alias exists will we populate it over, otherwise, the strings will just get ignored + referencedBindings.emplace_back(expr->value.data, expr->value.size); + return true; + } + + bool visit(AstType* node) override + { + return true; + } + + bool visit(AstStatTypeAlias* alias) override + { + declaredAliases.insert(std::string(alias->name.value)); + return true; + } + + bool visit(AstTypeReference* ref) override + { + if (std::optional prefix = ref->prefix) + referencedImportedBindings.emplace_back(prefix->value, ref->name.value); + else + referencedBindings.emplace_back(ref->name.value); + + return true; + } + + bool visit(AstExpr* expr) override + { + if (auto opt = dfg->getDefOptional(expr)) + mentionedDefs.insert(opt->get()); + if (auto ref = dfg->getRefinementKey(expr)) + mentionedDefs.insert(ref->def); + if (auto local = expr->as()) + localBindingsReferenced.emplace_back(dfg->getDef(local), local->local); + return true; + } + + NotNull dfg; + DenseHashSet declaredAliases{""}; + std::vector> localBindingsReferenced; + DenseHashSet mentionedDefs{nullptr}; + std::vector referencedBindings{""}; + std::vector> referencedImportedBindings{{"", ""}}; +}; + +// Runs the `UsageFinder` traversal on the fragment and grabs all of the types that are +// referenced in the fragment. We'll clone these and place them in the appropriate spots +// in the scope so that they are available during typechecking. +void cloneTypesFromFragment( + CloneState& cloneState, + const Scope* staleScope, + const ModulePtr& staleModule, + NotNull destArena, + NotNull dfg, + AstStatBlock* program, + Scope* destScope +) +{ + LUAU_TIMETRACE_SCOPE("Luau::cloneTypesFromFragment", "FragmentAutocomplete"); + + UsageFinder f{dfg}; + program->visit(&f); + // These are defs that have been mentioned. find the appropriate lvalue type and rvalue types and place them in the scope + // First - any locals that have been mentioned in the fragment need to be placed in the bindings and lvalueTypes secionts. + + for (const auto& d : f.mentionedDefs) + { + if (std::optional rValueRefinement = staleScope->lookupRValueRefinementType(NotNull{d})) + { + destScope->rvalueRefinements[d] = Luau::cloneIncremental(*rValueRefinement, *destArena, cloneState, destScope); + } + + if (std::optional lValue = staleScope->lookupUnrefinedType(NotNull{d})) + { + destScope->lvalueTypes[d] = Luau::cloneIncremental(*lValue, *destArena, cloneState, destScope); + } + } + for (const auto& [d, loc] : f.localBindingsReferenced) + { + if (std::optional> pair = staleScope->linearSearchForBindingPair(loc->name.value, true)) + { + destScope->lvalueTypes[d] = Luau::cloneIncremental(pair->second.typeId, *destArena, cloneState, destScope); + destScope->bindings[pair->first] = Luau::cloneIncremental(pair->second, *destArena, cloneState, destScope); + } + } + + // Second - any referenced type alias bindings need to be placed in scope so type annotation can be resolved. + // If the actual type alias appears in the fragment on the lhs as a definition (in declaredAliases), it will be processed during typechecking + // anyway + for (const auto& x : f.referencedBindings) + { + if (f.declaredAliases.contains(x)) + continue; + if (std::optional tf = staleScope->lookupType(x)) + { + destScope->privateTypeBindings[x] = Luau::cloneIncremental(*tf, *destArena, cloneState, destScope); + } + } + + // Third - any referenced imported type bindings need to be imported in + for (const auto& [mod, name] : f.referencedImportedBindings) + { + if (std::optional tf = staleScope->lookupImportedType(mod, name)) + { + destScope->importedTypeBindings[mod].insert_or_assign(name, Luau::cloneIncremental(*tf, *destArena, cloneState, destScope)); + } + } + + // Finally - clone the returnType on the staleScope. This helps avoid potential leaks of free types. + if (staleScope->returnType) + destScope->returnType = Luau::cloneIncremental(staleScope->returnType, *destArena, cloneState, destScope); +} + + struct MixedModeIncrementalTCDefFinder : public AstVisitor { + bool visit(AstExprLocal* local) override { referencedLocalDefs.emplace_back(local->local, local); @@ -337,6 +472,17 @@ FragmentAutocompleteAncestryResult findAncestryForFragmentParse(AstStatBlock* ro } } } + else if (auto typeFun = stat->as(); typeFun && FFlag::LuauUserTypeFunTypecheck) + { + if (typeFun->location.contains(cursorPos)) + { + for (AstLocal* loc : typeFun->body->args) + { + localStack.push_back(loc); + localMap[loc->name] = loc; + } + } + } } } } @@ -622,7 +768,7 @@ static void reportFragmentString(IFragmentAutocompleteReporter* reporter, std::s reporter->reportFragmentString(fragment); } -FragmentTypeCheckResult typecheckFragment_( +FragmentTypeCheckResult typecheckFragmentHelper_DEPRECATED( Frontend& frontend, AstStatBlock* root, const ModulePtr& stale, @@ -692,6 +838,7 @@ FragmentTypeCheckResult typecheckFragment_( frontend.builtinTypes, iceHandler, stale->getModuleScope(), + frontend.globals.globalTypeFunctionScope, nullptr, nullptr, NotNull{&dfg}, @@ -729,7 +876,7 @@ FragmentTypeCheckResult typecheckFragment_( incrementalModule->scopes.emplace_back(root->location, freshChildOfNearestScope); mixedModeCompatibility(closestScope, freshChildOfNearestScope, stale, NotNull{&dfg}, root); // closest Scope -> children = { ...., freshChildOfNearestScope} - // We need to trim nearestChild from the scope hierarcy + // We need to trim nearestChild from the scope hierarchy closestScope->children.emplace_back(freshChildOfNearestScope.get()); cg.visitFragmentRoot(freshChildOfNearestScope, root); // Trim nearestChild from the closestScope @@ -788,6 +935,149 @@ FragmentTypeCheckResult typecheckFragment_( return {std::move(incrementalModule), std::move(freshChildOfNearestScope)}; } +FragmentTypeCheckResult typecheckFragment_( + Frontend& frontend, + AstStatBlock* root, + const ModulePtr& stale, + const ScopePtr& closestScope, + const Position& cursorPos, + std::unique_ptr astAllocator, + const FrontendOptions& opts, + IFragmentAutocompleteReporter* reporter +) +{ + LUAU_TIMETRACE_SCOPE("Luau::typecheckFragment_", "FragmentAutocomplete"); + + freeze(stale->internalTypes); + freeze(stale->interfaceTypes); + ModulePtr incrementalModule = std::make_shared(); + incrementalModule->name = stale->name; + incrementalModule->humanReadableName = "Incremental$" + stale->humanReadableName; + incrementalModule->internalTypes.owningModule = incrementalModule.get(); + incrementalModule->interfaceTypes.owningModule = incrementalModule.get(); + incrementalModule->allocator = std::move(astAllocator); + incrementalModule->checkedInNewSolver = true; + unfreeze(incrementalModule->internalTypes); + unfreeze(incrementalModule->interfaceTypes); + + /// Setup typecheck limits + TypeCheckLimits limits; + if (opts.moduleTimeLimitSec) + limits.finishTime = TimeTrace::getClock() + *opts.moduleTimeLimitSec; + else + limits.finishTime = std::nullopt; + limits.cancellationToken = opts.cancellationToken; + + /// Icehandler + NotNull iceHandler{&frontend.iceHandler}; + /// Make the shared state for the unifier (recursion + iteration limits) + UnifierSharedState unifierState{iceHandler}; + unifierState.counters.recursionLimit = FInt::LuauTypeInferRecursionLimit; + unifierState.counters.iterationLimit = limits.unifierIterationLimit.value_or(FInt::LuauTypeInferIterationLimit); + + /// Initialize the normalizer + Normalizer normalizer{&incrementalModule->internalTypes, frontend.builtinTypes, NotNull{&unifierState}}; + + /// User defined type functions runtime + TypeFunctionRuntime typeFunctionRuntime(iceHandler, NotNull{&limits}); + + if (FFlag::LuauFragmentNoTypeFunEval || FFlag::LuauUserTypeFunTypecheck) + typeFunctionRuntime.allowEvaluation = false; + + /// Create a DataFlowGraph just for the surrounding context + DataFlowGraph dfg = DataFlowGraphBuilder::build(root, NotNull{&incrementalModule->defArena}, NotNull{&incrementalModule->keyArena}, iceHandler); + reportWaypoint(reporter, FragmentAutocompleteWaypoint::DfgBuildEnd); + + SimplifierPtr simplifier = newSimplifier(NotNull{&incrementalModule->internalTypes}, frontend.builtinTypes); + + FrontendModuleResolver& resolver = getModuleResolver(frontend, opts); + + /// Contraint Generator + ConstraintGenerator cg{ + incrementalModule, + NotNull{&normalizer}, + NotNull{simplifier.get()}, + NotNull{&typeFunctionRuntime}, + NotNull{&resolver}, + frontend.builtinTypes, + iceHandler, + stale->getModuleScope(), + frontend.globals.globalTypeFunctionScope, + nullptr, + nullptr, + NotNull{&dfg}, + {} + }; + + CloneState cloneState{frontend.builtinTypes}; + std::shared_ptr freshChildOfNearestScope = std::make_shared(nullptr); + incrementalModule->scopes.emplace_back(root->location, freshChildOfNearestScope); + freshChildOfNearestScope->interiorFreeTypes.emplace(); + cg.rootScope = freshChildOfNearestScope.get(); + + if (FFlag::LuauUserTypeFunTypecheck) + { + // Create module-local scope for the type function environment + ScopePtr localTypeFunctionScope = std::make_shared(cg.typeFunctionScope); + localTypeFunctionScope->location = root->location; + cg.typeFunctionRuntime->rootScope = localTypeFunctionScope; + } + + reportWaypoint(reporter, FragmentAutocompleteWaypoint::CloneAndSquashScopeStart); + cloneTypesFromFragment( + cloneState, closestScope.get(), stale, NotNull{&incrementalModule->internalTypes}, NotNull{&dfg}, root, freshChildOfNearestScope.get() + ); + reportWaypoint(reporter, FragmentAutocompleteWaypoint::CloneAndSquashScopeEnd); + + cg.visitFragmentRoot(freshChildOfNearestScope, root); + + for (auto p : cg.scopes) + incrementalModule->scopes.emplace_back(std::move(p)); + + + reportWaypoint(reporter, FragmentAutocompleteWaypoint::ConstraintSolverStart); + + /// Initialize the constraint solver and run it + ConstraintSolver cs{ + NotNull{&normalizer}, + NotNull{simplifier.get()}, + NotNull{&typeFunctionRuntime}, + NotNull(cg.rootScope), + borrowConstraints(cg.constraints), + NotNull{&cg.scopeToFunction}, + incrementalModule->name, + NotNull{&resolver}, + {}, + nullptr, + NotNull{&dfg}, + limits + }; + + try + { + cs.run(); + } + catch (const TimeLimitError&) + { + stale->timeout = true; + } + catch (const UserCancelError&) + { + stale->cancelled = true; + } + + reportWaypoint(reporter, FragmentAutocompleteWaypoint::ConstraintSolverEnd); + + // In frontend we would forbid internal types + // because this is just for autocomplete, we don't actually care + // We also don't even need to typecheck - just synthesize types as best as we can + + freeze(incrementalModule->internalTypes); + freeze(incrementalModule->interfaceTypes); + freshChildOfNearestScope->parent = closestScope; + return {std::move(incrementalModule), std::move(freshChildOfNearestScope)}; +} + std::pair typecheckFragment( Frontend& frontend, @@ -850,7 +1140,11 @@ std::pair typecheckFragment( FrontendOptions frontendOptions = opts.value_or(frontend.options); const ScopePtr& closestScope = findClosestScope(module, parseResult.nearestStatement); FragmentTypeCheckResult result = - typecheckFragment_(frontend, parseResult.root, module, closestScope, cursorPos, std::move(parseResult.alloc), frontendOptions, reporter); + FFlag::LuauIncrementalAutocompleteDemandBasedCloning + ? typecheckFragment_(frontend, parseResult.root, module, closestScope, cursorPos, std::move(parseResult.alloc), frontendOptions, reporter) + : typecheckFragmentHelper_DEPRECATED( + frontend, parseResult.root, module, closestScope, cursorPos, std::move(parseResult.alloc), frontendOptions, reporter + ); result.ancestry = std::move(parseResult.ancestry); reportFragmentString(reporter, tryParse->fragmentToParse); return {FragmentTypeCheckStatus::Success, result}; diff --git a/Analysis/src/Frontend.cpp b/Analysis/src/Frontend.cpp index 318db0b6..8cbcc1b7 100644 --- a/Analysis/src/Frontend.cpp +++ b/Analysis/src/Frontend.cpp @@ -1532,6 +1532,7 @@ ModulePtr check( NotNull moduleResolver, NotNull fileResolver, const ScopePtr& parentScope, + const ScopePtr& typeFunctionScope, std::function prepareModuleScope, FrontendOptions options, TypeCheckLimits limits, @@ -1548,6 +1549,7 @@ ModulePtr check( moduleResolver, fileResolver, parentScope, + typeFunctionScope, std::move(prepareModuleScope), options, limits, @@ -1609,6 +1611,7 @@ ModulePtr check( NotNull moduleResolver, NotNull fileResolver, const ScopePtr& parentScope, + const ScopePtr& typeFunctionScope, std::function prepareModuleScope, FrontendOptions options, TypeCheckLimits limits, @@ -1666,6 +1669,7 @@ ModulePtr check( builtinTypes, iceHandler, parentScope, + typeFunctionScope, std::move(prepareModuleScope), logger.get(), NotNull{&dfg}, @@ -1848,6 +1852,7 @@ ModulePtr Frontend::check( NotNull{forAutocomplete ? &moduleResolverForAutocomplete : &moduleResolver}, NotNull{fileResolver}, environmentScope ? *environmentScope : globals.globalScope, + globals.globalTypeFunctionScope, prepareModuleScopeWrap, options, typeCheckLimits, diff --git a/Analysis/src/Generalization.cpp b/Analysis/src/Generalization.cpp index 054ad509..e5f47a90 100644 --- a/Analysis/src/Generalization.cpp +++ b/Analysis/src/Generalization.cpp @@ -359,7 +359,7 @@ struct FreeTypeSearcher : TypeVisitor DenseHashSet seenPositive{nullptr}; DenseHashSet seenNegative{nullptr}; - bool seenWithPolarity(const void* ty) + bool seenWithCurrentPolarity(const void* ty) { switch (polarity) { @@ -401,7 +401,7 @@ struct FreeTypeSearcher : TypeVisitor bool visit(TypeId ty) override { - if (cachedTypes->contains(ty) || seenWithPolarity(ty)) + if (cachedTypes->contains(ty) || seenWithCurrentPolarity(ty)) return false; LUAU_ASSERT(ty); @@ -410,7 +410,7 @@ struct FreeTypeSearcher : TypeVisitor bool visit(TypeId ty, const FreeType& ft) override { - if (cachedTypes->contains(ty) || seenWithPolarity(ty)) + if (cachedTypes->contains(ty) || seenWithCurrentPolarity(ty)) return false; if (!subsumes(scope, ft.scope)) @@ -435,7 +435,7 @@ struct FreeTypeSearcher : TypeVisitor bool visit(TypeId ty, const TableType& tt) override { - if (cachedTypes->contains(ty) || seenWithPolarity(ty)) + if (cachedTypes->contains(ty) || seenWithCurrentPolarity(ty)) return false; if ((tt.state == TableState::Free || tt.state == TableState::Unsealed) && subsumes(scope, tt.scope)) @@ -481,7 +481,7 @@ struct FreeTypeSearcher : TypeVisitor bool visit(TypeId ty, const FunctionType& ft) override { - if (cachedTypes->contains(ty) || seenWithPolarity(ty)) + if (cachedTypes->contains(ty) || seenWithCurrentPolarity(ty)) return false; flip(); @@ -500,7 +500,7 @@ struct FreeTypeSearcher : TypeVisitor bool visit(TypePackId tp, const FreeTypePack& ftp) override { - if (seenWithPolarity(tp)) + if (seenWithCurrentPolarity(tp)) return false; if (!subsumes(scope, ftp.scope)) @@ -547,7 +547,7 @@ struct TypeCacher : TypeOnceVisitor { } - void cache(TypeId ty) + void cache(TypeId ty) const { cachedTypes->insert(ty); } diff --git a/Analysis/src/GlobalTypes.cpp b/Analysis/src/GlobalTypes.cpp index 9dd60caa..04a22b20 100644 --- a/Analysis/src/GlobalTypes.cpp +++ b/Analysis/src/GlobalTypes.cpp @@ -9,6 +9,7 @@ GlobalTypes::GlobalTypes(NotNull builtinTypes) : builtinTypes(builtinTypes) { globalScope = std::make_shared(globalTypes.addTypePack(TypePackVar{FreeTypePack{TypeLevel{}}})); + globalTypeFunctionScope = std::make_shared(globalTypes.addTypePack(TypePackVar{FreeTypePack{TypeLevel{}}})); globalScope->addBuiltinTypeBinding("any", TypeFun{{}, builtinTypes->anyType}); globalScope->addBuiltinTypeBinding("nil", TypeFun{{}, builtinTypes->nilType}); diff --git a/Analysis/src/NonStrictTypeChecker.cpp b/Analysis/src/NonStrictTypeChecker.cpp index 93a02c3f..eb86d401 100644 --- a/Analysis/src/NonStrictTypeChecker.cpp +++ b/Analysis/src/NonStrictTypeChecker.cpp @@ -22,6 +22,7 @@ LUAU_FASTFLAG(LuauFreeTypesMustHaveBounds) LUAU_FASTFLAGVARIABLE(LuauNonStrictVisitorImprovements) LUAU_FASTFLAGVARIABLE(LuauNewNonStrictWarnOnUnknownGlobals) +LUAU_FASTFLAGVARIABLE(LuauNonStrictFuncDefErrorFix) namespace Luau { @@ -763,7 +764,17 @@ struct NonStrictTypeChecker for (AstLocal* local : exprFn->args) { if (std::optional ty = willRunTimeErrorFunctionDefinition(local, remainder)) - reportError(NonStrictFunctionDefinitionError{exprFn->debugname.value, local->name.value, *ty}, local->location); + { + if (FFlag::LuauNonStrictFuncDefErrorFix) + { + const char* debugname = exprFn->debugname.value; + reportError(NonStrictFunctionDefinitionError{debugname ? debugname : "", local->name.value, *ty}, local->location); + } + else + { + reportError(NonStrictFunctionDefinitionError{exprFn->debugname.value, local->name.value, *ty}, local->location); + } + } remainder.remove(dfg->getDef(local)); } return remainder; diff --git a/Analysis/src/Normalize.cpp b/Analysis/src/Normalize.cpp index dc35a5a5..f1ed03b4 100644 --- a/Analysis/src/Normalize.cpp +++ b/Analysis/src/Normalize.cpp @@ -22,7 +22,7 @@ LUAU_FASTINTVARIABLE(LuauNormalizeIntersectionLimit, 200) LUAU_FASTFLAG(LuauSolverV2) LUAU_FASTFLAGVARIABLE(LuauNormalizeNegationFix) LUAU_FASTFLAGVARIABLE(LuauFixInfiniteRecursionInNormalization) -LUAU_FASTFLAGVARIABLE(LuauFixNormalizedIntersectionOfNegatedClass) +LUAU_FASTFLAGVARIABLE(LuauNormalizedBufferIsNotUnknown) namespace Luau { @@ -304,7 +304,9 @@ bool NormalizedType::isUnknown() const // Otherwise, we can still be unknown! bool hasAllPrimitives = isPrim(booleans, PrimitiveType::Boolean) && isPrim(nils, PrimitiveType::NilType) && isNumber(numbers) && - strings.isString() && isPrim(threads, PrimitiveType::Thread) && isThread(threads); + strings.isString() && + (FFlag::LuauNormalizedBufferIsNotUnknown ? isThread(threads) && isBuffer(buffers) + : isPrim(threads, PrimitiveType::Thread) && isThread(threads)); // Check is class bool isTopClass = false; @@ -2289,7 +2291,7 @@ void Normalizer::intersectClassesWithClass(NormalizedClassType& heres, TypeId th for (auto nIt = negations.begin(); nIt != negations.end();) { - if (FFlag::LuauFixNormalizedIntersectionOfNegatedClass && isSubclass(there, *nIt)) + if (isSubclass(there, *nIt)) { // Hitting this block means that the incoming class is a // subclass of this type, _and_ one of its negations is a diff --git a/Analysis/src/RequireTracer.cpp b/Analysis/src/RequireTracer.cpp index 95c1a344..76970f73 100644 --- a/Analysis/src/RequireTracer.cpp +++ b/Analysis/src/RequireTracer.cpp @@ -4,8 +4,6 @@ #include "Luau/Ast.h" #include "Luau/Module.h" -LUAU_FASTFLAGVARIABLE(LuauExtendedSimpleRequire) - namespace Luau { @@ -106,96 +104,50 @@ struct RequireTracer : AstVisitor { ModuleInfo moduleContext{currentModuleName}; - if (FFlag::LuauExtendedSimpleRequire) + // seed worklist with require arguments + work.reserve(requireCalls.size()); + + for (AstExprCall* require : requireCalls) + work.push_back(require->args.data[0]); + + // push all dependent expressions to the work stack; note that the vector is modified during traversal + for (size_t i = 0; i < work.size(); ++i) { - // seed worklist with require arguments - work.reserve(requireCalls.size()); - - for (AstExprCall* require : requireCalls) - work.push_back(require->args.data[0]); - - // push all dependent expressions to the work stack; note that the vector is modified during traversal - for (size_t i = 0; i < work.size(); ++i) - { - if (AstNode* dep = getDependent(work[i])) - work.push_back(dep); - } - - // resolve all expressions to a module info - for (size_t i = work.size(); i > 0; --i) - { - AstNode* expr = work[i - 1]; - - // when multiple expressions depend on the same one we push it to work queue multiple times - if (result.exprs.contains(expr)) - continue; - - std::optional info; - - if (AstNode* dep = getDependent(expr)) - { - const ModuleInfo* context = result.exprs.find(dep); - - if (context && expr->is()) - info = *context; // locals just inherit their dependent context, no resolution required - else if (context && (expr->is() || expr->is())) - info = *context; // simple group nodes propagate their value - else if (context && (expr->is() || expr->is())) - info = *context; // typeof type annotations will resolve to the typeof content - else if (AstExpr* asExpr = expr->asExpr()) - info = fileResolver->resolveModule(context, asExpr); - } - else if (AstExpr* asExpr = expr->asExpr()) - { - info = fileResolver->resolveModule(&moduleContext, asExpr); - } - - if (info) - result.exprs[expr] = std::move(*info); - } + if (AstNode* dep = getDependent(work[i])) + work.push_back(dep); } - else + + // resolve all expressions to a module info + for (size_t i = work.size(); i > 0; --i) { - // seed worklist with require arguments - work_DEPRECATED.reserve(requireCalls.size()); + AstNode* expr = work[i - 1]; - for (AstExprCall* require : requireCalls) - work_DEPRECATED.push_back(require->args.data[0]); + // when multiple expressions depend on the same one we push it to work queue multiple times + if (result.exprs.contains(expr)) + continue; - // push all dependent expressions to the work stack; note that the vector is modified during traversal - for (size_t i = 0; i < work_DEPRECATED.size(); ++i) - if (AstExpr* dep = getDependent_DEPRECATED(work_DEPRECATED[i])) - work_DEPRECATED.push_back(dep); + std::optional info; - // resolve all expressions to a module info - for (size_t i = work_DEPRECATED.size(); i > 0; --i) + if (AstNode* dep = getDependent(expr)) { - AstExpr* expr = work_DEPRECATED[i - 1]; + const ModuleInfo* context = result.exprs.find(dep); - // when multiple expressions depend on the same one we push it to work queue multiple times - if (result.exprs.contains(expr)) - continue; - - std::optional info; - - if (AstExpr* dep = getDependent_DEPRECATED(expr)) - { - const ModuleInfo* context = result.exprs.find(dep); - - // locals just inherit their dependent context, no resolution required - if (expr->is()) - info = context ? std::optional(*context) : std::nullopt; - else - info = fileResolver->resolveModule(context, expr); - } - else - { - info = fileResolver->resolveModule(&moduleContext, expr); - } - - if (info) - result.exprs[expr] = std::move(*info); + if (context && expr->is()) + info = *context; // locals just inherit their dependent context, no resolution required + else if (context && (expr->is() || expr->is())) + info = *context; // simple group nodes propagate their value + else if (context && (expr->is() || expr->is())) + info = *context; // typeof type annotations will resolve to the typeof content + else if (AstExpr* asExpr = expr->asExpr()) + info = fileResolver->resolveModule(context, asExpr); } + else if (AstExpr* asExpr = expr->asExpr()) + { + info = fileResolver->resolveModule(&moduleContext, asExpr); + } + + if (info) + result.exprs[expr] = std::move(*info); } // resolve all requires according to their argument @@ -224,7 +176,6 @@ struct RequireTracer : AstVisitor ModuleName currentModuleName; DenseHashMap locals; - std::vector work_DEPRECATED; std::vector work; std::vector requireCalls; }; diff --git a/Analysis/src/Scope.cpp b/Analysis/src/Scope.cpp index db99d827..35259fb6 100644 --- a/Analysis/src/Scope.cpp +++ b/Analysis/src/Scope.cpp @@ -84,6 +84,17 @@ std::optional Scope::lookupUnrefinedType(DefId def) const return std::nullopt; } +std::optional Scope::lookupRValueRefinementType(DefId def) const +{ + for (const Scope* current = this; current; current = current->parent.get()) + { + if (auto ty = current->rvalueRefinements.find(def)) + return *ty; + } + + return std::nullopt; +} + std::optional Scope::lookup(DefId def) const { for (const Scope* current = this; current; current = current->parent.get()) @@ -181,6 +192,29 @@ std::optional Scope::linearSearchForBinding(const std::string& name, bo return std::nullopt; } +std::optional> Scope::linearSearchForBindingPair(const std::string& name, bool traverseScopeChain) const +{ + const Scope* scope = this; + + while (scope) + { + for (auto& [n, binding] : scope->bindings) + { + if (n.local && n.local->name == name.c_str()) + return {{n, binding}}; + else if (n.global.value && n.global == name.c_str()) + return {{n, binding}}; + } + + scope = scope->parent.get(); + + if (!traverseScopeChain) + break; + } + + return std::nullopt; +} + // Updates the `this` scope with the assignments from the `childScope` including ones that doesn't exist in `this`. void Scope::inheritAssignments(const ScopePtr& childScope) { diff --git a/Analysis/src/Subtyping.cpp b/Analysis/src/Subtyping.cpp index a4f2ce1e..e55255da 100644 --- a/Analysis/src/Subtyping.cpp +++ b/Analysis/src/Subtyping.cpp @@ -22,7 +22,6 @@ #include LUAU_FASTFLAGVARIABLE(DebugLuauSubtypingCheckPathValidity) -LUAU_FASTFLAGVARIABLE(LuauSubtypingFixTailPack) namespace Luau { @@ -754,7 +753,8 @@ SubtypingResult Subtyping::isCovariantWith(SubtypingEnvironment& env, TypePackId // Match head types pairwise for (size_t i = 0; i < headSize; ++i) - results.push_back(isCovariantWith(env, subHead[i], superHead[i], scope).withBothComponent(TypePath::Index{i})); + results.push_back(isCovariantWith(env, subHead[i], superHead[i], scope).withBothComponent(TypePath::Index{i, TypePath::Index::Variant::Pack}) + ); // Handle mismatched head sizes @@ -767,7 +767,7 @@ SubtypingResult Subtyping::isCovariantWith(SubtypingEnvironment& env, TypePackId 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})); + .withSuperComponent(TypePath::Index{i, TypePath::Index::Variant::Pack})); } else if (auto gt = get(*subTail)) { @@ -821,7 +821,7 @@ SubtypingResult Subtyping::isCovariantWith(SubtypingEnvironment& env, TypePackId { for (size_t i = headSize; i < subHead.size(); ++i) results.push_back(isCovariantWith(env, subHead[i], vt->ty, scope) - .withSubComponent(TypePath::Index{i}) + .withSubComponent(TypePath::Index{i, TypePath::Index::Variant::Pack}) .withSuperPath(TypePath::PathBuilder().tail().variadic().build())); } else if (auto gt = get(*superTail)) @@ -859,7 +859,7 @@ SubtypingResult Subtyping::isCovariantWith(SubtypingEnvironment& env, TypePackId else return SubtypingResult{false} .withSuperComponent(TypePath::PackField::Tail) - .withError({scope->location, UnexpectedTypePackInSubtyping{FFlag::LuauSubtypingFixTailPack ? *superTail : *subTail}}); + .withError({scope->location, UnexpectedTypePackInSubtyping{*superTail}}); } else return {false}; @@ -1100,7 +1100,7 @@ SubtypingResult Subtyping::isCovariantWith(SubtypingEnvironment& env, const Unio std::vector subtypings; size_t i = 0; for (TypeId ty : subUnion) - subtypings.push_back(isCovariantWith(env, ty, superTy, scope).withSubComponent(TypePath::Index{i++})); + subtypings.push_back(isCovariantWith(env, ty, superTy, scope).withSubComponent(TypePath::Index{i++, TypePath::Index::Variant::Union})); return SubtypingResult::all(subtypings); } @@ -1110,7 +1110,7 @@ SubtypingResult Subtyping::isCovariantWith(SubtypingEnvironment& env, TypeId sub std::vector subtypings; size_t i = 0; for (TypeId ty : superIntersection) - subtypings.push_back(isCovariantWith(env, subTy, ty, scope).withSuperComponent(TypePath::Index{i++})); + subtypings.push_back(isCovariantWith(env, subTy, ty, scope).withSuperComponent(TypePath::Index{i++, TypePath::Index::Variant::Intersection})); return SubtypingResult::all(subtypings); } @@ -1120,7 +1120,7 @@ SubtypingResult Subtyping::isCovariantWith(SubtypingEnvironment& env, const Inte std::vector subtypings; size_t i = 0; for (TypeId ty : subIntersection) - subtypings.push_back(isCovariantWith(env, ty, superTy, scope).withSubComponent(TypePath::Index{i++})); + subtypings.push_back(isCovariantWith(env, ty, superTy, scope).withSubComponent(TypePath::Index{i++, TypePath::Index::Variant::Intersection})); return SubtypingResult::any(subtypings); } diff --git a/Analysis/src/TableLiteralInference.cpp b/Analysis/src/TableLiteralInference.cpp index 61418b78..f5127870 100644 --- a/Analysis/src/TableLiteralInference.cpp +++ b/Analysis/src/TableLiteralInference.cpp @@ -13,8 +13,6 @@ #include "Luau/TypeUtils.h" #include "Luau/Unifier2.h" -LUAU_FASTFLAGVARIABLE(LuauDontInPlaceMutateTableType) -LUAU_FASTFLAGVARIABLE(LuauAllowNonSharedTableTypesInLiteral) LUAU_FASTFLAGVARIABLE(LuauBidirectionalInferenceUpcast) namespace Luau @@ -277,19 +275,11 @@ TypeId matchLiteralType( Property& prop = it->second; - if (FFlag::LuauAllowNonSharedTableTypesInLiteral) - { - // If we encounter a duplcate property, we may have already - // set it to be read-only. If that's the case, the only thing - // that will definitely crash is trying to access a write - // only property. - LUAU_ASSERT(!prop.isWriteOnly()); - } - else - { - // Table literals always initially result in shared read-write types - LUAU_ASSERT(prop.isShared()); - } + // If we encounter a duplcate property, we may have already + // set it to be read-only. If that's the case, the only thing + // that will definitely crash is trying to access a write + // only property. + LUAU_ASSERT(!prop.isWriteOnly()); TypeId propTy = *prop.readTy; auto it2 = expectedTableTy->props.find(keyStr); @@ -322,10 +312,7 @@ TypeId matchLiteralType( else tableTy->indexer = TableIndexer{expectedTableTy->indexer->indexType, matchedType}; - if (FFlag::LuauDontInPlaceMutateTableType) - keysToDelete.insert(item.key->as()); - else - tableTy->props.erase(keyStr); + keysToDelete.insert(item.key->as()); } @@ -434,14 +421,11 @@ TypeId matchLiteralType( LUAU_ASSERT(!"Unexpected"); } - if (FFlag::LuauDontInPlaceMutateTableType) + for (const auto& key : keysToDelete) { - for (const auto& key : keysToDelete) - { - const AstArray& s = key->value; - std::string keyStr{s.data, s.data + s.size}; - tableTy->props.erase(keyStr); - } + const AstArray& s = key->value; + std::string keyStr{s.data, s.data + s.size}; + tableTy->props.erase(keyStr); } // Keys that the expectedType says we should have, but that aren't diff --git a/Analysis/src/Transpiler.cpp b/Analysis/src/Transpiler.cpp index 75d5c5e1..f8ba378e 100644 --- a/Analysis/src/Transpiler.cpp +++ b/Analysis/src/Transpiler.cpp @@ -14,6 +14,7 @@ LUAU_FASTFLAG(LuauStoreCSTData) LUAU_FASTFLAG(LuauExtendStatEndPosWithSemicolon) LUAU_FASTFLAG(LuauAstTypeGroup3) LUAU_FASTFLAG(LuauFixDoBlockEndLocation) +LUAU_FASTFLAG(LuauParseOptionalAsNode) namespace { @@ -271,6 +272,43 @@ private: const Position* commaPosition; }; +class ArgNameInserter +{ +public: + ArgNameInserter(Writer& w, AstArray> names, AstArray> colonPositions) + : writer(w) + , names(names) + , colonPositions(colonPositions) + { + } + + void operator()() + { + if (idx < names.size) + { + const auto name = names.data[idx]; + if (name.has_value()) + { + writer.advance(name->second.begin); + writer.identifier(name->first.value); + if (idx < colonPositions.size) + { + LUAU_ASSERT(colonPositions.data[idx].has_value()); + writer.advance(*colonPositions.data[idx]); + } + writer.symbol(":"); + } + } + idx++; + } + +private: + Writer& writer; + AstArray> names; + AstArray> colonPositions; + size_t idx = 0; +}; + struct Printer_DEPRECATED { explicit Printer_DEPRECATED(Writer& writer) @@ -1216,6 +1254,15 @@ struct Printer_DEPRECATED for (size_t i = 0; i < a->types.size; ++i) { + if (FFlag::LuauParseOptionalAsNode) + { + if (a->types.data[i]->is()) + { + writer.symbol("?"); + continue; + } + } + if (i > 0) { writer.maybeSpace(a->types.data[i]->location.begin, 2); @@ -1312,7 +1359,7 @@ struct Printer } } - void visualizeTypePackAnnotation(const AstTypePack& annotation, bool forVarArg) + void visualizeTypePackAnnotation(AstTypePack& annotation, bool forVarArg) { advance(annotation.location.begin); if (const AstTypePackVariadic* variadicTp = annotation.as()) @@ -1322,15 +1369,22 @@ struct Printer visualizeTypeAnnotation(*variadicTp->variadicType); } - else if (const AstTypePackGeneric* genericTp = annotation.as()) + else if (AstTypePackGeneric* genericTp = annotation.as()) { writer.symbol(genericTp->genericName.value); + if (const auto cstNode = lookupCstNode(genericTp)) + advance(cstNode->ellipsisPosition); writer.symbol("..."); } - else if (const AstTypePackExplicit* explicitTp = annotation.as()) + else if (AstTypePackExplicit* explicitTp = annotation.as()) { LUAU_ASSERT(!forVarArg); - visualizeTypeList(explicitTp->typeList, true); + if (const auto cstNode = lookupCstNode(explicitTp)) + visualizeTypeList( + explicitTp->typeList, true, cstNode->openParenthesesPosition, cstNode->closeParenthesesPosition, cstNode->commaPositions + ); + else + visualizeTypeList(explicitTp->typeList, true); } else { @@ -1338,19 +1392,37 @@ struct Printer } } - void visualizeTypeList(const AstTypeList& list, bool unconditionallyParenthesize) + void visualizeNamedTypeList( + const AstTypeList& list, + bool unconditionallyParenthesize, + std::optional openParenthesesPosition, + std::optional closeParenthesesPosition, + AstArray commaPositions, + AstArray> argNames, + AstArray> argNamesColonPositions + ) { size_t typeCount = list.types.size + (list.tailType != nullptr ? 1 : 0); if (typeCount == 0) { + if (openParenthesesPosition) + advance(*openParenthesesPosition); writer.symbol("("); + if (closeParenthesesPosition) + advance(*closeParenthesesPosition); writer.symbol(")"); } else if (typeCount == 1) { bool shouldParenthesize = unconditionallyParenthesize && (list.types.size == 0 || !list.types.data[0]->is()); if (FFlag::LuauAstTypeGroup3 ? shouldParenthesize : unconditionallyParenthesize) + { + if (openParenthesesPosition) + advance(*openParenthesesPosition); writer.symbol("("); + } + + ArgNameInserter(writer, argNames, argNamesColonPositions)(); // Only variadic tail if (list.types.size == 0) @@ -1363,33 +1435,50 @@ struct Printer } if (FFlag::LuauAstTypeGroup3 ? shouldParenthesize : unconditionallyParenthesize) + { + if (closeParenthesesPosition) + advance(*closeParenthesesPosition); writer.symbol(")"); + } } else { + if (openParenthesesPosition) + advance(*openParenthesesPosition); writer.symbol("("); - bool first = true; + CommaSeparatorInserter comma(writer, commaPositions.size > 0 ? commaPositions.begin() : nullptr); + ArgNameInserter argName(writer, argNames, argNamesColonPositions); for (const auto& el : list.types) { - if (first) - first = false; - else - writer.symbol(","); - + comma(); + argName(); visualizeTypeAnnotation(*el); } if (list.tailType) { - writer.symbol(","); + comma(); visualizeTypePackAnnotation(*list.tailType, false); } + if (closeParenthesesPosition) + advance(*closeParenthesesPosition); writer.symbol(")"); } } + void visualizeTypeList( + const AstTypeList& list, + bool unconditionallyParenthesize, + std::optional openParenthesesPosition = std::nullopt, + std::optional closeParenthesesPosition = std::nullopt, + AstArray commaPositions = {} + ) + { + visualizeNamedTypeList(list, unconditionallyParenthesize, openParenthesesPosition, closeParenthesesPosition, commaPositions, {}, {}); + } + bool isIntegerish(double d) { if (d <= std::numeric_limits::max() && d >= std::numeric_limits::min()) @@ -1406,7 +1495,7 @@ struct Printer { writer.symbol("("); visualize(*a->expr); - advance(Position{a->location.end.line, a->location.end.column - 1}); + advanceBefore(a->location.end, 1); writer.symbol(")"); } else if (expr.is()) @@ -1775,6 +1864,14 @@ struct Printer writer.advance(newPos); } + void advanceBefore(const Position& newPos, unsigned int tokenLength) + { + if (newPos.column >= tokenLength) + advance(Position{newPos.line, newPos.column - tokenLength}); + else + advance(newPos); + } + void visualize(AstStat& program) { advance(program.location.begin); @@ -1817,8 +1914,8 @@ struct Printer visualizeBlock(*a->body); if (const auto cstNode = lookupCstNode(a)) writer.advance(cstNode->untilPosition); - else if (a->condition->location.begin.column > 5) - writer.advance(Position{a->condition->location.begin.line, a->condition->location.begin.column - 6}); + else + advanceBefore(a->condition->location.begin, 6); writer.keyword("until"); visualize(*a->condition); } @@ -2121,7 +2218,20 @@ struct Printer { if (writeTypes) { - writer.keyword("type function"); + const auto cstNode = lookupCstNode(t); + if (t->exported) + writer.keyword("export"); + if (cstNode) + advance(cstNode->typeKeywordPosition); + else + writer.space(); + writer.keyword("type"); + if (cstNode) + advance(cstNode->functionKeywordPosition); + else + writer.space(); + writer.keyword("function"); + advance(t->nameLocation.begin); writer.identifier(t->name.value); visualizeFunctionBody(*t->body); } @@ -2152,16 +2262,22 @@ struct Printer if (program.hasSemicolon) { if (FFlag::LuauStoreCSTData) - advance(Position{program.location.end.line, program.location.end.column - 1}); + advanceBefore(program.location.end, 1); writer.symbol(";"); } } void visualizeFunctionBody(AstExprFunction& func) { + const auto cstNode = lookupCstNode(&func); + + // TODO(CLI-139347): need to handle attributes, argument types, and return type (incl. parentheses of return type) + if (func.generics.size > 0 || func.genericPacks.size > 0) { - CommaSeparatorInserter comma(writer); + CommaSeparatorInserter comma(writer, cstNode ? cstNode->genericsCommaPositions.begin() : nullptr); + if (cstNode) + advance(cstNode->openGenericsPosition); writer.symbol("<"); for (const auto& o : func.generics) { @@ -2176,13 +2292,19 @@ struct Printer writer.advance(o->location.begin); writer.identifier(o->name.value); + if (const auto* genericTypePackCstNode = lookupCstNode(o)) + advance(genericTypePackCstNode->ellipsisPosition); writer.symbol("..."); } + if (cstNode) + advance(cstNode->closeGenericsPosition); writer.symbol(">"); } + if (func.argLocation) + advance(func.argLocation->begin); writer.symbol("("); - CommaSeparatorInserter comma(writer); + CommaSeparatorInserter comma(writer, cstNode ? cstNode->argsCommaPositions.begin() : nullptr); for (size_t i = 0; i < func.args.size; ++i) { @@ -2212,10 +2334,14 @@ struct Printer } } + if (func.argLocation) + advanceBefore(func.argLocation->end, 1); writer.symbol(")"); if (writeTypes && func.returnAnnotation) { + if (cstNode) + advance(cstNode->returnSpecifierPosition); writer.symbol(":"); writer.space(); @@ -2340,9 +2466,13 @@ struct Printer } else if (const auto& a = typeAnnotation.as()) { + const auto cstNode = lookupCstNode(a); + if (a->generics.size > 0 || a->genericPacks.size > 0) { - CommaSeparatorInserter comma(writer); + CommaSeparatorInserter comma(writer, cstNode ? cstNode->genericsCommaPositions.begin() : nullptr); + if (cstNode) + advance(cstNode->openGenericsPosition); writer.symbol("<"); for (const auto& o : a->generics) { @@ -2357,15 +2487,29 @@ struct Printer writer.advance(o->location.begin); writer.identifier(o->name.value); + if (const auto* genericTypePackCstNode = lookupCstNode(o)) + advance(genericTypePackCstNode->ellipsisPosition); writer.symbol("..."); } + if (cstNode) + advance(cstNode->closeGenericsPosition); writer.symbol(">"); } { - visualizeTypeList(a->argTypes, true); + visualizeNamedTypeList( + a->argTypes, + true, + cstNode ? std::make_optional(cstNode->openArgsPosition) : std::nullopt, + cstNode ? std::make_optional(cstNode->closeArgsPosition) : std::nullopt, + cstNode ? cstNode->argumentsCommaPositions : Luau::AstArray{}, + a->argNames, + cstNode ? cstNode->argumentNameColonPositions : Luau::AstArray>{} + ); } + if (cstNode) + advance(cstNode->returnArrowPosition); writer.symbol("->"); visualizeTypeList(a->returnTypes, true); } @@ -2557,6 +2701,15 @@ struct Printer for (size_t i = 0; i < a->types.size; ++i) { + if (FFlag::LuauParseOptionalAsNode) + { + if (a->types.data[i]->is()) + { + writer.symbol("?"); + continue; + } + } + if (i > 0) { writer.maybeSpace(a->types.data[i]->location.begin, 2); @@ -2599,7 +2752,7 @@ struct Printer { writer.symbol("("); visualizeTypeAnnotation(*a->type); - advance(Position{a->location.end.line, a->location.end.column - 1}); + advanceBefore(a->location.end, 1); writer.symbol(")"); } else if (const auto& a = typeAnnotation.as()) diff --git a/Analysis/src/TypeAttach.cpp b/Analysis/src/TypeAttach.cpp index 0d038694..95ce6e6b 100644 --- a/Analysis/src/TypeAttach.cpp +++ b/Analysis/src/TypeAttach.cpp @@ -13,6 +13,8 @@ #include +LUAU_FASTFLAG(LuauStoreCSTData) + static char* allocateString(Luau::Allocator& allocator, std::string_view contents) { char* result = (char*)allocator.allocate(contents.size() + 1); @@ -305,7 +307,8 @@ public: std::optional* arg = &argNames.data[i++]; if (el) - new (arg) std::optional(AstArgumentName(AstName(el->name.c_str()), el->location)); + new (arg) + std::optional(AstArgumentName(AstName(el->name.c_str()), FFlag::LuauStoreCSTData ? Location() : el->location)); else new (arg) std::optional(); } diff --git a/Analysis/src/TypeChecker2.cpp b/Analysis/src/TypeChecker2.cpp index 01db570a..41ace420 100644 --- a/Analysis/src/TypeChecker2.cpp +++ b/Analysis/src/TypeChecker2.cpp @@ -26,10 +26,13 @@ #include "Luau/VisitType.h" #include +#include LUAU_FASTFLAG(DebugLuauMagicTypes) LUAU_FASTFLAG(LuauFreeTypesMustHaveBounds) +LUAU_FASTFLAGVARIABLE(LuauImproveTypePathsInErrors) +LUAU_FASTFLAG(LuauUserTypeFunTypecheck) namespace Luau { @@ -1201,7 +1204,8 @@ void TypeChecker2::visit(AstStatTypeAlias* stat) void TypeChecker2::visit(AstStatTypeFunction* stat) { - // TODO: add type checking for user-defined type functions + if (FFlag::LuauUserTypeFunTypecheck) + visit(stat->body); } void TypeChecker2::visit(AstTypeList types) @@ -2701,20 +2705,61 @@ Reasonings TypeChecker2::explainReasonings_(TID subTy, TID superTy, Location loc if (!subLeafTy && !superLeafTy && !subLeafTp && !superLeafTp) ice->ice("Subtyping test returned a reasoning where one path ends at a type and the other ends at a pack.", location); - std::string relation = "a subtype of"; - if (reasoning.variance == SubtypingVariance::Invariant) - relation = "exactly"; - else if (reasoning.variance == SubtypingVariance::Contravariant) - relation = "a supertype of"; + if (FFlag::LuauImproveTypePathsInErrors) + { + std::string relation = "a subtype of"; + if (reasoning.variance == SubtypingVariance::Invariant) + relation = "exactly"; + else if (reasoning.variance == SubtypingVariance::Contravariant) + relation = "a supertype of"; - std::string reason; - if (reasoning.subPath == reasoning.superPath) - reason = "at " + toString(reasoning.subPath) + ", " + toString(subLeaf) + " is not " + relation + " " + toString(superLeaf); + std::string subLeafAsString = toString(subLeaf); + // if the string is empty, it must be an empty type pack + if (subLeafAsString.empty()) + subLeafAsString = "()"; + + std::string superLeafAsString = toString(superLeaf); + // if the string is empty, it must be an empty type pack + if (superLeafAsString.empty()) + superLeafAsString = "()"; + + std::stringstream baseReasonBuilder; + baseReasonBuilder << "`" << subLeafAsString << "` is not " << relation << " `" << superLeafAsString << "`"; + std::string baseReason = baseReasonBuilder.str(); + + std::stringstream reason; + + if (reasoning.subPath == reasoning.superPath) + reason << toStringHuman(reasoning.subPath) << "`" << subLeafAsString << "` in the former type and `" << superLeafAsString + << "` in the latter type, and " << baseReason; + else if (!reasoning.subPath.empty() && !reasoning.superPath.empty()) + reason << toStringHuman(reasoning.subPath) << "`" << subLeafAsString << "` and " << toStringHuman(reasoning.superPath) << "`" + << superLeafAsString << "`, and " << baseReason; + else if (!reasoning.subPath.empty()) + reason << toStringHuman(reasoning.subPath) << "`" << subLeafAsString << "`, which is not " << relation << " `" << superLeafAsString + << "`"; + else + reason << toStringHuman(reasoning.superPath) << "`" << superLeafAsString << "`, and " << baseReason; + + reasons.push_back(reason.str()); + } else - reason = "type " + toString(subTy) + toString(reasoning.subPath, /* prefixDot */ true) + " (" + toString(subLeaf) + ") is not " + - relation + " " + toString(superTy) + toString(reasoning.superPath, /* prefixDot */ true) + " (" + toString(superLeaf) + ")"; + { + std::string relation = "a subtype of"; + if (reasoning.variance == SubtypingVariance::Invariant) + relation = "exactly"; + else if (reasoning.variance == SubtypingVariance::Contravariant) + relation = "a supertype of"; - reasons.push_back(reason); + std::string reason; + if (reasoning.subPath == reasoning.superPath) + reason = "at " + toString(reasoning.subPath) + ", " + toString(subLeaf) + " is not " + relation + " " + toString(superLeaf); + else + reason = "type " + toString(subTy) + toString(reasoning.subPath, /* prefixDot */ true) + " (" + toString(subLeaf) + ") is not " + + relation + " " + toString(superTy) + toString(reasoning.superPath, /* prefixDot */ true) + " (" + toString(superLeaf) + ")"; + + reasons.push_back(reason); + } // if we haven't already proved this isn't suppressing, we have to keep checking. if (suppressed) diff --git a/Analysis/src/TypeFunction.cpp b/Analysis/src/TypeFunction.cpp index 09648017..a3735e59 100644 --- a/Analysis/src/TypeFunction.cpp +++ b/Analysis/src/TypeFunction.cpp @@ -18,6 +18,7 @@ #include "Luau/ToString.h" #include "Luau/TxnLog.h" #include "Luau/Type.h" +#include "Luau/TypeChecker2.h" #include "Luau/TypeFunctionReductionGuesser.h" #include "Luau/TypeFunctionRuntime.h" #include "Luau/TypeFunctionRuntimeBuilder.h" @@ -49,12 +50,13 @@ LUAU_FASTFLAGVARIABLE(DebugLuauLogTypeFamilies) LUAU_FASTFLAG(DebugLuauEqSatSimplification) LUAU_FASTFLAGVARIABLE(LuauMetatableTypeFunctions) LUAU_FASTFLAGVARIABLE(LuauClipNestedAndRecursiveUnion) -LUAU_FASTFLAGVARIABLE(LuauDoNotGeneralizeInTypeFunctions) -LUAU_FASTFLAGVARIABLE(LuauPreventReentrantTypeFunctionReduction) +LUAU_FASTFLAGVARIABLE(LuauIndexTypeFunctionImprovements) +LUAU_FASTFLAGVARIABLE(LuauIndexTypeFunctionFunctionMetamethods) LUAU_FASTFLAGVARIABLE(LuauIntersectNotNil) LUAU_FASTFLAGVARIABLE(LuauSkipNoRefineDuringRefinement) LUAU_FASTFLAGVARIABLE(LuauDontForgetToReduceUnionFunc) LUAU_FASTFLAGVARIABLE(LuauSearchForRefineableType) +LUAU_FASTFLAGVARIABLE(LuauIndexAnyIsAny) namespace Luau { @@ -451,48 +453,29 @@ static FunctionGraphReductionResult reduceFunctionsInternal( TypeFunctionReducer reducer{std::move(queuedTys), std::move(queuedTps), std::move(shouldGuess), std::move(cyclics), location, ctx, force}; int iterationCount = 0; - if (FFlag::LuauPreventReentrantTypeFunctionReduction) + // If we are reducing a type function while reducing a type function, + // we're probably doing something clowny. One known place this can + // occur is type function reduction => overload selection => subtyping + // => back to type function reduction. At worst, if there's a reduction + // that _doesn't_ loop forever and _needs_ reentrancy, we'll fail to + // handle that and potentially emit an error when we didn't need to. + if (ctx.normalizer->sharedState->reentrantTypeReduction) + return {}; + + TypeReductionRentrancyGuard _{ctx.normalizer->sharedState}; + while (!reducer.done()) { - // If we are reducing a type function while reducing a type function, - // we're probably doing something clowny. One known place this can - // occur is type function reduction => overload selection => subtyping - // => back to type function reduction. At worst, if there's a reduction - // that _doesn't_ loop forever and _needs_ reentrancy, we'll fail to - // handle that and potentially emit an error when we didn't need to. - if (ctx.normalizer->sharedState->reentrantTypeReduction) - return {}; + reducer.step(); - TypeReductionRentrancyGuard _{ctx.normalizer->sharedState}; - while (!reducer.done()) + ++iterationCount; + if (iterationCount > DFInt::LuauTypeFamilyGraphReductionMaximumSteps) { - reducer.step(); - - ++iterationCount; - if (iterationCount > DFInt::LuauTypeFamilyGraphReductionMaximumSteps) - { - reducer.result.errors.emplace_back(location, CodeTooComplex{}); - break; - } + reducer.result.errors.emplace_back(location, CodeTooComplex{}); + break; } - - return std::move(reducer.result); } - else - { - while (!reducer.done()) - { - reducer.step(); - ++iterationCount; - if (iterationCount > DFInt::LuauTypeFamilyGraphReductionMaximumSteps) - { - reducer.result.errors.emplace_back(location, CodeTooComplex{}); - break; - } - } - - return std::move(reducer.result); - } + return std::move(reducer.result); } FunctionGraphReductionResult reduceTypeFunctions(TypeId entrypoint, Location location, TypeFunctionContext ctx, bool force) @@ -861,15 +844,6 @@ TypeFunctionReductionResult lenTypeFunction( if (isPending(operandTy, ctx->solver)) return {std::nullopt, Reduction::MaybeOk, {operandTy}, {}}; - // if the type is free but has only one remaining reference, we can generalize it to its upper bound here. - if (ctx->solver && !FFlag::LuauDoNotGeneralizeInTypeFunctions) - { - std::optional maybeGeneralized = ctx->solver->generalizeFreeType(ctx->scope, operandTy, /* avoidSealingTables */ true); - if (!maybeGeneralized) - return {std::nullopt, Reduction::MaybeOk, {operandTy}, {}}; - operandTy = *maybeGeneralized; - } - std::shared_ptr normTy = ctx->normalizer->normalize(operandTy); NormalizationResult inhabited = ctx->normalizer->isInhabited(normTy.get()); @@ -953,15 +927,6 @@ TypeFunctionReductionResult unmTypeFunction( if (isPending(operandTy, ctx->solver)) return {std::nullopt, Reduction::MaybeOk, {operandTy}, {}}; - // if the type is free but has only one remaining reference, we can generalize it to its upper bound here. - if (ctx->solver && !FFlag::LuauDoNotGeneralizeInTypeFunctions) - { - std::optional maybeGeneralized = ctx->solver->generalizeFreeType(ctx->scope, operandTy); - if (!maybeGeneralized) - return {std::nullopt, Reduction::MaybeOk, {operandTy}, {}}; - operandTy = *maybeGeneralized; - } - std::shared_ptr normTy = ctx->normalizer->normalize(operandTy); // if the operand failed to normalize, we can't reduce, but know nothing about inhabitance. @@ -1196,21 +1161,6 @@ TypeFunctionReductionResult numericBinopTypeFunction( else if (isPending(rhsTy, ctx->solver)) return {std::nullopt, Reduction::MaybeOk, {rhsTy}, {}}; - // if either type is free but has only one remaining reference, we can generalize it to its upper bound here. - if (ctx->solver && !FFlag::LuauDoNotGeneralizeInTypeFunctions) - { - std::optional lhsMaybeGeneralized = ctx->solver->generalizeFreeType(ctx->scope, lhsTy); - std::optional rhsMaybeGeneralized = ctx->solver->generalizeFreeType(ctx->scope, rhsTy); - - if (!lhsMaybeGeneralized) - return {std::nullopt, Reduction::MaybeOk, {lhsTy}, {}}; - else if (!rhsMaybeGeneralized) - return {std::nullopt, Reduction::MaybeOk, {rhsTy}, {}}; - - lhsTy = *lhsMaybeGeneralized; - rhsTy = *rhsMaybeGeneralized; - } - // TODO: Normalization needs to remove cyclic type functions from a `NormalizedType`. std::shared_ptr normLhsTy = ctx->normalizer->normalize(lhsTy); std::shared_ptr normRhsTy = ctx->normalizer->normalize(rhsTy); @@ -1433,21 +1383,6 @@ TypeFunctionReductionResult concatTypeFunction( else if (isPending(rhsTy, ctx->solver)) return {std::nullopt, Reduction::MaybeOk, {rhsTy}, {}}; - // if either type is free but has only one remaining reference, we can generalize it to its upper bound here. - if (ctx->solver && !FFlag::LuauDoNotGeneralizeInTypeFunctions) - { - std::optional lhsMaybeGeneralized = ctx->solver->generalizeFreeType(ctx->scope, lhsTy); - std::optional rhsMaybeGeneralized = ctx->solver->generalizeFreeType(ctx->scope, rhsTy); - - if (!lhsMaybeGeneralized) - return {std::nullopt, Reduction::MaybeOk, {lhsTy}, {}}; - else if (!rhsMaybeGeneralized) - return {std::nullopt, Reduction::MaybeOk, {rhsTy}, {}}; - - lhsTy = *lhsMaybeGeneralized; - rhsTy = *rhsMaybeGeneralized; - } - std::shared_ptr normLhsTy = ctx->normalizer->normalize(lhsTy); std::shared_ptr normRhsTy = ctx->normalizer->normalize(rhsTy); @@ -1548,21 +1483,6 @@ TypeFunctionReductionResult andTypeFunction( else if (isPending(rhsTy, ctx->solver)) return {std::nullopt, Reduction::MaybeOk, {rhsTy}, {}}; - // if either type is free but has only one remaining reference, we can generalize it to its upper bound here. - if (ctx->solver && !FFlag::LuauDoNotGeneralizeInTypeFunctions) - { - std::optional lhsMaybeGeneralized = ctx->solver->generalizeFreeType(ctx->scope, lhsTy); - std::optional rhsMaybeGeneralized = ctx->solver->generalizeFreeType(ctx->scope, rhsTy); - - if (!lhsMaybeGeneralized) - return {std::nullopt, Reduction::MaybeOk, {lhsTy}, {}}; - else if (!rhsMaybeGeneralized) - return {std::nullopt, Reduction::MaybeOk, {rhsTy}, {}}; - - lhsTy = *lhsMaybeGeneralized; - rhsTy = *rhsMaybeGeneralized; - } - // And evalutes to a boolean if the LHS is falsey, and the RHS type if LHS is truthy. SimplifyResult filteredLhs = simplifyIntersection(ctx->builtins, ctx->arena, lhsTy, ctx->builtins->falsyType); SimplifyResult overallResult = simplifyUnion(ctx->builtins, ctx->arena, rhsTy, filteredLhs.result); @@ -1603,21 +1523,6 @@ TypeFunctionReductionResult orTypeFunction( else if (isPending(rhsTy, ctx->solver)) return {std::nullopt, Reduction::MaybeOk, {rhsTy}, {}}; - // if either type is free but has only one remaining reference, we can generalize it to its upper bound here. - if (ctx->solver && !FFlag::LuauDoNotGeneralizeInTypeFunctions) - { - std::optional lhsMaybeGeneralized = ctx->solver->generalizeFreeType(ctx->scope, lhsTy); - std::optional rhsMaybeGeneralized = ctx->solver->generalizeFreeType(ctx->scope, rhsTy); - - if (!lhsMaybeGeneralized) - return {std::nullopt, Reduction::MaybeOk, {lhsTy}, {}}; - else if (!rhsMaybeGeneralized) - return {std::nullopt, Reduction::MaybeOk, {rhsTy}, {}}; - - lhsTy = *lhsMaybeGeneralized; - rhsTy = *rhsMaybeGeneralized; - } - // Or evalutes to the LHS type if the LHS is truthy, and the RHS type if LHS is falsy. SimplifyResult filteredLhs = simplifyIntersection(ctx->builtins, ctx->arena, lhsTy, ctx->builtins->truthyType); SimplifyResult overallResult = simplifyUnion(ctx->builtins, ctx->arena, rhsTy, filteredLhs.result); @@ -1689,21 +1594,6 @@ static TypeFunctionReductionResult comparisonTypeFunction( lhsTy = follow(lhsTy); rhsTy = follow(rhsTy); - // if either type is free but has only one remaining reference, we can generalize it to its upper bound here. - if (ctx->solver && !FFlag::LuauDoNotGeneralizeInTypeFunctions) - { - std::optional lhsMaybeGeneralized = ctx->solver->generalizeFreeType(ctx->scope, lhsTy); - std::optional rhsMaybeGeneralized = ctx->solver->generalizeFreeType(ctx->scope, rhsTy); - - if (!lhsMaybeGeneralized) - return {std::nullopt, Reduction::MaybeOk, {lhsTy}, {}}; - else if (!rhsMaybeGeneralized) - return {std::nullopt, Reduction::MaybeOk, {rhsTy}, {}}; - - lhsTy = *lhsMaybeGeneralized; - rhsTy = *rhsMaybeGeneralized; - } - // check to see if both operand types are resolved enough, and wait to reduce if not std::shared_ptr normLhsTy = ctx->normalizer->normalize(lhsTy); @@ -1827,21 +1717,6 @@ TypeFunctionReductionResult eqTypeFunction( else if (isPending(rhsTy, ctx->solver)) return {std::nullopt, Reduction::MaybeOk, {rhsTy}, {}}; - // if either type is free but has only one remaining reference, we can generalize it to its upper bound here. - if (ctx->solver && !FFlag::LuauDoNotGeneralizeInTypeFunctions) - { - std::optional lhsMaybeGeneralized = ctx->solver->generalizeFreeType(ctx->scope, lhsTy); - std::optional rhsMaybeGeneralized = ctx->solver->generalizeFreeType(ctx->scope, rhsTy); - - if (!lhsMaybeGeneralized) - return {std::nullopt, Reduction::MaybeOk, {lhsTy}, {}}; - else if (!rhsMaybeGeneralized) - return {std::nullopt, Reduction::MaybeOk, {rhsTy}, {}}; - - lhsTy = *lhsMaybeGeneralized; - rhsTy = *rhsMaybeGeneralized; - } - std::shared_ptr normLhsTy = ctx->normalizer->normalize(lhsTy); std::shared_ptr normRhsTy = ctx->normalizer->normalize(rhsTy); NormalizationResult lhsInhabited = ctx->normalizer->isInhabited(normLhsTy.get()); @@ -2000,20 +1875,6 @@ TypeFunctionReductionResult refineTypeFunction( auto stepRefine = [&ctx](TypeId target, TypeId discriminant) -> std::pair> { std::vector toBlock; - if (ctx->solver && !FFlag::LuauDoNotGeneralizeInTypeFunctions) - { - std::optional targetMaybeGeneralized = ctx->solver->generalizeFreeType(ctx->scope, target); - std::optional discriminantMaybeGeneralized = ctx->solver->generalizeFreeType(ctx->scope, discriminant); - - if (!targetMaybeGeneralized) - return std::pair>{nullptr, {target}}; - else if (!discriminantMaybeGeneralized) - return std::pair>{nullptr, {discriminant}}; - - target = *targetMaybeGeneralized; - discriminant = *discriminantMaybeGeneralized; - } - // we need a more complex check for blocking on the discriminant in particular FindRefinementBlockers frb; frb.traverse(discriminant); @@ -2131,15 +1992,6 @@ TypeFunctionReductionResult singletonTypeFunction( if (isPending(type, ctx->solver)) return {std::nullopt, Reduction::MaybeOk, {type}, {}}; - // if the type is free but has only one remaining reference, we can generalize it to its upper bound here. - if (ctx->solver && !FFlag::LuauDoNotGeneralizeInTypeFunctions) - { - std::optional maybeGeneralized = ctx->solver->generalizeFreeType(ctx->scope, type); - if (!maybeGeneralized) - return {std::nullopt, Reduction::MaybeOk, {type}, {}}; - type = *maybeGeneralized; - } - TypeId followed = type; // we want to follow through a negation here as well. if (auto negation = get(followed)) @@ -2207,73 +2059,24 @@ TypeFunctionReductionResult unionTypeFunction( if (typeParams.size() == 1) return {follow(typeParams[0]), Reduction::MaybeOk, {}, {}}; - if (FFlag::LuauClipNestedAndRecursiveUnion) + + CollectUnionTypeOptions collector{ctx}; + collector.traverse(instance); + + if (!collector.blockingTypes.empty()) { - - CollectUnionTypeOptions collector{ctx}; - collector.traverse(instance); - - if (!collector.blockingTypes.empty()) - { - std::vector blockingTypes{collector.blockingTypes.begin(), collector.blockingTypes.end()}; - return {std::nullopt, Reduction::MaybeOk, std::move(blockingTypes), {}}; - } - - TypeId resultTy = ctx->builtins->neverType; - for (auto ty : collector.options) - { - SimplifyResult result = simplifyUnion(ctx->builtins, ctx->arena, resultTy, ty); - // This condition might fire if one of the arguments to this type - // function is a free type somewhere deep in a nested union or - // intersection type, even though we ran a pass above to capture - // some blocked types. - if (!result.blockedTypes.empty()) - return {std::nullopt, Reduction::MaybeOk, {result.blockedTypes.begin(), result.blockedTypes.end()}, {}}; - - resultTy = result.result; - } - - return {resultTy, Reduction::MaybeOk, {}, {}}; + std::vector blockingTypes{collector.blockingTypes.begin(), collector.blockingTypes.end()}; + return {std::nullopt, Reduction::MaybeOk, std::move(blockingTypes), {}}; } - // we need to follow all of the type parameters. - std::vector types; - types.reserve(typeParams.size()); - for (auto ty : typeParams) - types.emplace_back(follow(ty)); - - // unfortunately, we need this short-circuit: if all but one type is `never`, we will return that one type. - // this also will early return if _everything_ is `never`, since we already have to check that. - std::optional lastType = std::nullopt; - for (auto ty : types) - { - // if we have a previous type and it's not `never` and the current type isn't `never`... - if (lastType && !get(lastType) && !get(ty)) - { - // we know we are not taking the short-circuited path. - lastType = std::nullopt; - break; - } - - if (get(ty)) - continue; - lastType = ty; - } - - // if we still have a `lastType` at the end, we're taking the short-circuit and reducing early. - if (lastType) - return {lastType, Reduction::MaybeOk, {}, {}}; - - // check to see if the operand types are resolved enough, and wait to reduce if not - for (auto ty : types) - if (isPending(ty, ctx->solver)) - return {std::nullopt, Reduction::MaybeOk, {ty}, {}}; - - // fold over the types with `simplifyUnion` TypeId resultTy = ctx->builtins->neverType; - for (auto ty : types) + for (auto ty : collector.options) { SimplifyResult result = simplifyUnion(ctx->builtins, ctx->arena, resultTy, ty); + // This condition might fire if one of the arguments to this type + // function is a free type somewhere deep in a nested union or + // intersection type, even though we ran a pass above to capture + // some blocked types. if (!result.blockedTypes.empty()) return {std::nullopt, Reduction::MaybeOk, {result.blockedTypes.begin(), result.blockedTypes.end()}, {}}; @@ -2281,6 +2084,7 @@ TypeFunctionReductionResult unionTypeFunction( } return {resultTy, Reduction::MaybeOk, {}, {}}; + } @@ -2621,7 +2425,12 @@ bool searchPropsAndIndexer( if (auto propUnionTy = get(propTy)) { for (TypeId option : propUnionTy->options) - result.insert(option); + { + if (FFlag::LuauIndexTypeFunctionImprovements) + result.insert(follow(option)); + else + result.insert(option); + } } else // property is a singular type or intersection type -> we can simply append result.insert(propTy); @@ -2641,7 +2450,12 @@ bool searchPropsAndIndexer( if (auto idxResUnionTy = get(idxResultTy)) { for (TypeId option : idxResUnionTy->options) - result.insert(option); + { + if (FFlag::LuauIndexTypeFunctionImprovements) + result.insert(follow(option)); + else + result.insert(option); + } } else // indexResultType is a singular type or intersection type -> we can simply append result.insert(idxResultTy); @@ -2656,7 +2470,7 @@ bool searchPropsAndIndexer( /* Handles recursion / metamethods of tables/classes `isRaw` parameter indicates whether or not we should follow __index metamethods returns false if property of `ty` could not be found */ -bool tblIndexInto(TypeId indexer, TypeId indexee, DenseHashSet& result, NotNull ctx, bool isRaw) +bool tblIndexInto_DEPRECATED(TypeId indexer, TypeId indexee, DenseHashSet& result, NotNull ctx, bool isRaw) { indexer = follow(indexer); indexee = follow(indexee); @@ -2686,13 +2500,113 @@ bool tblIndexInto(TypeId indexer, TypeId indexee, DenseHashSet& result, ErrorVec dummy; std::optional mmType = findMetatableEntry(ctx->builtins, dummy, indexee, "__index", Location{}); if (mmType) - return tblIndexInto(indexer, *mmType, result, ctx, isRaw); + return tblIndexInto_DEPRECATED(indexer, *mmType, result, ctx, isRaw); } } return false; } +bool tblIndexInto(TypeId indexer, TypeId indexee, DenseHashSet& result, DenseHashSet& seenSet, NotNull ctx, bool isRaw) +{ + indexer = follow(indexer); + indexee = follow(indexee); + + if (seenSet.contains(indexee)) + return false; + seenSet.insert(indexee); + + if (FFlag::LuauIndexTypeFunctionFunctionMetamethods) + { + if (auto unionTy = get(indexee)) + { + bool res = true; + for (auto component : unionTy) + { + // if the component is in the seen set and isn't the indexee itself, + // we can skip it cause it means we encountered it in an earlier component in the union. + if (seenSet.contains(component) && component != indexee) + continue; + + res = res && tblIndexInto(indexer, component, result, seenSet, ctx, isRaw); + } + return res; + } + + if (get(indexee)) + { + TypePackId argPack = ctx->arena->addTypePack({indexer}); + SolveResult solveResult = solveFunctionCall( + ctx->arena, + ctx->builtins, + ctx->simplifier, + ctx->normalizer, + ctx->typeFunctionRuntime, + ctx->ice, + ctx->limits, + ctx->scope, + ctx->scope->location, + indexee, + argPack + ); + + if (!solveResult.typePackId.has_value()) + return false; + + TypePack extracted = extendTypePack(*ctx->arena, ctx->builtins, *solveResult.typePackId, 1); + if (extracted.head.empty()) + return false; + + result.insert(follow(extracted.head.front())); + return true; + } + } + + // we have a table type to try indexing + if (auto tableTy = get(indexee)) + { + return searchPropsAndIndexer(indexer, tableTy->props, tableTy->indexer, result, ctx); + } + + // we have a metatable type to try indexing + if (auto metatableTy = get(indexee)) + { + if (auto tableTy = get(follow(metatableTy->table))) + { + + // try finding all properties within the current scope of the table + if (searchPropsAndIndexer(indexer, tableTy->props, tableTy->indexer, result, ctx)) + return true; + } + + // if the code reached here, it means we weren't able to find all properties -> look into __index metamethod + if (!isRaw) + { + // findMetatableEntry demands the ability to emit errors, so we must give it + // the necessary state to do that, even if we intend to just eat the errors. + ErrorVec dummy; + std::optional mmType = findMetatableEntry(ctx->builtins, dummy, indexee, "__index", Location{}); + if (mmType) + return tblIndexInto(indexer, *mmType, result, seenSet, ctx, isRaw); + } + } + + return false; +} + +bool tblIndexInto(TypeId indexer, TypeId indexee, DenseHashSet& result, NotNull ctx, bool isRaw) +{ + if (FFlag::LuauIndexTypeFunctionImprovements) + { + DenseHashSet seenSet{{}}; + return tblIndexInto(indexer, indexee, result, seenSet, ctx, isRaw); + } + else + { + return tblIndexInto_DEPRECATED(indexer, indexee, result, ctx, isRaw); + } +} + /* Vocabulary note: indexee refers to the type that contains the properties, indexer refers to the type that is used to access indexee Example: index => `Person` is the indexee and `"name"` is the indexer */ @@ -2710,6 +2624,13 @@ TypeFunctionReductionResult indexFunctionImpl( if (!indexeeNormTy) return {std::nullopt, Reduction::MaybeOk, {}, {}}; + if (FFlag::LuauIndexAnyIsAny) + { + // if the indexee is `any`, then indexing also gives us `any`. + if (indexeeNormTy->shouldSuppressErrors()) + return {ctx->builtins->anyType, Reduction::MaybeOk, {}, {}}; + } + // if we don't have either just tables or just classes, we've got nothing to index into if (indexeeNormTy->hasTables() == indexeeNormTy->hasClasses()) return {std::nullopt, Reduction::Erroneous, {}, {}}; @@ -2809,17 +2730,19 @@ TypeFunctionReductionResult indexFunctionImpl( } } - // Call `follow()` on each element to resolve all Bound types before returning - std::transform( - properties.begin(), - properties.end(), - properties.begin(), - [](TypeId ty) - { - return follow(ty); - } + if (!FFlag::LuauIndexTypeFunctionImprovements) + { + // Call `follow()` on each element to resolve all Bound types before returning + std::transform( + properties.begin(), + properties.end(), + properties.begin(), + [](TypeId ty) + { + return follow(ty); + } ); - +} // If the type being reduced to is a single type, no need to union if (properties.size() == 1) return {*properties.begin(), Reduction::MaybeOk, {}, {}}; diff --git a/Analysis/src/TypeFunctionRuntime.cpp b/Analysis/src/TypeFunctionRuntime.cpp index fb33560e..b4c1f915 100644 --- a/Analysis/src/TypeFunctionRuntime.cpp +++ b/Analysis/src/TypeFunctionRuntime.cpp @@ -13,10 +13,7 @@ #include #include -LUAU_FASTFLAGVARIABLE(LuauTypeFunFixHydratedClasses) LUAU_DYNAMIC_FASTINT(LuauTypeFunctionSerdeIterationLimit) -LUAU_FASTFLAGVARIABLE(LuauTypeFunSingletonEquality) -LUAU_FASTFLAGVARIABLE(LuauUserTypeFunTypeofReturnsType) LUAU_FASTFLAGVARIABLE(LuauTypeFunPrintFix) LUAU_FASTFLAGVARIABLE(LuauTypeFunReadWriteParents) @@ -1617,11 +1614,8 @@ void registerTypeUserData(lua_State* L) // Create and register metatable for type userdata luaL_newmetatable(L, "type"); - if (FFlag::LuauUserTypeFunTypeofReturnsType) - { - lua_pushstring(L, "type"); - lua_setfield(L, -2, "__type"); - } + lua_pushstring(L, "type"); + lua_setfield(L, -2, "__type"); // Protect metatable from being changed lua_pushstring(L, "The metatable is locked"); @@ -1758,14 +1752,14 @@ bool areEqual(SeenSet& seen, const TypeFunctionSingletonType& lhs, const TypeFun { const TypeFunctionBooleanSingleton* lp = get(&lhs); - const TypeFunctionBooleanSingleton* rp = get(FFlag::LuauTypeFunSingletonEquality ? &rhs : &lhs); + const TypeFunctionBooleanSingleton* rp = get(&rhs); if (lp && rp) return lp->value == rp->value; } { const TypeFunctionStringSingleton* lp = get(&lhs); - const TypeFunctionStringSingleton* rp = get(FFlag::LuauTypeFunSingletonEquality ? &rhs : &lhs); + const TypeFunctionStringSingleton* rp = get(&rhs); if (lp && rp) return lp->value == rp->value; } @@ -1918,10 +1912,7 @@ bool areEqual(SeenSet& seen, const TypeFunctionClassType& lhs, const TypeFunctio if (seenSetContains(seen, &lhs, &rhs)) return true; - if (FFlag::LuauTypeFunFixHydratedClasses) - return lhs.classTy == rhs.classTy; - else - return lhs.name_DEPRECATED == rhs.name_DEPRECATED; + return lhs.classTy == rhs.classTy; } bool areEqual(SeenSet& seen, const TypeFunctionType& lhs, const TypeFunctionType& rhs) diff --git a/Analysis/src/TypeFunctionRuntimeBuilder.cpp b/Analysis/src/TypeFunctionRuntimeBuilder.cpp index 8a8779b2..3aa6f70c 100644 --- a/Analysis/src/TypeFunctionRuntimeBuilder.cpp +++ b/Analysis/src/TypeFunctionRuntimeBuilder.cpp @@ -19,7 +19,6 @@ // used to control the recursion limit of any operations done by user-defined type functions // currently, controls serialization, deserialization, and `type.copy` LUAU_DYNAMIC_FASTINTVARIABLE(LuauTypeFunctionSerdeIterationLimit, 100'000); -LUAU_FASTFLAG(LuauTypeFunFixHydratedClasses) LUAU_FASTFLAG(LuauTypeFunReadWriteParents) namespace Luau @@ -209,19 +208,11 @@ private: } else if (auto c = get(ty)) { - if (FFlag::LuauTypeFunFixHydratedClasses) - { - // 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 - target = typeFunctionRuntime->typeArena.allocate(TypeFunctionClassType{{}, std::nullopt, std::nullopt, std::nullopt, std::nullopt, std::nullopt, ty}); - } - else - { - state->classesSerialized_DEPRECATED[c->name] = ty; - target = typeFunctionRuntime->typeArena.allocate( - TypeFunctionClassType{{}, std::nullopt, std::nullopt, std::nullopt, std::nullopt, std::nullopt, /* classTy */ nullptr, c->name} - ); - } + // 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 + target = typeFunctionRuntime->typeArena.allocate( + TypeFunctionClassType{{}, std::nullopt, std::nullopt, std::nullopt, std::nullopt, std::nullopt, ty} + ); } else if (auto g = get(ty)) { @@ -713,17 +704,7 @@ private: } else if (auto c = get(ty)) { - if (FFlag::LuauTypeFunFixHydratedClasses) - { - target = c->classTy; - } - else - { - if (auto result = state->classesSerialized_DEPRECATED.find(c->name_DEPRECATED)) - target = *result; - else - state->ctx->ice->ice("Deserializing user defined type function arguments: mysterious class type is being deserialized"); - } + target = c->classTy; } else if (auto g = get(ty)) { diff --git a/Analysis/src/TypeInfer.cpp b/Analysis/src/TypeInfer.cpp index 832e433e..d92f28a0 100644 --- a/Analysis/src/TypeInfer.cpp +++ b/Analysis/src/TypeInfer.cpp @@ -5721,6 +5721,10 @@ TypeId TypeChecker::resolveTypeWorker(const ScopePtr& scope, const AstType& anno TypeId ty = checkExpr(scope, *typeOf->expr).type; return ty; } + else if (annotation.is()) + { + return builtinTypes->nilType; + } else if (const auto& un = annotation.as()) { if (FFlag::LuauPreserveUnionIntersectionNodeForLeadingTokenSingleType) diff --git a/Analysis/src/TypePath.cpp b/Analysis/src/TypePath.cpp index baf7bb11..32cc3f57 100644 --- a/Analysis/src/TypePath.cpp +++ b/Analysis/src/TypePath.cpp @@ -14,7 +14,8 @@ #include LUAU_FASTFLAG(LuauSolverV2); -LUAU_FASTFLAGVARIABLE(LuauDisableNewSolverAssertsInMixedMode); +LUAU_FASTFLAGVARIABLE(LuauDisableNewSolverAssertsInMixedMode) + // 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 // logic. @@ -638,6 +639,247 @@ std::string toString(const TypePath::Path& path, bool prefixDot) return result.str(); } +std::string toStringHuman(const TypePath::Path& path) +{ + LUAU_ASSERT(FFlag::LuauSolverV2); + + enum class State + { + Initial, + Normal, + Property, + PendingIs, + PendingAs, + PendingWhich, + }; + + std::stringstream result; + State state = State::Initial; + bool last = false; + + auto strComponent = [&](auto&& c) + { + using T = std::decay_t; + if constexpr (std::is_same_v) + { + if (state == State::PendingIs) + result << ", "; + + switch (state) + { + case State::Initial: + case State::PendingIs: + if (c.isRead) + result << "accessing `"; + else + result << "writing to `"; + break; + case State::Property: + // if the previous state was a property, then we're doing a sequence of indexing + result << '.'; + break; + default: + break; + } + + result << c.name; + + state = State::Property; + } + 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" << ' '; + + 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) + { + case TypePath::Index::Variant::Pack: + result << ' ' << "entry in the type pack"; + break; + case TypePath::Index::Variant::Union: + result << ' ' << "component of the union"; + break; + case TypePath::Index::Variant::Intersection: + result << ' ' << "component of the intersection"; + break; + } + + if (state == State::PendingWhich) + result << ' ' << "which"; + + if (state == State::PendingIs || state == State::Property) + state = State::PendingAs; + else + state = State::PendingIs; + } + else if constexpr (std::is_same_v) + { + if (state == State::Initial && !last) + result << "in" << ' '; + else if (state == State::PendingIs) + result << ", "; + else if (state == State::Property) + result << '`' << ' ' << "has" << ' '; + + switch (c) + { + case TypePath::TypeField::Table: + result << "the table portion"; + if (state == State::Property) + state = State::PendingAs; + else + state = State::PendingIs; + break; + case TypePath::TypeField::Metatable: + result << "the metatable portion"; + if (state == State::Property) + state = State::PendingAs; + else + state = State::PendingIs; + break; + case TypePath::TypeField::LowerBound: + result << "the lower bound of" << ' '; + state = State::Normal; + break; + case TypePath::TypeField::UpperBound: + result << "the upper bound of" << ' '; + state = State::Normal; + break; + case TypePath::TypeField::IndexLookup: + result << "the index type"; + if (state == State::Property) + state = State::PendingAs; + else + state = State::PendingIs; + break; + case TypePath::TypeField::IndexResult: + result << "the result of indexing"; + if (state == State::Property) + state = State::PendingAs; + else + state = State::PendingIs; + break; + case TypePath::TypeField::Negated: + result << "the negation" << ' '; + state = State::Normal; + break; + case TypePath::TypeField::Variadic: + result << "the variadic" << ' '; + state = State::Normal; + break; + } + } + else if constexpr (std::is_same_v) + { + if (state == State::PendingIs) + result << ", "; + else if (state == State::Property) + result << "`, "; + + switch (c) + { + case TypePath::PackField::Arguments: + if (state == State::Initial) + result << "it" << ' '; + else if (state == State::PendingIs) + result << "the function" << ' '; + + result << "takes"; + break; + case TypePath::PackField::Returns: + if (state == State::Initial) + result << "it" << ' '; + else if (state == State::PendingIs) + result << "the function" << ' '; + + result << "returns"; + break; + case TypePath::PackField::Tail: + if (state == State::Initial) + result << "it has" << ' '; + result << "a tail of"; + break; + } + + if (state == State::PendingIs) + { + result << ' '; + state = State::PendingWhich; + } + else + { + result << ' '; + state = State::Normal; + } + } + else if constexpr (std::is_same_v) + { + if (state == State::Initial) + result << "it" << ' '; + result << "reduces to" << ' '; + state = State::Normal; + } + else + { + static_assert(always_false_v, "Unhandled Component variant"); + } + }; + + size_t count = 0; + + for (const TypePath::Component& component : path.components) + { + count++; + if (count == path.components.size()) + last = true; + + Luau::visit(strComponent, component); + } + + switch (state) + { + case State::Property: + result << "` results in "; + break; + case State::PendingWhich: + // pending `which` becomes `is` if it's at the end + result << "is" << ' '; + break; + case State::PendingIs: + result << ' ' << "is" << ' '; + break; + case State::PendingAs: + result << ' ' << "as" << ' '; + break; + default: + break; + } + + return result.str(); +} + static bool traverse(TraversalState& state, const Path& path) { auto step = [&state](auto&& c) diff --git a/Analysis/src/Unifier2.cpp b/Analysis/src/Unifier2.cpp index a224c4bc..18c570be 100644 --- a/Analysis/src/Unifier2.cpp +++ b/Analysis/src/Unifier2.cpp @@ -677,7 +677,7 @@ struct FreeTypeSearcher : TypeVisitor DenseHashSet seenPositive{nullptr}; DenseHashSet seenNegative{nullptr}; - bool seenWithPolarity(const void* ty) + bool seenWithCurrentPolarity(const void* ty) { switch (polarity) { @@ -719,7 +719,7 @@ struct FreeTypeSearcher : TypeVisitor bool visit(TypeId ty) override { - if (seenWithPolarity(ty)) + if (seenWithCurrentPolarity(ty)) return false; LUAU_ASSERT(ty); @@ -728,7 +728,7 @@ struct FreeTypeSearcher : TypeVisitor bool visit(TypeId ty, const FreeType& ft) override { - if (seenWithPolarity(ty)) + if (seenWithCurrentPolarity(ty)) return false; if (!subsumes(scope, ft.scope)) @@ -753,7 +753,7 @@ struct FreeTypeSearcher : TypeVisitor bool visit(TypeId ty, const TableType& tt) override { - if (seenWithPolarity(ty)) + if (seenWithCurrentPolarity(ty)) return false; if ((tt.state == TableState::Free || tt.state == TableState::Unsealed) && subsumes(scope, tt.scope)) @@ -799,7 +799,7 @@ struct FreeTypeSearcher : TypeVisitor bool visit(TypeId ty, const FunctionType& ft) override { - if (seenWithPolarity(ty)) + if (seenWithCurrentPolarity(ty)) return false; flip(); @@ -818,7 +818,7 @@ struct FreeTypeSearcher : TypeVisitor bool visit(TypePackId tp, const FreeTypePack& ftp) override { - if (seenWithPolarity(tp)) + if (seenWithCurrentPolarity(tp)) return false; if (!subsumes(scope, ftp.scope)) diff --git a/Ast/include/Luau/Ast.h b/Ast/include/Luau/Ast.h index 34f0072e..4d4e1280 100644 --- a/Ast/include/Luau/Ast.h +++ b/Ast/include/Luau/Ast.h @@ -1127,6 +1127,16 @@ public: AstExpr* expr; }; +class AstTypeOptional : public AstType +{ +public: + LUAU_RTTI(AstTypeOptional) + + AstTypeOptional(const Location& location); + + void visit(AstVisitor* visitor) override; +}; + class AstTypeUnion : public AstType { public: @@ -1488,6 +1498,10 @@ public: { return visit(static_cast(node)); } + virtual bool visit(class AstTypeOptional* node) + { + return visit(static_cast(node)); + } virtual bool visit(class AstTypeUnion* node) { return visit(static_cast(node)); diff --git a/Ast/include/Luau/Cst.h b/Ast/include/Luau/Cst.h index 95211f14..8c6cf34c 100644 --- a/Ast/include/Luau/Cst.h +++ b/Ast/include/Luau/Cst.h @@ -105,6 +105,20 @@ public: Position closeBracketPosition; }; +class CstExprFunction : public CstNode +{ +public: + LUAU_CST_RTTI(CstExprFunction) + + CstExprFunction(); + + Position openGenericsPosition{0,0}; + AstArray genericsCommaPositions; + Position closeGenericsPosition{0,0}; + AstArray argsCommaPositions; + Position returnSpecifierPosition{0,0}; +}; + class CstExprTable : public CstNode { public: @@ -311,6 +325,17 @@ public: Position equalsPosition; }; +class CstStatTypeFunction : public CstNode +{ +public: + LUAU_CST_RTTI(CstStatTypeFunction) + + CstStatTypeFunction(Position typeKeywordPosition, Position functionKeywordPosition); + + Position typeKeywordPosition; + Position functionKeywordPosition; +}; + class CstTypeReference : public CstNode { public: @@ -359,6 +384,32 @@ public: bool isArray = false; }; +class CstTypeFunction : public CstNode +{ +public: + LUAU_CST_RTTI(CstTypeFunction) + + CstTypeFunction( + Position openGenericsPosition, + AstArray genericsCommaPositions, + Position closeGenericsPosition, + Position openArgsPosition, + AstArray> argumentNameColonPositions, + AstArray argumentsCommaPositions, + Position closeArgsPosition, + Position returnArrowPosition + ); + + Position openGenericsPosition; + AstArray genericsCommaPositions; + Position closeGenericsPosition; + Position openArgsPosition; + AstArray> argumentNameColonPositions; + AstArray argumentsCommaPositions; + Position closeArgsPosition; + Position returnArrowPosition; +}; + class CstTypeTypeof : public CstNode { public: @@ -382,4 +433,26 @@ public: unsigned int blockDepth; }; +class CstTypePackExplicit : public CstNode +{ +public: + LUAU_CST_RTTI(CstTypePackExplicit) + + CstTypePackExplicit(Position openParenthesesPosition, Position closeParenthesesPosition, AstArray commaPositions); + + Position openParenthesesPosition; + Position closeParenthesesPosition; + AstArray commaPositions; +}; + +class CstTypePackGeneric : public CstNode +{ +public: + LUAU_CST_RTTI(CstTypePackGeneric) + + explicit CstTypePackGeneric(Position ellipsisPosition); + + Position ellipsisPosition; +}; + } // namespace Luau \ No newline at end of file diff --git a/Ast/include/Luau/Parser.h b/Ast/include/Luau/Parser.h index cfe7d08c..22137832 100644 --- a/Ast/include/Luau/Parser.h +++ b/Ast/include/Luau/Parser.h @@ -155,7 +155,7 @@ private: AstStat* parseTypeAlias(const Location& start, bool exported, Position typeKeywordPosition); // type function Name ... end - AstStat* parseTypeFunction(const Location& start, bool exported); + AstStat* parseTypeFunction(const Location& start, bool exported, Position typeKeywordPosition); AstDeclaredClassProp parseDeclaredClassMethod(); @@ -192,7 +192,8 @@ private: std::tuple parseBindingList( TempVector& result, bool allowDot3 = false, - TempVector* commaPositions = nullptr + AstArray* commaPositions = nullptr, + std::optional initialCommaPosition = std::nullopt ); AstType* parseOptionalType(); @@ -209,9 +210,14 @@ private: // | `(' [TypeList] `)' `->` ReturnType // Returns the variadic annotation, if it exists. - AstTypePack* parseTypeList(TempVector& result, TempVector>& resultNames); + AstTypePack* parseTypeList( + TempVector& result, + TempVector>& resultNames, + TempVector* commaPositions = nullptr, + TempVector>* nameColonPositions = nullptr + ); - std::optional parseOptionalReturnType(); + std::optional parseOptionalReturnType(Position* returnSpecifierPosition = nullptr); std::pair parseReturnType(); struct TableIndexerResult @@ -305,7 +311,7 @@ private: std::pair, AstArray> parseGenericTypeList( bool withDefaultValues, Position* openPosition = nullptr, - TempVector* commaPositions = nullptr, + AstArray* commaPositions = nullptr, Position* closePosition = nullptr ); @@ -491,6 +497,7 @@ private: std::vector scratchGenericTypePacks; std::vector> scratchOptArgName; std::vector scratchPosition; + std::vector> scratchOptPosition; std::string scratchData; CstNodeMap cstNodeMap; diff --git a/Ast/src/Ast.cpp b/Ast/src/Ast.cpp index ab42ec8c..dd0779bb 100644 --- a/Ast/src/Ast.cpp +++ b/Ast/src/Ast.cpp @@ -1069,6 +1069,16 @@ void AstTypeTypeof::visit(AstVisitor* visitor) expr->visit(visitor); } +AstTypeOptional::AstTypeOptional(const Location& location) + : AstType(ClassIndex(), location) +{ +} + +void AstTypeOptional::visit(AstVisitor* visitor) +{ + visitor->visit(this); +} + AstTypeUnion::AstTypeUnion(const Location& location, const AstArray& types) : AstType(ClassIndex(), location) , types(types) diff --git a/Ast/src/Cst.cpp b/Ast/src/Cst.cpp index 0d1b8352..7ed73c5f 100644 --- a/Ast/src/Cst.cpp +++ b/Ast/src/Cst.cpp @@ -38,6 +38,10 @@ CstExprIndexExpr::CstExprIndexExpr(Position openBracketPosition, Position closeB { } +CstExprFunction::CstExprFunction() : CstNode(CstClassIndex()) +{ +} + CstExprTable::CstExprTable(const AstArray& items) : CstNode(CstClassIndex()) , items(items) @@ -160,6 +164,13 @@ CstStatTypeAlias::CstStatTypeAlias( { } +CstStatTypeFunction::CstStatTypeFunction(Position typeKeywordPosition, Position functionKeywordPosition) + : CstNode(CstClassIndex()) + , typeKeywordPosition(typeKeywordPosition) + , functionKeywordPosition(functionKeywordPosition) +{ +} + CstTypeReference::CstTypeReference( std::optional prefixPointPosition, Position openParametersPosition, @@ -181,6 +192,28 @@ CstTypeTable::CstTypeTable(AstArray items, bool isArray) { } +CstTypeFunction::CstTypeFunction( + Position openGenericsPosition, + AstArray genericsCommaPositions, + Position closeGenericsPosition, + Position openArgsPosition, + AstArray> argumentNameColonPositions, + AstArray argumentsCommaPositions, + Position closeArgsPosition, + Position returnArrowPosition +) + : CstNode(CstClassIndex()) + , openGenericsPosition(openGenericsPosition) + , genericsCommaPositions(genericsCommaPositions) + , closeGenericsPosition(closeGenericsPosition) + , openArgsPosition(openArgsPosition) + , argumentNameColonPositions(argumentNameColonPositions) + , argumentsCommaPositions(argumentsCommaPositions) + , closeArgsPosition(closeArgsPosition) + , returnArrowPosition(returnArrowPosition) +{ +} + CstTypeTypeof::CstTypeTypeof(Position openPosition, Position closePosition) : CstNode(CstClassIndex()) , openPosition(openPosition) @@ -197,4 +230,18 @@ CstTypeSingletonString::CstTypeSingletonString(AstArray sourceString, CstE LUAU_ASSERT(quoteStyle != CstExprConstantString::QuotedInterp); } +CstTypePackExplicit::CstTypePackExplicit(Position openParenthesesPosition, Position closeParenthesesPosition, AstArray commaPositions) + : CstNode(CstClassIndex()) + , openParenthesesPosition(openParenthesesPosition) + , closeParenthesesPosition(closeParenthesesPosition) + , commaPositions(commaPositions) +{ +} + +CstTypePackGeneric::CstTypePackGeneric(Position ellipsisPosition) + : CstNode(CstClassIndex()) + , ellipsisPosition(ellipsisPosition) +{ +} + } // namespace Luau diff --git a/Ast/src/Lexer.cpp b/Ast/src/Lexer.cpp index e2760807..c2743640 100644 --- a/Ast/src/Lexer.cpp +++ b/Ast/src/Lexer.cpp @@ -8,8 +8,6 @@ #include -LUAU_FASTFLAGVARIABLE(LexerFixInterpStringStart) - namespace Luau { @@ -789,7 +787,7 @@ Lexeme Lexer::readNext() return Lexeme(Location(start, 1), '}'); } - return readInterpolatedStringSection(FFlag::LexerFixInterpStringStart ? start : position(), Lexeme::InterpStringMid, Lexeme::InterpStringEnd); + return readInterpolatedStringSection(start, Lexeme::InterpStringMid, Lexeme::InterpStringEnd); } case '=': diff --git a/Ast/src/Parser.cpp b/Ast/src/Parser.cpp index 65c0b24b..474efd6a 100644 --- a/Ast/src/Parser.cpp +++ b/Ast/src/Parser.cpp @@ -27,6 +27,7 @@ LUAU_FASTFLAGVARIABLE(LuauPreserveUnionIntersectionNodeForLeadingTokenSingleType LUAU_FASTFLAGVARIABLE(LuauAstTypeGroup3) LUAU_FASTFLAGVARIABLE(ParserNoErrorLimit) LUAU_FASTFLAGVARIABLE(LuauFixDoBlockEndLocation) +LUAU_FASTFLAGVARIABLE(LuauParseOptionalAsNode) namespace Luau { @@ -647,16 +648,16 @@ AstStat* Parser::parseFor() else { TempVector names(scratchBinding); - TempVector varsCommaPosition(scratchPosition); + AstArray varsCommaPosition; names.push_back(varname); if (lexer.current().type == ',') { if (FFlag::LuauStoreCSTData && options.storeCstData) { - varsCommaPosition.push_back(lexer.current().location.begin); + Position initialCommaPosition = lexer.current().location.begin; nextLexeme(); - parseBindingList(names, false, &varsCommaPosition); + parseBindingList(names, false, &varsCommaPosition, initialCommaPosition); } else { @@ -701,7 +702,7 @@ AstStat* Parser::parseFor() AstStatForIn* node = allocator.alloc(Location(start, end), copy(vars), copy(values), body, hasIn, inLocation, hasDo, matchDo.location); if (options.storeCstData) - cstNodeMap[node] = allocator.alloc(copy(varsCommaPosition), copy(valuesCommaPositions)); + cstNodeMap[node] = allocator.alloc(varsCommaPosition, copy(valuesCommaPositions)); return node; } else @@ -957,7 +958,7 @@ AstStat* Parser::parseLocal(const AstArray& attributes) matchRecoveryStopOnToken['=']++; TempVector names(scratchBinding); - TempVector varsCommaPositions(scratchPosition); + AstArray varsCommaPositions; if (FFlag::LuauStoreCSTData && options.storeCstData) parseBindingList(names, false, &varsCommaPositions); else @@ -990,7 +991,7 @@ AstStat* Parser::parseLocal(const AstArray& attributes) { AstStatLocal* node = allocator.alloc(Location(start, end), copy(vars), copy(values), equalsSignLocation); if (options.storeCstData) - cstNodeMap[node] = allocator.alloc(copy(varsCommaPositions), copy(valuesCommaPositions)); + cstNodeMap[node] = allocator.alloc(varsCommaPositions, copy(valuesCommaPositions)); return node; } else @@ -1033,7 +1034,7 @@ AstStat* Parser::parseTypeAlias(const Location& start, bool exported, Position t { // parsing a type function if (lexer.current().type == Lexeme::ReservedFunction) - return parseTypeFunction(start, exported); + return parseTypeFunction(start, exported, typeKeywordPosition); // parsing a type alias @@ -1046,7 +1047,7 @@ AstStat* Parser::parseTypeAlias(const Location& start, bool exported, Position t name = Name(nameError, lexer.current().location); Position genericsOpenPosition{0, 0}; - TempVector genericsCommaPositions(scratchPosition); + AstArray genericsCommaPositions; Position genericsClosePosition{0, 0}; auto [generics, genericPacks] = FFlag::LuauStoreCSTData && options.storeCstData ? parseGenericTypeList( @@ -1065,7 +1066,7 @@ AstStat* Parser::parseTypeAlias(const Location& start, bool exported, Position t allocator.alloc(Location(start, type->location), name->name, name->location, generics, genericPacks, type, exported); if (options.storeCstData) cstNodeMap[node] = allocator.alloc( - typeKeywordPosition, genericsOpenPosition, copy(genericsCommaPositions), genericsClosePosition, equalsPosition + typeKeywordPosition, genericsOpenPosition, genericsCommaPositions, genericsClosePosition, equalsPosition ); return node; } @@ -1076,7 +1077,7 @@ AstStat* Parser::parseTypeAlias(const Location& start, bool exported, Position t } // type function Name `(' arglist `)' `=' funcbody `end' -AstStat* Parser::parseTypeFunction(const Location& start, bool exported) +AstStat* Parser::parseTypeFunction(const Location& start, bool exported, Position typeKeywordPosition) { Lexeme matchFn = lexer.current(); nextLexeme(); @@ -1097,7 +1098,18 @@ AstStat* Parser::parseTypeFunction(const Location& start, bool exported) matchRecoveryStopOnToken[Lexeme::ReservedEnd]--; - return allocator.alloc(Location(start, body->location), fnName->name, fnName->location, body, exported); + if (FFlag::LuauStoreCSTData) + { + AstStatTypeFunction* node = + allocator.alloc(Location(start, body->location), fnName->name, fnName->location, body, exported); + if (options.storeCstData) + cstNodeMap[node] = allocator.alloc(typeKeywordPosition, matchFn.location.begin); + return node; + } + else + { + return allocator.alloc(Location(start, body->location), fnName->name, fnName->location, body, exported); + } } AstDeclaredClassProp Parser::parseDeclaredClassMethod() @@ -1439,7 +1451,15 @@ std::pair Parser::parseFunctionBody( { Location start = matchFunction.location; - auto [generics, genericPacks] = parseGenericTypeList(/* withDefaultValues= */ false); + auto* cstNode = FFlag::LuauStoreCSTData && options.storeCstData ? allocator.alloc() : nullptr; + + auto [generics, genericPacks] = FFlag::LuauStoreCSTData && cstNode ? parseGenericTypeList( + /* withDefaultValues= */ false, + &cstNode->openGenericsPosition, + &cstNode->genericsCommaPositions, + &cstNode->closeGenericsPosition + ) + : parseGenericTypeList(/* withDefaultValues= */ false); MatchLexeme matchParen = lexer.current(); expectAndConsume('(', "function"); @@ -1464,7 +1484,8 @@ std::pair Parser::parseFunctionBody( AstTypePack* varargAnnotation = nullptr; if (lexer.current().type != ')') - std::tie(vararg, varargLocation, varargAnnotation) = parseBindingList(args, /* allowDot3= */ true); + std::tie(vararg, varargLocation, varargAnnotation) = + parseBindingList(args, /* allowDot3= */ true, cstNode ? &cstNode->argsCommaPositions : nullptr); std::optional argLocation; @@ -1476,7 +1497,7 @@ std::pair Parser::parseFunctionBody( if (FFlag::LuauErrorRecoveryForTableTypes) matchRecoveryStopOnToken[')']--; - std::optional typelist = parseOptionalReturnType(); + std::optional typelist = parseOptionalReturnType(cstNode ? &cstNode->returnSpecifierPosition : nullptr); AstLocal* funLocal = nullptr; @@ -1503,8 +1524,9 @@ std::pair Parser::parseFunctionBody( bool hasEnd = expectMatchEndAndConsume(Lexeme::ReservedEnd, matchFunction); body->hasEnd = hasEnd; - return { - allocator.alloc( + if (FFlag::LuauStoreCSTData) + { + AstExprFunction* node = allocator.alloc( Location(start, end), attributes, generics, @@ -1519,9 +1541,34 @@ std::pair Parser::parseFunctionBody( typelist, varargAnnotation, argLocation - ), - funLocal - }; + ); + if (options.storeCstData) + cstNodeMap[node] = cstNode; + + return {node, funLocal}; + } + else + { + return { + allocator.alloc( + Location(start, end), + attributes, + generics, + genericPacks, + self, + vars, + vararg, + varargLocation, + body, + functionStack.size(), + debugname, + typelist, + varargAnnotation, + argLocation + ), + funLocal + }; + } } // explist ::= {exp `,'} exp @@ -1559,8 +1606,13 @@ Parser::Binding Parser::parseBinding() } // bindinglist ::= (binding | `...') [`,' bindinglist] -std::tuple Parser::parseBindingList(TempVector& result, bool allowDot3, TempVector* commaPositions) +std::tuple Parser::parseBindingList(TempVector& result, bool allowDot3, AstArray* commaPositions, std::optional initialCommaPosition) { + TempVector localCommaPositions(scratchPosition); + + if (FFlag::LuauStoreCSTData && commaPositions && initialCommaPosition) + localCommaPositions.push_back(*initialCommaPosition); + while (true) { if (lexer.current().type == Lexeme::Dot3 && allowDot3) @@ -1575,6 +1627,9 @@ std::tuple Parser::parseBindingList(TempVector Parser::parseBindingList(TempVectorpush_back(lexer.current().location.begin); + localCommaPositions.push_back(lexer.current().location.begin); nextLexeme(); } + if (FFlag::LuauStoreCSTData && commaPositions) + *commaPositions = copy(localCommaPositions); + return {false, Location(), nullptr}; } @@ -1602,7 +1660,12 @@ AstType* Parser::parseOptionalType() } // TypeList ::= Type [`,' TypeList] | ...Type -AstTypePack* Parser::parseTypeList(TempVector& result, TempVector>& resultNames) +AstTypePack* Parser::parseTypeList( + TempVector& result, + TempVector>& resultNames, + TempVector* commaPositions, + TempVector>* nameColonPositions +) { while (true) { @@ -1614,22 +1677,33 @@ AstTypePack* Parser::parseTypeList(TempVector& result, TempVectorsize() < result.size()) + nameColonPositions->push_back({}); + } resultNames.push_back(AstArgumentName{AstName(lexer.current().name), lexer.current().location}); nextLexeme(); + if (FFlag::LuauStoreCSTData && nameColonPositions) + nameColonPositions->push_back(lexer.current().location.begin); expectAndConsume(':'); } else if (!resultNames.empty()) { // If we have a type with named arguments, provide elements for all types resultNames.push_back({}); + if (FFlag::LuauStoreCSTData && nameColonPositions) + nameColonPositions->push_back({}); } result.push_back(parseType()); if (lexer.current().type != ',') break; + if (FFlag::LuauStoreCSTData && commaPositions) + commaPositions->push_back(lexer.current().location.begin); nextLexeme(); if (lexer.current().type == ')') @@ -1642,13 +1716,15 @@ AstTypePack* Parser::parseTypeList(TempVector& result, TempVector Parser::parseOptionalReturnType() +std::optional Parser::parseOptionalReturnType(Position* returnSpecifierPosition) { if (lexer.current().type == ':' || lexer.current().type == Lexeme::SkinnyArrow) { if (lexer.current().type == Lexeme::SkinnyArrow) report(lexer.current().location, "Function return type annotations are written after ':' instead of '->'"); + if (FFlag::LuauStoreCSTData && returnSpecifierPosition) + *returnSpecifierPosition = lexer.current().location.begin; nextLexeme(); unsigned int oldRecursionCount = recursionCounter; @@ -2016,7 +2092,14 @@ AstTypeOrPack Parser::parseFunctionType(bool allowPack, const AstArray Lexeme begin = lexer.current(); - auto [generics, genericPacks] = parseGenericTypeList(/* withDefaultValues= */ false); + Position genericsOpenPosition{0, 0}; + AstArray genericsCommaPositions; + Position genericsClosePosition{0, 0}; + auto [generics, genericPacks] = FFlag::LuauStoreCSTData && options.storeCstData + ? parseGenericTypeList( + /* withDefaultValues= */ false, &genericsOpenPosition, &genericsCommaPositions, &genericsClosePosition + ) + : parseGenericTypeList(/* withDefaultValues= */ false); Lexeme parameterStart = lexer.current(); @@ -2026,10 +2109,17 @@ AstTypeOrPack Parser::parseFunctionType(bool allowPack, const AstArray TempVector params(scratchType); TempVector> names(scratchOptArgName); + TempVector> nameColonPositions(scratchOptPosition); + TempVector argCommaPositions(scratchPosition); AstTypePack* varargAnnotation = nullptr; if (lexer.current().type != ')') - varargAnnotation = parseTypeList(params, names); + { + if (FFlag::LuauStoreCSTData && options.storeCstData) + varargAnnotation = parseTypeList(params, names, &argCommaPositions, &nameColonPositions); + else + varargAnnotation = parseTypeList(params, names); + } Location closeArgsLocation = lexer.current().location; expectMatchAndConsume(')', parameterStart, true); @@ -2047,7 +2137,20 @@ AstTypeOrPack Parser::parseFunctionType(bool allowPack, const AstArray if (params.size() == 1 && !varargAnnotation && !forceFunctionType && !returnTypeIntroducer) { if (allowPack) - return {{}, allocator.alloc(begin.location, AstTypeList{paramTypes, nullptr})}; + { + if (FFlag::LuauStoreCSTData) + { + AstTypePackExplicit* node = allocator.alloc(begin.location, AstTypeList{paramTypes, nullptr}); + if (options.storeCstData) + cstNodeMap[node] = + allocator.alloc(parameterStart.location.begin, closeArgsLocation.begin, copy(argCommaPositions)); + return {{}, node}; + } + else + { + return {{}, allocator.alloc(begin.location, AstTypeList{paramTypes, nullptr})}; + } + } else { if (FFlag::LuauAstTypeGroup3) @@ -2058,11 +2161,46 @@ AstTypeOrPack Parser::parseFunctionType(bool allowPack, const AstArray } if (!forceFunctionType && !returnTypeIntroducer && allowPack) - return {{}, allocator.alloc(begin.location, AstTypeList{paramTypes, varargAnnotation})}; + { + if (FFlag::LuauStoreCSTData) + { + AstTypePackExplicit* node = allocator.alloc(begin.location, AstTypeList{paramTypes, varargAnnotation}); + if (options.storeCstData) + cstNodeMap[node] = + allocator.alloc(parameterStart.location.begin, closeArgsLocation.begin, copy(argCommaPositions)); + return {{}, node}; + } + else + { + return {{}, allocator.alloc(begin.location, AstTypeList{paramTypes, varargAnnotation})}; + } + } AstArray> paramNames = copy(names); - return {parseFunctionTypeTail(begin, attributes, generics, genericPacks, paramTypes, paramNames, varargAnnotation), {}}; + if (FFlag::LuauStoreCSTData) + { + Position returnArrowPosition = lexer.current().location.begin; + AstType* node = parseFunctionTypeTail(begin, attributes, generics, genericPacks, paramTypes, paramNames, varargAnnotation); + if (options.storeCstData && node->is()) + { + cstNodeMap[node] = allocator.alloc( + genericsOpenPosition, + genericsCommaPositions, + genericsClosePosition, + parameterStart.location.begin, + copy(nameColonPositions), + copy(argCommaPositions), + closeArgsLocation.begin, + returnArrowPosition + ); + } + return {node, {}}; + } + else + { + return {parseFunctionTypeTail(begin, attributes, generics, genericPacks, paramTypes, paramNames, varargAnnotation), {}}; + } } AstType* Parser::parseFunctionTypeTail( @@ -2124,7 +2262,9 @@ AstType* Parser::parseTypeSuffix(AstType* type, const Location& begin) bool isUnion = false; bool isIntersection = false; - bool hasOptional = false; + // Clip with FFlag::LuauParseOptionalAsNode + bool hasOptional_DEPRECATED = false; + unsigned int optionalCount = 0; Location location = begin; @@ -2148,11 +2288,19 @@ AstType* Parser::parseTypeSuffix(AstType* type, const Location& begin) Location loc = lexer.current().location; nextLexeme(); - if (!hasOptional) - parts.push_back(allocator.alloc(loc, std::nullopt, nameNil, std::nullopt, loc)); + if (FFlag::LuauParseOptionalAsNode) + { + parts.push_back(allocator.alloc(Location(loc))); + optionalCount++; + } + else + { + if (!hasOptional_DEPRECATED) + parts.push_back(allocator.alloc(loc, std::nullopt, nameNil, std::nullopt, loc)); + } isUnion = true; - hasOptional = true; + hasOptional_DEPRECATED = true; } else if (c == '&') { @@ -2172,8 +2320,16 @@ AstType* Parser::parseTypeSuffix(AstType* type, const Location& begin) else break; - if (parts.size() > unsigned(FInt::LuauTypeLengthLimit) + hasOptional) - ParseError::raise(parts.back()->location, "Exceeded allowed type length; simplify your type annotation to make the code compile"); + if (FFlag::LuauParseOptionalAsNode) + { + if (parts.size() > unsigned(FInt::LuauTypeLengthLimit) + optionalCount) + ParseError::raise(parts.back()->location, "Exceeded allowed type length; simplify your type annotation to make the code compile"); + } + else + { + if (parts.size() > unsigned(FInt::LuauTypeLengthLimit) + hasOptional_DEPRECATED) + ParseError::raise(parts.back()->location, "Exceeded allowed type length; simplify your type annotation to make the code compile"); + } } if (FFlag::LuauPreserveUnionIntersectionNodeForLeadingTokenSingleType) @@ -2465,7 +2621,17 @@ AstTypePack* Parser::parseVariadicArgumentTypePack() // This will not fail because of the lookahead guard. expectAndConsume(Lexeme::Dot3, "generic type pack annotation"); - return allocator.alloc(Location(name.location, end), name.name); + if (FFlag::LuauStoreCSTData) + { + AstTypePackGeneric* node = allocator.alloc(Location(name.location, end), name.name); + if (options.storeCstData) + cstNodeMap[node] = allocator.alloc(end.begin); + return node; + } + else + { + return allocator.alloc(Location(name.location, end), name.name); + } } // Variadic: T else @@ -2493,7 +2659,17 @@ AstTypePack* Parser::parseTypePack() // This will not fail because of the lookahead guard. expectAndConsume(Lexeme::Dot3, "generic type pack annotation"); - return allocator.alloc(Location(name.location, end), name.name); + if (FFlag::LuauStoreCSTData) + { + AstTypePackGeneric* node = allocator.alloc(Location(name.location, end), name.name); + if (options.storeCstData) + cstNodeMap[node] = allocator.alloc(end.begin); + return node; + } + else + { + return allocator.alloc(Location(name.location, end), name.name); + } } // TODO: shouldParseTypePack can be removed and parseTypePack can be called unconditionally instead @@ -3348,12 +3524,13 @@ Parser::Name Parser::parseIndexName(const char* context, const Position& previou std::pair, AstArray> Parser::parseGenericTypeList( bool withDefaultValues, Position* openPosition, - TempVector* commaPositions, + AstArray* commaPositions, Position* closePosition ) { TempVector names{scratchGenericTypes}; TempVector namePacks{scratchGenericTypePacks}; + TempVector localCommaPositions{scratchPosition}; if (lexer.current().type == '<') { @@ -3483,7 +3660,7 @@ std::pair, AstArray> Parser::pars if (lexer.current().type == ',') { if (FFlag::LuauStoreCSTData && commaPositions) - commaPositions->push_back(lexer.current().location.begin); + localCommaPositions.push_back(lexer.current().location.begin); nextLexeme(); if (lexer.current().type == '>') @@ -3501,6 +3678,9 @@ std::pair, AstArray> Parser::pars expectMatchAndConsume('>', begin); } + if (FFlag::LuauStoreCSTData && commaPositions) + *commaPositions = copy(localCommaPositions); + AstArray generics = copy(names); AstArray genericPacks = copy(namePacks); return {generics, genericPacks}; diff --git a/Compiler/src/Types.cpp b/Compiler/src/Types.cpp index 34a27f4f..e984370e 100644 --- a/Compiler/src/Types.cpp +++ b/Compiler/src/Types.cpp @@ -125,6 +125,10 @@ static LuauBytecodeType getType( { return getType(group->type, generics, typeAliases, resolveAliases, hostVectorType, userdataTypes, bytecode); } + else if (const AstTypeOptional* optional = ty->as()) + { + return LBC_TYPE_NIL; + } return LBC_TYPE_ANY; } diff --git a/tests/AnyTypeSummary.test.cpp b/tests/AnyTypeSummary.test.cpp index 99a02a36..a701f2cb 100644 --- a/tests/AnyTypeSummary.test.cpp +++ b/tests/AnyTypeSummary.test.cpp @@ -53,9 +53,9 @@ type A = (number, string) -> ...any ModulePtr module = frontend.moduleResolver.getModule("game/Gui/Modules/A"); - LUAU_ASSERT(module->ats.typeInfo.size() == 1); - LUAU_ASSERT(module->ats.typeInfo[0].code == Pattern::Alias); - LUAU_ASSERT(module->ats.typeInfo[0].node == "type A = (number, string)->( ...any)"); + REQUIRE(module->ats.typeInfo.size() == 1); + CHECK(module->ats.typeInfo[0].code == Pattern::Alias); + CHECK(module->ats.typeInfo[0].node == "type A = (number, string)->( ...any)"); } TEST_CASE_FIXTURE(ATSFixture, "export_alias") @@ -74,23 +74,23 @@ export type t8 = t0 &((true | any)->('')) ModulePtr module = frontend.moduleResolver.getModule("game/Gui/Modules/A"); - LUAU_ASSERT(module->ats.typeInfo.size() == 1); - LUAU_ASSERT(module->ats.typeInfo[0].code == Pattern::Alias); + REQUIRE(module->ats.typeInfo.size() == 1); + CHECK(module->ats.typeInfo[0].code == Pattern::Alias); if (FFlag::LuauStoreCSTData && FFlag::LuauAstTypeGroup3) { - LUAU_ASSERT(module->ats.typeInfo[0].node == "export type t8 = t0& (( true | any)->(''))"); + CHECK(module->ats.typeInfo[0].node == "export type t8 = t0& (( true | any)->(''))"); } else if (FFlag::LuauStoreCSTData) { - LUAU_ASSERT(module->ats.typeInfo[0].node == "export type t8 = t0 &(( true | any)->(''))"); + CHECK(module->ats.typeInfo[0].node == "export type t8 = t0 &(( true | any)->(''))"); } else if (FFlag::LuauAstTypeGroup3) { - LUAU_ASSERT(module->ats.typeInfo[0].node == "export type t8 = t0& ((true | any)->(''))"); + CHECK(module->ats.typeInfo[0].node == "export type t8 = t0& ((true | any)->(''))"); } else { - LUAU_ASSERT(module->ats.typeInfo[0].node == "export type t8 = t0 &((true | any)->(''))"); + CHECK(module->ats.typeInfo[0].node == "export type t8 = t0 &((true | any)->(''))"); } } @@ -115,9 +115,9 @@ end ModulePtr module = frontend.moduleResolver.getModule("game/Gui/Modules/A"); - LUAU_ASSERT(module->ats.typeInfo.size() == 3); - LUAU_ASSERT(module->ats.typeInfo[1].code == Pattern::TypePk); - LUAU_ASSERT( + REQUIRE(module->ats.typeInfo.size() == 3); + CHECK(module->ats.typeInfo[1].code == Pattern::TypePk); + CHECK( module->ats.typeInfo[0].node == "local function fallible(t: number): ...any\n if t > 0 then\n return true, t\n end\n return false, 'must be positive'\nend" ); @@ -145,7 +145,7 @@ end ModulePtr module = frontend.moduleResolver.getModule("game/Gui/Modules/A"); - LUAU_ASSERT(module->ats.typeInfo.size() == 0); + REQUIRE(module->ats.typeInfo.size() == 0); } TEST_CASE_FIXTURE(ATSFixture, "var_typepack_any_gen_table") @@ -164,9 +164,9 @@ type Pair = {first: T, second: any} ModulePtr module = frontend.moduleResolver.getModule("game/Gui/Modules/A"); - LUAU_ASSERT(module->ats.typeInfo.size() == 1); - LUAU_ASSERT(module->ats.typeInfo[0].code == Pattern::Alias); - LUAU_ASSERT(module->ats.typeInfo[0].node == "type Pair = {first: T, second: any}"); + REQUIRE(module->ats.typeInfo.size() == 1); + CHECK(module->ats.typeInfo[0].code == Pattern::Alias); + CHECK(module->ats.typeInfo[0].node == "type Pair = {first: T, second: any}"); } TEST_CASE_FIXTURE(ATSFixture, "assign_uneq") @@ -190,7 +190,7 @@ local x, y, z = greetings("Dibri") -- mismatch LUAU_REQUIRE_ERROR_COUNT(1, result1); ModulePtr module = frontend.moduleResolver.getModule("game/Gui/Modules/B"); - LUAU_ASSERT(module->ats.typeInfo.size() == 0); + REQUIRE(module->ats.typeInfo.size() == 0); } TEST_CASE_FIXTURE(ATSFixture, "var_typepack_any_gen") @@ -210,9 +210,9 @@ type Pair = (boolean, T) -> ...any ModulePtr module = frontend.moduleResolver.getModule("game/Gui/Modules/A"); - LUAU_ASSERT(module->ats.typeInfo.size() == 1); - LUAU_ASSERT(module->ats.typeInfo[0].code == Pattern::Alias); - LUAU_ASSERT(module->ats.typeInfo[0].node == "type Pair = (boolean, T)->( ...any)"); + REQUIRE(module->ats.typeInfo.size() == 1); + CHECK(module->ats.typeInfo[0].code == Pattern::Alias); + CHECK(module->ats.typeInfo[0].node == "type Pair = (boolean, T)->( ...any)"); } TEST_CASE_FIXTURE(ATSFixture, "typeof_any_in_func") @@ -234,9 +234,9 @@ TEST_CASE_FIXTURE(ATSFixture, "typeof_any_in_func") ModulePtr module = frontend.moduleResolver.getModule("game/Gui/Modules/A"); - LUAU_ASSERT(module->ats.typeInfo.size() == 2); - LUAU_ASSERT(module->ats.typeInfo[0].code == Pattern::VarAnnot); - LUAU_ASSERT(module->ats.typeInfo[0].node == "local function f()\n local a: any = 1\n local b: typeof(a) = 1\n end"); + REQUIRE(module->ats.typeInfo.size() == 2); + CHECK(module->ats.typeInfo[0].code == Pattern::VarAnnot); + CHECK(module->ats.typeInfo[0].node == "local function f()\n local a: any = 1\n local b: typeof(a) = 1\n end"); } TEST_CASE_FIXTURE(ATSFixture, "generic_types") @@ -264,9 +264,9 @@ foo(addNumbers) ModulePtr module = frontend.moduleResolver.getModule("game/Gui/Modules/A"); - LUAU_ASSERT(module->ats.typeInfo.size() == 3); - LUAU_ASSERT(module->ats.typeInfo[1].code == Pattern::FuncApp); - LUAU_ASSERT(module->ats.typeInfo[0].node == "local function foo(a: (...A)->( any),...: A)\n return a(...)\nend"); + REQUIRE(module->ats.typeInfo.size() == 3); + CHECK(module->ats.typeInfo[1].code == Pattern::FuncApp); + CHECK(module->ats.typeInfo[0].node == "local function foo(a: (...A)->( any),...: A)\n return a(...)\nend"); } TEST_CASE_FIXTURE(ATSFixture, "no_annot") @@ -285,7 +285,7 @@ local character = script.Parent ModulePtr module = frontend.moduleResolver.getModule("game/Gui/Modules/A"); - LUAU_ASSERT(module->ats.typeInfo.size() == 0); + REQUIRE(module->ats.typeInfo.size() == 0); } TEST_CASE_FIXTURE(ATSFixture, "if_any") @@ -314,9 +314,9 @@ end ModulePtr module = frontend.moduleResolver.getModule("game/Gui/Modules/A"); - LUAU_ASSERT(module->ats.typeInfo.size() == 1); - LUAU_ASSERT(module->ats.typeInfo[0].code == Pattern::FuncArg); - LUAU_ASSERT( + REQUIRE(module->ats.typeInfo.size() == 1); + CHECK(module->ats.typeInfo[0].code == Pattern::FuncArg); + CHECK( module->ats.typeInfo[0].node == "function f(x: any)\nif not x then\nx = {\n y = math.random(0, 2^31-1),\n left = nil,\n right = " "nil\n}\nelse\n local expected = x * 5\nend\nend" ); @@ -342,9 +342,9 @@ TEST_CASE_FIXTURE(ATSFixture, "variadic_any") ModulePtr module = frontend.moduleResolver.getModule("game/Gui/Modules/A"); - LUAU_ASSERT(module->ats.typeInfo.size() == 2); - LUAU_ASSERT(module->ats.typeInfo[0].code == Pattern::FuncRet); - LUAU_ASSERT(module->ats.typeInfo[0].node == "local function f(): (number, ...any)\n return 1, 5\n end"); + REQUIRE(module->ats.typeInfo.size() == 2); + CHECK(module->ats.typeInfo[0].code == Pattern::FuncRet); + CHECK(module->ats.typeInfo[0].node == "local function f(): (number, ...any)\n return 1, 5\n end"); } TEST_CASE_FIXTURE(ATSFixture, "type_alias_intersection") @@ -366,9 +366,9 @@ TEST_CASE_FIXTURE(ATSFixture, "type_alias_intersection") ModulePtr module = frontend.moduleResolver.getModule("game/Gui/Modules/A"); - LUAU_ASSERT(module->ats.typeInfo.size() == 3); - LUAU_ASSERT(module->ats.typeInfo[2].code == Pattern::VarAnnot); - LUAU_ASSERT(module->ats.typeInfo[2].node == "local vec2: Vector2 = {x = 1, y = 2}"); + REQUIRE(module->ats.typeInfo.size() == 3); + CHECK(module->ats.typeInfo[2].code == Pattern::VarAnnot); + CHECK(module->ats.typeInfo[2].node == "local vec2: Vector2 = {x = 1, y = 2}"); } TEST_CASE_FIXTURE(ATSFixture, "var_func_arg") @@ -394,9 +394,9 @@ TEST_CASE_FIXTURE(ATSFixture, "var_func_arg") ModulePtr module = frontend.moduleResolver.getModule("game/Gui/Modules/A"); - LUAU_ASSERT(module->ats.typeInfo.size() == 4); - LUAU_ASSERT(module->ats.typeInfo[0].code == Pattern::VarAny); - LUAU_ASSERT(module->ats.typeInfo[0].node == "local function f(...: any)\n end"); + REQUIRE(module->ats.typeInfo.size() == 4); + CHECK(module->ats.typeInfo[0].code == Pattern::VarAny); + CHECK(module->ats.typeInfo[0].node == "local function f(...: any)\n end"); } TEST_CASE_FIXTURE(ATSFixture, "var_func_apps") @@ -418,9 +418,9 @@ TEST_CASE_FIXTURE(ATSFixture, "var_func_apps") ModulePtr module = frontend.moduleResolver.getModule("game/Gui/Modules/A"); - LUAU_ASSERT(module->ats.typeInfo.size() == 3); - LUAU_ASSERT(module->ats.typeInfo[0].code == Pattern::VarAny); - LUAU_ASSERT(module->ats.typeInfo[0].node == "local function f(...: any)\n end"); + REQUIRE(module->ats.typeInfo.size() == 3); + CHECK(module->ats.typeInfo[0].code == Pattern::VarAny); + CHECK(module->ats.typeInfo[0].node == "local function f(...: any)\n end"); } @@ -455,7 +455,7 @@ end CHECK_EQ(module->ats.typeInfo[0].node, "descendant.CollisionGroup = CAR_COLLISION_GROUP"); } else - LUAU_ASSERT(module->ats.typeInfo.size() == 0); + REQUIRE(module->ats.typeInfo.size() == 0); } TEST_CASE_FIXTURE(ATSFixture, "unknown_symbol") @@ -477,9 +477,9 @@ end ModulePtr module = frontend.moduleResolver.getModule("game/Gui/Modules/A"); - LUAU_ASSERT(module->ats.typeInfo.size() == 2); - LUAU_ASSERT(module->ats.typeInfo[0].code == Pattern::FuncArg); - LUAU_ASSERT(module->ats.typeInfo[0].node == "local function manageRace(raceContainer: Model)\n RaceManager.new(raceContainer)\nend"); + REQUIRE(module->ats.typeInfo.size() == 2); + CHECK(module->ats.typeInfo[0].code == Pattern::FuncArg); + CHECK(module->ats.typeInfo[0].node == "local function manageRace(raceContainer: Model)\n RaceManager.new(raceContainer)\nend"); } TEST_CASE_FIXTURE(ATSFixture, "racing_3_short") @@ -518,9 +518,9 @@ initialize() ModulePtr module = frontend.moduleResolver.getModule("game/Gui/Modules/A"); - LUAU_ASSERT(module->ats.typeInfo.size() == 5); - LUAU_ASSERT(module->ats.typeInfo[0].code == Pattern::FuncArg); - LUAU_ASSERT(module->ats.typeInfo[0].node == "local function manageRace(raceContainer: Model)\n RaceManager.new(raceContainer)\nend"); + REQUIRE(module->ats.typeInfo.size() == 5); + CHECK(module->ats.typeInfo[0].code == Pattern::FuncArg); + CHECK(module->ats.typeInfo[0].node == "local function manageRace(raceContainer: Model)\n RaceManager.new(raceContainer)\nend"); } TEST_CASE_FIXTURE(ATSFixture, "racing_collision_2") @@ -596,10 +596,10 @@ initialize() ModulePtr module = frontend.moduleResolver.getModule("game/Gui/Modules/A"); if (FFlag::LuauSkipNoRefineDuringRefinement) - CHECK_EQ(module->ats.typeInfo.size(), 12); + REQUIRE_EQ(module->ats.typeInfo.size(), 12); else - LUAU_ASSERT(module->ats.typeInfo.size() == 11); - LUAU_ASSERT(module->ats.typeInfo[0].code == Pattern::FuncArg); + REQUIRE(module->ats.typeInfo.size() == 11); + CHECK(module->ats.typeInfo[0].code == Pattern::FuncArg); if (FFlag::LuauStoreCSTData) { CHECK_EQ( @@ -612,7 +612,7 @@ initialize() } else { - LUAU_ASSERT( + CHECK( module->ats.typeInfo[0].node == "local function onCharacterAdded(character: Model)\n\n character.DescendantAdded:Connect(function(descendant)\n if " "descendant:IsA('BasePart')then\n descendant.CollisionGroup = CHARACTER_COLLISION_GROUP\n end\n end)\n\n\n for _, descendant in " @@ -685,9 +685,9 @@ initialize() ModulePtr module = frontend.moduleResolver.getModule("game/Gui/Modules/A"); - LUAU_ASSERT(module->ats.typeInfo.size() == 7); - LUAU_ASSERT(module->ats.typeInfo[0].code == Pattern::FuncArg); - LUAU_ASSERT( + REQUIRE(module->ats.typeInfo.size() == 7); + CHECK(module->ats.typeInfo[0].code == Pattern::FuncArg); + CHECK( module->ats.typeInfo[0].node == "local function setupKiosk(kiosk: Model)\n local spawnLocation = kiosk:FindFirstChild('SpawnLocation')\n assert(spawnLocation, " "`{kiosk:GetFullName()} has no SpawnLocation part`)\n local promptPart = kiosk:FindFirstChild('Prompt')\n assert(promptPart, " @@ -719,7 +719,7 @@ TEST_CASE_FIXTURE(ATSFixture, "mutually_recursive_generic") ModulePtr module = frontend.moduleResolver.getModule("game/Gui/Modules/A"); - LUAU_ASSERT(module->ats.typeInfo.size() == 0); + REQUIRE(module->ats.typeInfo.size() == 0); } TEST_CASE_FIXTURE(ATSFixture, "explicit_pack") @@ -739,9 +739,9 @@ type Bar = Foo<(number, any)> ModulePtr module = frontend.moduleResolver.getModule("game/Gui/Modules/A"); - LUAU_ASSERT(module->ats.typeInfo.size() == 1); - LUAU_ASSERT(module->ats.typeInfo[0].code == Pattern::Alias); - LUAU_ASSERT(module->ats.typeInfo[0].node == "type Bar = Foo<(number, any)>"); + REQUIRE(module->ats.typeInfo.size() == 1); + CHECK(module->ats.typeInfo[0].code == Pattern::Alias); + CHECK(module->ats.typeInfo[0].node == "type Bar = Foo<(number, any)>"); } TEST_CASE_FIXTURE(ATSFixture, "local_val") @@ -760,9 +760,9 @@ local a, b, c = 1 :: any ModulePtr module = frontend.moduleResolver.getModule("game/Gui/Modules/A"); - LUAU_ASSERT(module->ats.typeInfo.size() == 1); - LUAU_ASSERT(module->ats.typeInfo[0].code == Pattern::Casts); - LUAU_ASSERT(module->ats.typeInfo[0].node == "local a, b, c = 1 :: any"); + REQUIRE(module->ats.typeInfo.size() == 1); + CHECK(module->ats.typeInfo[0].code == Pattern::Casts); + CHECK(module->ats.typeInfo[0].node == "local a, b, c = 1 :: any"); } TEST_CASE_FIXTURE(ATSFixture, "var_any_local") @@ -784,9 +784,9 @@ local x: number, y: any, z, h: nil = 1, nil ModulePtr module = frontend.moduleResolver.getModule("game/Gui/Modules/A"); - LUAU_ASSERT(module->ats.typeInfo.size() == 3); - LUAU_ASSERT(module->ats.typeInfo[0].code == Pattern::VarAnnot); - LUAU_ASSERT(module->ats.typeInfo[0].node == "local x: any = 2, 3"); + REQUIRE(module->ats.typeInfo.size() == 3); + CHECK(module->ats.typeInfo[0].code == Pattern::VarAnnot); + CHECK(module->ats.typeInfo[0].node == "local x: any = 2, 3"); } TEST_CASE_FIXTURE(ATSFixture, "table_uses_any") @@ -807,9 +807,9 @@ TEST_CASE_FIXTURE(ATSFixture, "table_uses_any") ModulePtr module = frontend.moduleResolver.getModule("game/Gui/Modules/A"); - LUAU_ASSERT(module->ats.typeInfo.size() == 1); - LUAU_ASSERT(module->ats.typeInfo[0].code == Pattern::VarAnnot); - LUAU_ASSERT(module->ats.typeInfo[0].node == "local x: any = 0"); + REQUIRE(module->ats.typeInfo.size() == 1); + CHECK(module->ats.typeInfo[0].code == Pattern::VarAnnot); + CHECK(module->ats.typeInfo[0].node == "local x: any = 0"); } TEST_CASE_FIXTURE(ATSFixture, "typeof_any") @@ -830,9 +830,9 @@ TEST_CASE_FIXTURE(ATSFixture, "typeof_any") ModulePtr module = frontend.moduleResolver.getModule("game/Gui/Modules/A"); - LUAU_ASSERT(module->ats.typeInfo.size() == 2); - LUAU_ASSERT(module->ats.typeInfo[1].code == Pattern::FuncArg); - LUAU_ASSERT(module->ats.typeInfo[0].node == "function some1(x: typeof(x))\n end"); + REQUIRE(module->ats.typeInfo.size() == 2); + CHECK(module->ats.typeInfo[1].code == Pattern::FuncArg); + CHECK(module->ats.typeInfo[0].node == "function some1(x: typeof(x))\n end"); } TEST_CASE_FIXTURE(ATSFixture, "table_type_assigned") @@ -854,9 +854,9 @@ TEST_CASE_FIXTURE(ATSFixture, "table_type_assigned") ModulePtr module = frontend.moduleResolver.getModule("game/Gui/Modules/A"); - LUAU_ASSERT(module->ats.typeInfo.size() == 2); - LUAU_ASSERT(module->ats.typeInfo[1].code == Pattern::Assign); - LUAU_ASSERT(module->ats.typeInfo[0].node == "local x: { x: any?} = {x = 1}"); + REQUIRE(module->ats.typeInfo.size() == 2); + CHECK(module->ats.typeInfo[1].code == Pattern::Assign); + CHECK(module->ats.typeInfo[0].node == "local x: { x: any?} = {x = 1}"); } TEST_CASE_FIXTURE(ATSFixture, "simple_func_wo_ret") @@ -876,9 +876,9 @@ TEST_CASE_FIXTURE(ATSFixture, "simple_func_wo_ret") ModulePtr module = frontend.moduleResolver.getModule("game/Gui/Modules/A"); - LUAU_ASSERT(module->ats.typeInfo.size() == 1); - LUAU_ASSERT(module->ats.typeInfo[0].code == Pattern::FuncArg); - LUAU_ASSERT(module->ats.typeInfo[0].node == "function some(x: any)\n end"); + REQUIRE(module->ats.typeInfo.size() == 1); + CHECK(module->ats.typeInfo[0].code == Pattern::FuncArg); + CHECK(module->ats.typeInfo[0].node == "function some(x: any)\n end"); } TEST_CASE_FIXTURE(ATSFixture, "simple_func_w_ret") @@ -899,9 +899,9 @@ TEST_CASE_FIXTURE(ATSFixture, "simple_func_w_ret") ModulePtr module = frontend.moduleResolver.getModule("game/Gui/Modules/A"); - LUAU_ASSERT(module->ats.typeInfo.size() == 1); - LUAU_ASSERT(module->ats.typeInfo[0].code == Pattern::FuncRet); - LUAU_ASSERT(module->ats.typeInfo[0].node == "function other(y: number): any\n return 'gotcha!'\n end"); + REQUIRE(module->ats.typeInfo.size() == 1); + CHECK(module->ats.typeInfo[0].code == Pattern::FuncRet); + CHECK(module->ats.typeInfo[0].node == "function other(y: number): any\n return 'gotcha!'\n end"); } TEST_CASE_FIXTURE(ATSFixture, "nested_local") @@ -923,9 +923,9 @@ TEST_CASE_FIXTURE(ATSFixture, "nested_local") ModulePtr module = frontend.moduleResolver.getModule("game/Gui/Modules/A"); - LUAU_ASSERT(module->ats.typeInfo.size() == 1); - LUAU_ASSERT(module->ats.typeInfo[0].code == Pattern::VarAnnot); - LUAU_ASSERT(module->ats.typeInfo[0].node == "function cool(y: number): number\n local g: any = 'gratatataaa'\n return y\n end"); + REQUIRE(module->ats.typeInfo.size() == 1); + CHECK(module->ats.typeInfo[0].code == Pattern::VarAnnot); + CHECK(module->ats.typeInfo[0].node == "function cool(y: number): number\n local g: any = 'gratatataaa'\n return y\n end"); } TEST_CASE_FIXTURE(ATSFixture, "generic_func") @@ -946,9 +946,9 @@ TEST_CASE_FIXTURE(ATSFixture, "generic_func") ModulePtr module = frontend.moduleResolver.getModule("game/Gui/Modules/A"); - LUAU_ASSERT(module->ats.typeInfo.size() == 1); - LUAU_ASSERT(module->ats.typeInfo[0].code == Pattern::FuncArg); - LUAU_ASSERT(module->ats.typeInfo[0].node == "function reverse(a: {T}, b: any): {T}\n return a\n end"); + REQUIRE(module->ats.typeInfo.size() == 1); + CHECK(module->ats.typeInfo[0].code == Pattern::FuncArg); + CHECK(module->ats.typeInfo[0].node == "function reverse(a: {T}, b: any): {T}\n return a\n end"); } TEST_CASE_FIXTURE(ATSFixture, "type_alias_any") @@ -968,9 +968,9 @@ TEST_CASE_FIXTURE(ATSFixture, "type_alias_any") ModulePtr module = frontend.moduleResolver.getModule("game/Gui/Modules/A"); - LUAU_ASSERT(module->ats.typeInfo.size() == 2); - LUAU_ASSERT(module->ats.typeInfo[0].code == Pattern::Alias); - LUAU_ASSERT(module->ats.typeInfo[0].node == "type Clear = any"); + REQUIRE(module->ats.typeInfo.size() == 2); + CHECK(module->ats.typeInfo[0].code == Pattern::Alias); + CHECK(module->ats.typeInfo[0].node == "type Clear = any"); } TEST_CASE_FIXTURE(ATSFixture, "multi_module_any") @@ -1001,9 +1001,9 @@ TEST_CASE_FIXTURE(ATSFixture, "multi_module_any") ModulePtr module = frontend.moduleResolver.getModule("game/B"); - LUAU_ASSERT(module->ats.typeInfo.size() == 2); - LUAU_ASSERT(module->ats.typeInfo[0].code == Pattern::Alias); - LUAU_ASSERT(module->ats.typeInfo[0].node == "type Clear = any"); + REQUIRE(module->ats.typeInfo.size() == 2); + CHECK(module->ats.typeInfo[0].code == Pattern::Alias); + CHECK(module->ats.typeInfo[0].node == "type Clear = any"); } TEST_CASE_FIXTURE(ATSFixture, "cast_on_cyclic_req") @@ -1029,9 +1029,9 @@ TEST_CASE_FIXTURE(ATSFixture, "cast_on_cyclic_req") ModulePtr module = frontend.moduleResolver.getModule("game/B"); - LUAU_ASSERT(module->ats.typeInfo.size() == 3); - LUAU_ASSERT(module->ats.typeInfo[1].code == Pattern::Alias); - LUAU_ASSERT(module->ats.typeInfo[1].node == "type Clear = any"); + REQUIRE(module->ats.typeInfo.size() == 3); + CHECK(module->ats.typeInfo[1].code == Pattern::Alias); + CHECK(module->ats.typeInfo[1].node == "type Clear = any"); } diff --git a/tests/Autocomplete.test.cpp b/tests/Autocomplete.test.cpp index 6ab06f47..3c07c088 100644 --- a/tests/Autocomplete.test.cpp +++ b/tests/Autocomplete.test.cpp @@ -7,6 +7,8 @@ #include "Luau/VisitType.h" #include "Luau/StringUtils.h" + +#include "ClassFixture.h" #include "Fixture.h" #include "ScopedFlags.h" @@ -19,6 +21,8 @@ LUAU_FASTFLAG(LuauSetMetatableDoesNotTimeTravel) LUAU_FASTINT(LuauTypeInferRecursionLimit) LUAU_FASTFLAG(LuauExposeRequireByStringAutocomplete) +LUAU_FASTFLAG(LuauAutocompleteUnionCopyPreviousSeen) +LUAU_FASTFLAG(LuauUserTypeFunTypecheck) using namespace Luau; @@ -154,6 +158,10 @@ struct ACBuiltinsFixture : ACFixtureImpl { }; +struct ACClassFixture : ACFixtureImpl +{ +}; + TEST_SUITE_BEGIN("AutocompleteTest"); TEST_CASE_FIXTURE(ACFixture, "empty_program") @@ -4416,4 +4424,76 @@ local x = 1 + result. CHECK(ac.entryMap.count("x")); } +TEST_CASE_FIXTURE(ACClassFixture, "ac_dont_overflow_on_recursive_union") +{ + ScopedFastFlag _{FFlag::LuauAutocompleteUnionCopyPreviousSeen, true}; + check(R"( + local table1: {ChildClass} = {} + local table2 = {} + + for index, value in table2[1] do + table.insert(table1, value) + value.@1 + end + )"); + + auto ac = autocomplete('1'); + // RIDE-11517: This should *really* be the members of `ChildClass`, but + // would previously stack overflow. + CHECK(ac.entryMap.empty()); +} + +TEST_CASE_FIXTURE(ACBuiltinsFixture, "type_function_has_types_definitions") +{ + // Needs new global initialization in the Fixture, but can't place the flag inside the base Fixture + if (!FFlag::LuauUserTypeFunTypecheck) + return; + + ScopedFastFlag newSolver{FFlag::LuauSolverV2, true}; + + check(R"( +type function foo() + types.@1 +end + )"); + + auto ac = autocomplete('1'); + CHECK_EQ(ac.entryMap.count("singleton"), 1); +} + +TEST_CASE_FIXTURE(ACBuiltinsFixture, "type_function_private_scope") +{ + // Needs new global initialization in the Fixture, but can't place the flag inside the base Fixture + if (!FFlag::LuauUserTypeFunTypecheck) + return; + + ScopedFastFlag newSolver{FFlag::LuauSolverV2, true}; + + // Global scope polution by the embedder has no effect + addGlobalBinding(frontend.globals, "thisAlsoShouldNotBeThere", Binding{builtinTypes->anyType}); + addGlobalBinding(frontend.globalsForAutocomplete, "thisAlsoShouldNotBeThere", Binding{builtinTypes->anyType}); + + check(R"( +local function thisShouldNotBeThere() end + +type function thisShouldBeThere() end + +type function foo() + this@1 +end + +this@2 + )"); + + auto ac = autocomplete('1'); + CHECK_EQ(ac.entryMap.count("thisShouldNotBeThere"), 0); + CHECK_EQ(ac.entryMap.count("thisAlsoShouldNotBeThere"), 0); + CHECK_EQ(ac.entryMap.count("thisShouldBeThere"), 1); + + ac = autocomplete('2'); + CHECK_EQ(ac.entryMap.count("thisShouldNotBeThere"), 1); + CHECK_EQ(ac.entryMap.count("thisAlsoShouldNotBeThere"), 1); + CHECK_EQ(ac.entryMap.count("thisShouldBeThere"), 0); +} + TEST_SUITE_END(); diff --git a/tests/ClassFixture.cpp b/tests/ClassFixture.cpp index 40d06c85..6ec4ec20 100644 --- a/tests/ClassFixture.cpp +++ b/tests/ClassFixture.cpp @@ -9,7 +9,8 @@ using std::nullopt; namespace Luau { -ClassFixture::ClassFixture() +ClassFixture::ClassFixture(bool prepareAutocomplete) + : BuiltinsFixture(prepareAutocomplete) { GlobalTypes& globals = frontend.globals; TypeArena& arena = globals.globalTypes; diff --git a/tests/ClassFixture.h b/tests/ClassFixture.h index 4d8275c1..d7db1220 100644 --- a/tests/ClassFixture.h +++ b/tests/ClassFixture.h @@ -8,7 +8,7 @@ namespace Luau struct ClassFixture : BuiltinsFixture { - ClassFixture(); + explicit ClassFixture(bool prepareAutocomplete = false); TypeId vector2Type; TypeId vector2InstanceType; diff --git a/tests/ConstraintGeneratorFixture.cpp b/tests/ConstraintGeneratorFixture.cpp index e10e60d4..af7178d1 100644 --- a/tests/ConstraintGeneratorFixture.cpp +++ b/tests/ConstraintGeneratorFixture.cpp @@ -34,6 +34,7 @@ void ConstraintGeneratorFixture::generateConstraints(const std::string& code) builtinTypes, NotNull(&ice), frontend.globals.globalScope, + frontend.globals.globalTypeFunctionScope, /*prepareModuleScope*/ nullptr, &logger, NotNull{dfg.get()}, diff --git a/tests/Fixture.cpp b/tests/Fixture.cpp index a63784ec..3ab85a1d 100644 --- a/tests/Fixture.cpp +++ b/tests/Fixture.cpp @@ -323,6 +323,7 @@ AstStatBlock* Fixture::parse(const std::string& source, const ParseOptions& pars NotNull{&moduleResolver}, NotNull{&fileResolver}, frontend.globals.globalScope, + frontend.globals.globalTypeFunctionScope, /*prepareModuleScope*/ nullptr, frontend.options, {}, diff --git a/tests/FragmentAutocomplete.test.cpp b/tests/FragmentAutocomplete.test.cpp index 45b50936..fcb70573 100644 --- a/tests/FragmentAutocomplete.test.cpp +++ b/tests/FragmentAutocomplete.test.cpp @@ -43,6 +43,8 @@ LUAU_FASTFLAG(LuauDisableNewSolverAssertsInMixedMode) LUAU_FASTFLAG(LuauCloneTypeAliasBindings) LUAU_FASTFLAG(LuauDoNotClonePersistentBindings) LUAU_FASTFLAG(LuauCloneReturnTypePack) +LUAU_FASTFLAG(LuauIncrementalAutocompleteDemandBasedCloning) +LUAU_FASTFLAG(LuauUserTypeFunTypecheck) static std::optional nullCallback(std::string tag, std::optional ptr, std::optional contents) { @@ -83,6 +85,7 @@ struct FragmentAutocompleteFixtureImpl : BaseType ScopedFastFlag luauCloneTypeAliasBindings{FFlag::LuauCloneTypeAliasBindings, true}; ScopedFastFlag luauDoNotClonePersistentBindings{FFlag::LuauDoNotClonePersistentBindings, true}; ScopedFastFlag luauCloneReturnTypePack{FFlag::LuauCloneReturnTypePack, true}; + ScopedFastFlag luauIncrementalAutocompleteDemandBasedCloning{FFlag::LuauIncrementalAutocompleteDemandBasedCloning, true}; FragmentAutocompleteFixtureImpl() : BaseType(true) @@ -157,12 +160,14 @@ struct FragmentAutocompleteFixtureImpl : BaseType this->check(document, getOptions()); FragmentAutocompleteStatusResult result = autocompleteFragment(updated, cursorPos, fragmentEndPosition); + CHECK(result.status != FragmentAutocompleteStatus::InternalIce); assertions(result); ScopedFastFlag _{FFlag::LuauSolverV2, false}; this->check(document, getOptions()); result = autocompleteFragment(updated, cursorPos, fragmentEndPosition); + CHECK(result.status != FragmentAutocompleteStatus::InternalIce); assertions(result); } @@ -2341,4 +2346,40 @@ z = a.P.E autocompleteFragmentInBothSolvers(source, dest, Position{8, 9}, [](FragmentAutocompleteStatusResult& _) {}); } +TEST_CASE_FIXTURE(FragmentAutocompleteBuiltinsFixture, "user_defined_type_function_local") +{ + ScopedFastFlag luauUserTypeFunTypecheck{FFlag::LuauUserTypeFunTypecheck, true}; + + const std::string source = R"(--!strict +type function foo(x: type): type + if x.tag == "singleton" then + local t = x:value() + + return types.unionof(types.singleton(t), types.singleton(nil)) + end + + return types.number +end +)"; + + const std::string dest = R"(--!strict +type function foo(x: type): type + if x.tag == "singleton" then + local t = x:value() + x + return types.unionof(types.singleton(t), types.singleton(nil)) + end + + return types.number +end +)"; + + // Only checking in new solver as old solver doesn't handle type functions and constraint solver will ICE + ScopedFastFlag sff{FFlag::LuauSolverV2, true}; + this->check(source, getOptions()); + + FragmentAutocompleteStatusResult result = autocompleteFragment(dest, Position{4, 9}, std::nullopt); + CHECK(result.status != FragmentAutocompleteStatus::InternalIce); +} + TEST_SUITE_END(); diff --git a/tests/Frontend.test.cpp b/tests/Frontend.test.cpp index 566e770e..024956b7 100644 --- a/tests/Frontend.test.cpp +++ b/tests/Frontend.test.cpp @@ -18,6 +18,7 @@ LUAU_FASTFLAG(DebugLuauFreezeArena) LUAU_FASTFLAG(DebugLuauMagicTypes) LUAU_FASTFLAG(LuauSelectivelyRetainDFGArena) LUAU_FASTFLAG(LuauModuleHoldsAstRoot) +LUAU_FASTFLAG(LuauImproveTypePathsInErrors) namespace { @@ -920,7 +921,17 @@ TEST_CASE_FIXTURE(FrontendFixture, "it_should_be_safe_to_stringify_errors_when_f // When this test fails, it is because the TypeIds needed by the error have been deallocated. // It is thus basically impossible to predict what will happen when this assert is evaluated. // It could segfault, or you could see weird type names like the empty string or - if (FFlag::LuauSolverV2) + if (FFlag::LuauSolverV2 && FFlag::LuauImproveTypePathsInErrors) + { + REQUIRE_EQ( + "Type\n\t" + "'{ count: string }'" + "\ncould not be converted into\n\t" + "'{ Count: number }'", + toString(result.errors[0]) + ); + } + else if (FFlag::LuauSolverV2) REQUIRE_EQ( R"(Type '{ count: string }' diff --git a/tests/Lexer.test.cpp b/tests/Lexer.test.cpp index 6133305d..a791e340 100644 --- a/tests/Lexer.test.cpp +++ b/tests/Lexer.test.cpp @@ -8,8 +8,6 @@ using namespace Luau; -LUAU_FASTFLAG(LexerFixInterpStringStart) - TEST_SUITE_BEGIN("LexerTests"); TEST_CASE("broken_string_works") @@ -156,7 +154,7 @@ TEST_CASE("string_interpolation_basic") Lexeme interpEnd = lexer.next(); CHECK_EQ(interpEnd.type, Lexeme::InterpStringEnd); // The InterpStringEnd should start with }, not `. - CHECK_EQ(interpEnd.location.begin.column, FFlag::LexerFixInterpStringStart ? 11 : 12); + CHECK_EQ(interpEnd.location.begin.column, 11); } TEST_CASE("string_interpolation_full") @@ -177,7 +175,7 @@ TEST_CASE("string_interpolation_full") Lexeme interpMid = lexer.next(); CHECK_EQ(interpMid.type, Lexeme::InterpStringMid); CHECK_EQ(interpMid.toString(), "} {"); - CHECK_EQ(interpMid.location.begin.column, FFlag::LexerFixInterpStringStart ? 11 : 12); + CHECK_EQ(interpMid.location.begin.column, 11); Lexeme quote2 = lexer.next(); CHECK_EQ(quote2.type, Lexeme::QuotedString); @@ -186,7 +184,7 @@ TEST_CASE("string_interpolation_full") Lexeme interpEnd = lexer.next(); CHECK_EQ(interpEnd.type, Lexeme::InterpStringEnd); CHECK_EQ(interpEnd.toString(), "} end`"); - CHECK_EQ(interpEnd.location.begin.column, FFlag::LexerFixInterpStringStart ? 19 : 20); + CHECK_EQ(interpEnd.location.begin.column, 19); } TEST_CASE("string_interpolation_double_brace") diff --git a/tests/NonStrictTypeChecker.test.cpp b/tests/NonStrictTypeChecker.test.cpp index 61ebecf3..57e30583 100644 --- a/tests/NonStrictTypeChecker.test.cpp +++ b/tests/NonStrictTypeChecker.test.cpp @@ -17,6 +17,8 @@ LUAU_FASTFLAG(LuauNewNonStrictWarnOnUnknownGlobals) LUAU_FASTFLAG(LuauNonStrictVisitorImprovements) +LUAU_FASTFLAG(LuauNonStrictFuncDefErrorFix) +LUAU_FASTFLAG(LuauNormalizedBufferIsNotUnknown) using namespace Luau; @@ -359,6 +361,23 @@ end NONSTRICT_REQUIRE_FUNC_DEFINITION_ERR(Position(1, 11), "x", result); } +TEST_CASE_FIXTURE(NonStrictTypeCheckerFixture, "function_def_sequencing_errors_2") +{ + ScopedFastFlag luauNonStrictFuncDefErrorFix{FFlag::LuauNonStrictFuncDefErrorFix, true}; + ScopedFastFlag luauNonStrictVisitorImprovements{FFlag::LuauNonStrictVisitorImprovements, true}; + + CheckResult result = checkNonStrict(R"( +local t = {function(x) + abs(x) + lower(x) +end} +)"); + 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"); +} + TEST_CASE_FIXTURE(NonStrictTypeCheckerFixture, "local_fn_produces_error") { CheckResult result = checkNonStrict(R"( @@ -649,4 +668,17 @@ TEST_CASE_FIXTURE(Fixture, "unknown_globals_in_non_strict") LUAU_REQUIRE_ERROR_COUNT(2, result); } +TEST_CASE_FIXTURE(BuiltinsFixture, "buffer_is_not_unknown") +{ + ScopedFastFlag luauNormalizedBufferIsNotUnknown{FFlag::LuauNormalizedBufferIsNotUnknown, true}; + + CheckResult result = check(Mode::Nonstrict, R"( +local function wrap(b: buffer, i: number, v: number) + buffer.writeu32(b, i * 4, v) +end + )"); + + LUAU_REQUIRE_NO_ERRORS(result); +} + TEST_SUITE_END(); diff --git a/tests/Normalize.test.cpp b/tests/Normalize.test.cpp index dba2924b..ce45c57b 100644 --- a/tests/Normalize.test.cpp +++ b/tests/Normalize.test.cpp @@ -12,7 +12,6 @@ LUAU_FASTFLAG(LuauSolverV2) LUAU_FASTINT(LuauTypeInferRecursionLimit) -LUAU_FASTFLAG(LuauFixNormalizedIntersectionOfNegatedClass) LUAU_FASTFLAG(LuauNormalizeNegationFix) using namespace Luau; @@ -852,7 +851,6 @@ TEST_CASE_FIXTURE(NormalizeFixture, "crazy_metatable") TEST_CASE_FIXTURE(NormalizeFixture, "negations_of_classes") { - ScopedFastFlag _{FFlag::LuauFixNormalizedIntersectionOfNegatedClass, true}; createSomeClasses(&frontend); CHECK("(Parent & ~Child) | Unrelated" == toString(normal("(Parent & Not) | Unrelated"))); CHECK("((class & ~Child) | boolean | buffer | function | number | string | table | thread)?" == toString(normal("Not"))); diff --git a/tests/Parser.test.cpp b/tests/Parser.test.cpp index 59805c59..2f4e7be2 100644 --- a/tests/Parser.test.cpp +++ b/tests/Parser.test.cpp @@ -23,6 +23,7 @@ LUAU_FASTFLAG(LuauExtendStatEndPosWithSemicolon) LUAU_FASTFLAG(LuauPreserveUnionIntersectionNodeForLeadingTokenSingleType) LUAU_FASTFLAG(LuauAstTypeGroup3) LUAU_FASTFLAG(LuauFixDoBlockEndLocation) +LUAU_FASTFLAG(LuauParseOptionalAsNode) namespace { @@ -3818,7 +3819,10 @@ TEST_CASE_FIXTURE(Fixture, "grouped_function_type") } else CHECK(unionTy->types.data[0]->is()); // () -> () - CHECK(unionTy->types.data[1]->is()); // nil + if (FFlag::LuauParseOptionalAsNode) + CHECK(unionTy->types.data[1]->is()); // ? + else + CHECK(unionTy->types.data[1]->is()); // nil } TEST_CASE_FIXTURE(Fixture, "complex_union_in_generic_ty") diff --git a/tests/RequireTracer.test.cpp b/tests/RequireTracer.test.cpp index eac9f96b..3ad3ffec 100644 --- a/tests/RequireTracer.test.cpp +++ b/tests/RequireTracer.test.cpp @@ -6,8 +6,6 @@ #include "doctest.h" -LUAU_FASTFLAG(LuauExtendedSimpleRequire) - using namespace Luau; namespace @@ -182,8 +180,6 @@ TEST_CASE_FIXTURE(RequireTracerFixture, "follow_string_indexexpr") TEST_CASE_FIXTURE(RequireTracerFixture, "follow_group") { - ScopedFastFlag luauExtendedSimpleRequire{FFlag::LuauExtendedSimpleRequire, true}; - AstStatBlock* block = parse(R"( local R = (((game).Test)) require(R) @@ -200,8 +196,6 @@ TEST_CASE_FIXTURE(RequireTracerFixture, "follow_group") TEST_CASE_FIXTURE(RequireTracerFixture, "follow_type_annotation") { - ScopedFastFlag luauExtendedSimpleRequire{FFlag::LuauExtendedSimpleRequire, true}; - AstStatBlock* block = parse(R"( local R = game.Test :: (typeof(game.Redirect)) require(R) @@ -218,8 +212,6 @@ TEST_CASE_FIXTURE(RequireTracerFixture, "follow_type_annotation") TEST_CASE_FIXTURE(RequireTracerFixture, "follow_type_annotation_2") { - ScopedFastFlag luauExtendedSimpleRequire{FFlag::LuauExtendedSimpleRequire, true}; - AstStatBlock* block = parse(R"( local R = game.Test :: (typeof(game.Redirect)) local N = R.Nested diff --git a/tests/Subtyping.test.cpp b/tests/Subtyping.test.cpp index 76efc835..46bf5b5f 100644 --- a/tests/Subtyping.test.cpp +++ b/tests/Subtyping.test.cpp @@ -15,7 +15,8 @@ #include -LUAU_FASTFLAG(LuauSolverV2); +LUAU_FASTFLAG(LuauSolverV2) +LUAU_FASTFLAG(LuauNormalizedBufferIsNotUnknown) using namespace Luau; @@ -961,6 +962,20 @@ TEST_IS_NOT_SUBTYPE(childClass, negate(rootClass)); TEST_IS_NOT_SUBTYPE(childClass, meet(builtinTypes->classType, negate(rootClass))); TEST_IS_SUBTYPE(anotherChildClass, meet(builtinTypes->classType, negate(childClass))); +// Negated primitives against unknown +TEST_IS_NOT_SUBTYPE(builtinTypes->unknownType, negate(builtinTypes->booleanType)); +TEST_IS_NOT_SUBTYPE(builtinTypes->unknownType, negate(builtinTypes->numberType)); +TEST_IS_NOT_SUBTYPE(builtinTypes->unknownType, negate(builtinTypes->stringType)); +TEST_IS_NOT_SUBTYPE(builtinTypes->unknownType, negate(builtinTypes->threadType)); + +TEST_CASE_FIXTURE(SubtypeFixture, "unknown unknownType, negate(builtinTypes->bufferType)); +} + TEST_CASE_FIXTURE(SubtypeFixture, "Root <: class") { CHECK_IS_SUBTYPE(rootClass, builtinTypes->classType); diff --git a/tests/ToString.test.cpp b/tests/ToString.test.cpp index 536a4081..1a8c3af7 100644 --- a/tests/ToString.test.cpp +++ b/tests/ToString.test.cpp @@ -5,14 +5,16 @@ #include "Fixture.h" +#include "Luau/TypeChecker2.h" #include "ScopedFlags.h" #include "doctest.h" using namespace Luau; -LUAU_FASTFLAG(LuauRecursiveTypeParameterRestriction); -LUAU_FASTFLAG(LuauSolverV2); -LUAU_FASTFLAG(LuauAttributeSyntax); +LUAU_FASTFLAG(LuauRecursiveTypeParameterRestriction) +LUAU_FASTFLAG(LuauSolverV2) +LUAU_FASTFLAG(LuauAttributeSyntax) +LUAU_FASTFLAG(LuauImproveTypePathsInErrors) TEST_SUITE_BEGIN("ToString"); @@ -871,9 +873,28 @@ TEST_CASE_FIXTURE(Fixture, "tostring_error_mismatch") )"); std::string expected; - if (FFlag::LuauSolverV2) + if (FFlag::LuauSolverV2 && FFlag::LuauImproveTypePathsInErrors) + expected = + "Type pack '{ a: number, b: string, c: { d: string } }' could not be converted into '{ a: number, b: string, c: { d: number } }'; \n" + "this is because in the 1st entry in the type pack, accessing `c.d` results in `string` in the former type and `number` in the latter " + "type, and `string` is not exactly `number`"; + else if (FFlag::LuauSolverV2) expected = R"(Type pack '{ a: number, b: string, c: { d: string } }' could not be converted into '{ a: number, b: string, c: { d: number } }'; at [0][read "c"][read "d"], string is not exactly number)"; + else if (FFlag::LuauImproveTypePathsInErrors) + expected = R"(Type + '{ a: number, b: string, c: { d: string } }' +could not be converted into + '{| a: number, b: string, c: {| d: number |} |}' +caused by: + Property 'c' is not compatible. +Type + '{ d: string }' +could not be converted into + '{| d: number |}' +caused by: + Property 'd' is not compatible. +Type 'string' could not be converted into 'number' in an invariant context)"; else expected = R"(Type '{ a: number, b: string, c: { d: string } }' diff --git a/tests/Transpiler.test.cpp b/tests/Transpiler.test.cpp index 397ae378..ba229a1e 100644 --- a/tests/Transpiler.test.cpp +++ b/tests/Transpiler.test.cpp @@ -15,7 +15,6 @@ using namespace Luau; LUAU_FASTFLAG(LuauStoreCSTData) LUAU_FASTFLAG(LuauExtendStatEndPosWithSemicolon) LUAU_FASTFLAG(LuauAstTypeGroup3); -LUAU_FASTFLAG(LexerFixInterpStringStart) TEST_SUITE_BEGIN("TranspilerTests"); @@ -304,6 +303,95 @@ TEST_CASE("function") CHECK_EQ(two, transpile(two).code); } +TEST_CASE("function_spaces_around_tokens") +{ + ScopedFastFlag _{FFlag::LuauStoreCSTData, true}; + const std::string two = R"( function p(o, m, ...) end )"; + CHECK_EQ(two, transpile(two).code); + + const std::string three = R"( function p( o, m, ...) end )"; + CHECK_EQ(three, transpile(three).code); + + const std::string four = R"( function p(o , m, ...) end )"; + CHECK_EQ(four, transpile(four).code); + + const std::string five = R"( function p(o, m, ...) end )"; + CHECK_EQ(five, transpile(five).code); + + const std::string six = R"( function p(o, m , ...) end )"; + CHECK_EQ(six, transpile(six).code); + + const std::string seven = R"( function p(o, m, ...) end )"; + CHECK_EQ(seven, transpile(seven).code); + + const std::string eight = R"( function p(o, m, ... ) end )"; + CHECK_EQ(eight, transpile(eight).code); + + const std::string nine = R"( function p(o, m, ...) end )"; + CHECK_EQ(nine, transpile(nine).code); +} + +TEST_CASE("function_with_types_spaces_around_tokens") +{ + ScopedFastFlag _{FFlag::LuauStoreCSTData, true}; + std::string code = R"( function p(o: string, m: number, ...: any): string end )"; + CHECK_EQ(code, transpile(code, {}, true).code); + + code = R"( function p (o: string, m: number, ...: any): string end )"; + CHECK_EQ(code, transpile(code, {}, true).code); + + code = R"( function p(o: string, m: number, ...: any): string end )"; + CHECK_EQ(code, transpile(code, {}, true).code); + + code = R"( function p(o: string, m: number, ...: any): string end )"; + CHECK_EQ(code, transpile(code, {}, true).code); + + code = R"( function p(o: string, m: number, ...: any): string end )"; + CHECK_EQ(code, transpile(code, {}, true).code); + + code = R"( function p(o: string, m: number, ...: any): string end )"; + CHECK_EQ(code, transpile(code, {}, true).code); + + code = R"( function p(o: string, m: number, ...: any): string end )"; + CHECK_EQ(code, transpile(code, {}, true).code); + + code = R"( function p (o: string, m: number, ...: any): string end )"; + CHECK_EQ(code, transpile(code, {}, true).code); + +// TODO(CLI-139347): re-enable test once colon positions are supported +// code = R"( function p(o : string, m: number, ...: any): string end )"; +// CHECK_EQ(code, transpile(code, {}, true).code); + + code = R"( function p(o: string, m: number, ...: any): string end )"; + CHECK_EQ(code, transpile(code, {}, true).code); + + code = R"( function p(o: string , m: number, ...: any): string end )"; + CHECK_EQ(code, transpile(code, {}, true).code); + + code = R"( function p(o: string, m: number, ...: any): string end )"; + CHECK_EQ(code, transpile(code, {}, true).code); + + code = R"( function p(o: string, m: number, ...: any): string end )"; + CHECK_EQ(code, transpile(code, {}, true).code); + +// TODO(CLI-139347): re-enable test once colon positions are supported +// code = R"( function p(o: string, m: number, ... : any): string end )"; +// CHECK_EQ(code, transpile(code, {}, true).code); + + code = R"( function p(o: string, m: number, ...: any): string end )"; + CHECK_EQ(code, transpile(code, {}, true).code); + + code = R"( function p(o: string, m: number, ...: any ): string end )"; + CHECK_EQ(code, transpile(code, {}, true).code); + +// TODO(CLI-139347): re-enable test once return type positions are supported +// code = R"( function p(o: string, m: number, ...: any) :string end )"; +// CHECK_EQ(code, transpile(code, {}, true).code); + + code = R"( function p(o: string, m: number, ...: any): string end )"; + CHECK_EQ(code, transpile(code, {}, true).code); +} + TEST_CASE("returns_spaces_around_tokens") { ScopedFastFlag _{FFlag::LuauStoreCSTData, true}; @@ -968,7 +1056,17 @@ TEST_CASE_FIXTURE(Fixture, "type_lists_should_be_emitted_correctly") end )"; - std::string expected = R"( + std::string expected = FFlag::LuauStoreCSTData ? R"( + local a:(a:string,b:number,...string)->(string,...number)=function(a:string,b:number,...:string): (string,...number) + end + + local b:(...string)->(...number)=function(...:string): ...number + end + + local c:()->()=function(): () + end + )" + : R"( local a:(string,number,...string)->(string,...number)=function(a:string,b:number,...:string): (string,...number) end @@ -1157,6 +1255,49 @@ local b: Packed<(number, string)> CHECK_EQ(code, transpile(code, {}, true).code); } +TEST_CASE_FIXTURE(Fixture, "type_packs_spaces_around_tokens") +{ + ScopedFastFlag _{FFlag::LuauStoreCSTData, true}; + std::string code = R"( type _ = Packed< T...> )"; + CHECK_EQ(code, transpile(code, {}, true).code); + + code = R"( type _ = Packed )"; + CHECK_EQ(code, transpile(code, {}, true).code); + + code = R"( type _ = Packed< ...T> )"; + CHECK_EQ(code, transpile(code, {}, true).code); + + code = R"( type _ = Packed<... T> )"; + CHECK_EQ(code, transpile(code, {}, true).code); + + code = R"( type _ = Packed< ()> )"; + CHECK_EQ(code, transpile(code, {}, true).code); + + code = R"( type _ = Packed< (string, number)> )"; + CHECK_EQ(code, transpile(code, {}, true).code); + + code = R"( type _ = Packed<( string, number)> )"; + CHECK_EQ(code, transpile(code, {}, true).code); + + code = R"( type _ = Packed<(string , number)> )"; + CHECK_EQ(code, transpile(code, {}, true).code); + + code = R"( type _ = Packed<(string, number)> )"; + CHECK_EQ(code, transpile(code, {}, true).code); + + code = R"( type _ = Packed<(string, number )> )"; + CHECK_EQ(code, transpile(code, {}, true).code); + + code = R"( type _ = Packed<(string, number) > )"; + CHECK_EQ(code, transpile(code, {}, true).code); + + code = R"( type _ = Packed<( )> )"; + CHECK_EQ(code, transpile(code, {}, true).code); + + code = R"( type _ = Packed<() > )"; + CHECK_EQ(code, transpile(code, {}, true).code); +} + TEST_CASE_FIXTURE(Fixture, "transpile_union_type_nested") { std::string code = "local a: ((number)->(string))|((string)->(string))"; @@ -1488,7 +1629,6 @@ TEST_CASE_FIXTURE(Fixture, "transpile_string_interp") { ScopedFastFlag fflags[] = { {FFlag::LuauStoreCSTData, true}, - {FFlag::LexerFixInterpStringStart, true}, }; std::string code = R"( local _ = `hello {name}` )"; @@ -1499,7 +1639,6 @@ TEST_CASE_FIXTURE(Fixture, "transpile_string_interp_multiline") { ScopedFastFlag fflags[] = { {FFlag::LuauStoreCSTData, true}, - {FFlag::LexerFixInterpStringStart, true}, }; std::string code = R"( local _ = `hello { name @@ -1512,7 +1651,6 @@ TEST_CASE_FIXTURE(Fixture, "transpile_string_interp_on_new_line") { ScopedFastFlag fflags[] = { {FFlag::LuauStoreCSTData, true}, - {FFlag::LexerFixInterpStringStart, true}, }; std::string code = R"( error( @@ -1536,7 +1674,6 @@ TEST_CASE_FIXTURE(Fixture, "transpile_string_literal_escape") { ScopedFastFlag fflags[] = { {FFlag::LuauStoreCSTData, true}, - {FFlag::LexerFixInterpStringStart, true}, }; std::string code = R"( local _ = ` bracket = \{, backtick = \` = {'ok'} ` )"; @@ -1550,6 +1687,22 @@ TEST_CASE_FIXTURE(Fixture, "transpile_type_functions") CHECK_EQ(code, transpile(code, {}, true).code); } +TEST_CASE_FIXTURE(Fixture, "transpile_type_functions_spaces_around_tokens") +{ + ScopedFastFlag _{FFlag::LuauStoreCSTData, true}; + std::string code = R"( type function foo() end )"; + CHECK_EQ(code, transpile(code, {}, true).code); + + code = R"( type function foo() end )"; + CHECK_EQ(code, transpile(code, {}, true).code); + + code = R"( type function foo () end )"; + CHECK_EQ(code, transpile(code, {}, true).code); + + code = R"( export type function foo() end )"; + CHECK_EQ(code, transpile(code, {}, true).code); +} + TEST_CASE_FIXTURE(Fixture, "transpile_typeof_spaces_around_tokens") { ScopedFastFlag _{FFlag::LuauStoreCSTData, true}; @@ -1752,4 +1905,122 @@ TEST_CASE("transpile_types_preserve_parentheses_style") CHECK_EQ(code, transpile(code, {}, true).code); } +TEST_CASE("fuzzer_transpile_with_zero_location") +{ + const std::string example = R"( +if _ then +elseif _ then +elseif l0 then +else +local function l0(...):(t0,(any)|(((any)|((""[[[[[[[[[[[[[[[[[[[[[[[[!*t")->()))->())) +end +end +)"; + + Luau::ParseOptions parseOptions; + parseOptions.captureComments = true; + + auto allocator = std::make_unique(); + auto names = std::make_unique(*allocator); + ParseResult parseResult = Parser::parse(example.data(), example.size(), *names, *allocator, parseOptions); + + transpileWithTypes(*parseResult.root); +} + +TEST_CASE("transpile_type_function_unnamed_arguments") +{ + ScopedFastFlag _{FFlag::LuauStoreCSTData, true}; + std::string code = R"( type Foo = () -> () )"; + CHECK_EQ(R"( type Foo = () ->() )", transpile(code, {}, true).code); + + code = R"( type Foo = () -> () )"; + CHECK_EQ(R"( type Foo = () ->() )", transpile(code, {}, true).code); + + code = R"( type Foo = (string) -> () )"; + CHECK_EQ(R"( type Foo = (string) ->() )", transpile(code, {}, true).code); + + code = R"( type Foo = (string, number) -> () )"; + CHECK_EQ(R"( type Foo = (string, number) ->() )", transpile(code, {}, true).code); + + code = R"( type Foo = ( string, number) -> () )"; + CHECK_EQ(R"( type Foo = ( string, number) ->() )", transpile(code, {}, true).code); + + code = R"( type Foo = (string , number) -> () )"; + CHECK_EQ(R"( type Foo = (string , number) ->() )", transpile(code, {}, true).code); + + code = R"( type Foo = (string, number) -> () )"; + CHECK_EQ(R"( type Foo = (string, number) ->() )", transpile(code, {}, true).code); + + code = R"( type Foo = (string, number ) -> () )"; + CHECK_EQ(R"( type Foo = (string, number ) ->() )", transpile(code, {}, true).code); + + code = R"( type Foo = (string, number) -> () )"; + CHECK_EQ(R"( type Foo = (string, number) ->() )", transpile(code, {}, true).code); + + code = R"( type Foo = (string, number) -> () )"; + CHECK_EQ(R"( type Foo = (string, number) ->() )", transpile(code, {}, true).code); +} + +TEST_CASE("transpile_type_function_named_arguments") +{ + ScopedFastFlag _{FFlag::LuauStoreCSTData, true}; + std::string code = R"( type Foo = (x: string) -> () )"; + CHECK_EQ(R"( type Foo = (x: string) ->() )", transpile(code, {}, true).code); + + code = R"( type Foo = (x: string, y: number) -> () )"; + CHECK_EQ(R"( type Foo = (x: string, y: number) ->() )", transpile(code, {}, true).code); + + code = R"( type Foo = ( x: string, y: number) -> () )"; + CHECK_EQ(R"( type Foo = ( x: string, y: number) ->() )", transpile(code, {}, true).code); + + code = R"( type Foo = (x : string, y: number) -> () )"; + CHECK_EQ(R"( type Foo = (x : string, y: number) ->() )", transpile(code, {}, true).code); + + code = R"( type Foo = (x: string, y: number) -> () )"; + CHECK_EQ(R"( type Foo = (x: string, y: number) ->() )", transpile(code, {}, true).code); + + code = R"( type Foo = (x: string, y: number) -> () )"; + CHECK_EQ(R"( type Foo = (x: string, y: number) ->() )", transpile(code, {}, true).code); + + code = R"( type Foo = (number, info: string) -> () )"; + CHECK_EQ(R"( type Foo = (number, info: string) ->() )", transpile(code, {}, true).code); + + code = R"( type Foo = (first: string, second: string, ...string) -> () )"; + CHECK_EQ(R"( type Foo = (first: string, second: string, ...string) ->() )", transpile(code, {}, true).code); +} + +TEST_CASE("transpile_type_function_generics") +{ + ScopedFastFlag _{FFlag::LuauStoreCSTData, true}; + std::string code = R"( type Foo = () -> () )"; + CHECK_EQ(R"( type Foo = () ->() )", transpile(code, {}, true).code); + + code = R"( type Foo = () -> () )"; + CHECK_EQ(R"( type Foo = () ->() )", transpile(code, {}, true).code); + + code = R"( type Foo = < X, Y, Z...>() -> () )"; + CHECK_EQ(R"( type Foo = < X, Y, Z...>() ->() )", transpile(code, {}, true).code); + + code = R"( type Foo = () -> () )"; + CHECK_EQ(R"( type Foo = () ->() )", transpile(code, {}, true).code); + + code = R"( type Foo = () -> () )"; + CHECK_EQ(R"( type Foo = () ->() )", transpile(code, {}, true).code); + + code = R"( type Foo = () -> () )"; + CHECK_EQ(R"( type Foo = () ->() )", transpile(code, {}, true).code); + + code = R"( type Foo = () -> () )"; + CHECK_EQ(R"( type Foo = () ->() )", transpile(code, {}, true).code); + + code = R"( type Foo = () -> () )"; + CHECK_EQ(R"( type Foo = () ->() )", transpile(code, {}, true).code); + + code = R"( type Foo = () -> () )"; + CHECK_EQ(R"( type Foo = () ->() )", transpile(code, {}, true).code); + + code = R"( type Foo = () -> () )"; + CHECK_EQ(R"( type Foo = () ->() )", transpile(code, {}, true).code); +} + TEST_SUITE_END(); diff --git a/tests/TypeFunction.test.cpp b/tests/TypeFunction.test.cpp index 096b3876..2bda60f1 100644 --- a/tests/TypeFunction.test.cpp +++ b/tests/TypeFunction.test.cpp @@ -13,8 +13,11 @@ using namespace Luau; LUAU_FASTFLAG(LuauSolverV2) +LUAU_FASTFLAG(LuauIndexTypeFunctionImprovements) LUAU_DYNAMIC_FASTINT(LuauTypeFamilyApplicationCartesianProductLimit) +LUAU_FASTFLAG(LuauIndexTypeFunctionFunctionMetamethods) LUAU_FASTFLAG(LuauMetatableTypeFunctions) +LUAU_FASTFLAG(LuauIndexAnyIsAny) struct TypeFunctionFixture : Fixture { @@ -904,6 +907,21 @@ end LUAU_REQUIRE_NO_ERRORS(result); } +TEST_CASE_FIXTURE(BuiltinsFixture, "index_of_any_is_any") +{ + if (!FFlag::LuauSolverV2) + return; + + ScopedFastFlag sff{FFlag::LuauIndexAnyIsAny, true}; + + CheckResult result = check(R"( + type T = index + )"); + + LUAU_REQUIRE_NO_ERRORS(result); + CHECK(toString(requireTypeAlias("T")) == "any"); +} + TEST_CASE_FIXTURE(BuiltinsFixture, "index_type_function_works") { if (!FFlag::LuauSolverV2) @@ -965,6 +983,31 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "index_type_function_works_w_array") LUAU_REQUIRE_NO_ERRORS(result); } +TEST_CASE_FIXTURE(BuiltinsFixture, "cyclic_metatable_should_not_crash_index") +{ + if (!FFlag::LuauSolverV2) + return; + + ScopedFastFlag sff{FFlag::LuauIndexTypeFunctionImprovements, true}; + + // t :: t1 where t1 = {metatable {__index: t1, __tostring: (t1) -> string}} + CheckResult result = check(R"( + local mt = {} + local t = setmetatable({}, mt) + mt.__index = t + + function mt:__tostring() + return t.p + end + + type IndexFromT = index + )"); + + LUAU_REQUIRE_ERROR_COUNT(2, result); + CHECK_EQ("Type 't' does not have key 'p'", toString(result.errors[0])); + CHECK_EQ("Property '\"p\"' does not exist on type 't'", toString(result.errors[1])); +} + TEST_CASE_FIXTURE(BuiltinsFixture, "index_type_function_works_w_generic_types") { if (!FFlag::LuauSolverV2) @@ -1003,6 +1046,61 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "index_type_function_errors_w_bad_indexer") CHECK(toString(result.errors[1]) == "Property 'boolean' does not exist on type 'MyObject'"); } +TEST_CASE_FIXTURE(BuiltinsFixture, "index_type_function_works_on_function_metamethods") +{ + if (!FFlag::LuauSolverV2) + return; + + ScopedFastFlag sff[] + { + {FFlag::LuauIndexTypeFunctionFunctionMetamethods, true}, + {FFlag::LuauIndexTypeFunctionImprovements, true}, + }; + + CheckResult result = check(R"( + type Foo = {x: string} + local t = {} + setmetatable(t, { + __index = function(x: string): Foo + return {x = x} + end + }) + + type Bar = index + )"); + + LUAU_REQUIRE_NO_ERRORS(result); + + + CHECK_EQ(toString(requireTypeAlias("Bar"), {true}), "{ x: string }"); +} + +TEST_CASE_FIXTURE(BuiltinsFixture, "index_type_function_works_on_function_metamethods2") +{ + if (!FFlag::LuauSolverV2) + return; + + ScopedFastFlag sff[] + { + {FFlag::LuauIndexTypeFunctionFunctionMetamethods, true}, + {FFlag::LuauIndexTypeFunctionImprovements, true}, + }; + + CheckResult result = check(R"( + type Foo = {x: string} + local t = {} + setmetatable(t, { + __index = function(x: string): Foo + return {x = x} + end + }) + + type Bar = index + )"); + + LUAU_REQUIRE_ERROR_COUNT(1, result); +} + TEST_CASE_FIXTURE(BuiltinsFixture, "index_type_function_errors_w_var_indexer") { if (!FFlag::LuauSolverV2) diff --git a/tests/TypeFunction.user.test.cpp b/tests/TypeFunction.user.test.cpp index 12c9ece2..9a4a7cd8 100644 --- a/tests/TypeFunction.user.test.cpp +++ b/tests/TypeFunction.user.test.cpp @@ -7,13 +7,12 @@ using namespace Luau; -LUAU_FASTFLAG(LuauTypeFunFixHydratedClasses) LUAU_FASTFLAG(LuauSolverV2) LUAU_FASTFLAG(DebugLuauEqSatSimplification) -LUAU_FASTFLAG(LuauTypeFunSingletonEquality) -LUAU_FASTFLAG(LuauUserTypeFunTypeofReturnsType) LUAU_FASTFLAG(LuauTypeFunReadWriteParents) LUAU_FASTFLAG(LuauTypeFunPrintFix) +LUAU_FASTFLAG(LuauImproveTypePathsInErrors) +LUAU_FASTFLAG(LuauUserTypeFunTypecheck) TEST_SUITE_BEGIN("UserDefinedTypeFunctionTests"); @@ -613,9 +612,12 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "udtf_function_methods_work") ty:setreturns(nil, types.boolean) -- (string, number) -> (...boolean) if ty:is("function") then -- creating a copy of `ty` parameters - local arr = {} - for index, val in ty:parameters().head do - table.insert(arr, val) + local arr: {type} = {} + local args = ty:parameters().head + if args then + for index, val in args do + table.insert(arr, val) + end end return types.newfunction({head = arr}, ty:returns()) -- (string, number) -> (...boolean) end @@ -648,7 +650,6 @@ TEST_CASE_FIXTURE(ClassFixture, "udtf_class_serialization_works") TEST_CASE_FIXTURE(ClassFixture, "udtf_class_serialization_works2") { ScopedFastFlag newSolver{FFlag::LuauSolverV2, true}; - ScopedFastFlag luauTypeFunFixHydratedClasses{FFlag::LuauTypeFunFixHydratedClasses, true}; CheckResult result = check(R"( type function serialize_class(arg) @@ -719,7 +720,7 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "udtf_check_mutability") readresult = types.boolean, writeresult = types.boolean, } - local ty = types.newtable(props, indexer, nil) -- {[number]: boolean} + local ty = types.newtable(nil, indexer, nil) -- {[number]: boolean} ty:setproperty(types.singleton("string"), types.number) -- {string: number, [number]: boolean} local metatbl = types.newtable(nil, nil, ty) -- { { }, @metatable { [number]: boolean, string: number } } -- mutate the table @@ -894,6 +895,7 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "udtf_type_overrides_eq_metamethod") if p1 == p2 and t1 == t2 then return types.number end + return types.unknown end local function ok(idx: hello<>): number return idx end )"); @@ -988,8 +990,37 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "udtf_calling_each_other_3") end )"); - LUAU_REQUIRE_ERROR_COUNT(4, result); - CHECK(toString(result.errors[0]) == R"('third' type function errored at runtime: [string "first"]:4: attempt to call a nil value)"); + if (FFlag::LuauUserTypeFunTypecheck) + { + LUAU_REQUIRE_ERROR_COUNT(5, result); + CHECK(toString(result.errors[0]) == R"(Unknown global 'fourth')"); + CHECK(toString(result.errors[1]) == R"('third' type function errored at runtime: [string "first"]:4: attempt to call a nil value)"); + } + else + { + LUAU_REQUIRE_ERROR_COUNT(4, result); + CHECK(toString(result.errors[0]) == R"('third' type function errored at runtime: [string "first"]:4: attempt to call a nil value)"); + } +} + +TEST_CASE_FIXTURE(BuiltinsFixture, "udtf_calling_each_other_unordered") +{ + ScopedFastFlag newSolver{FFlag::LuauSolverV2, true}; + + CheckResult result = check(R"( + type function bar() + return types.singleton(foo()) + end + type function foo() + return "hi" + end + local function ok(idx: bar<>): nil return idx end + )"); + + LUAU_REQUIRE_ERROR_COUNT(1, result); + TypePackMismatch* tpm = get(result.errors[0]); + REQUIRE(tpm); + CHECK(toString(tpm->givenTp) == "\"hi\""); } TEST_CASE_FIXTURE(BuiltinsFixture, "udtf_no_shared_state") @@ -1013,10 +1044,21 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "udtf_no_shared_state") local function ok2(idx: bar<'y'>): nil return idx end )"); - // We are only checking first errors, others are mostly duplicates - LUAU_REQUIRE_ERROR_COUNT(8, result); - CHECK(toString(result.errors[0]) == R"('bar' type function errored at runtime: [string "foo"]:4: attempt to modify a readonly table)"); - CHECK(toString(result.errors[1]) == R"(Type function instance bar<"x"> is uninhabited)"); + if (FFlag::LuauUserTypeFunTypecheck) + { + // We are only checking first errors, others are mostly duplicates + LUAU_REQUIRE_ERROR_COUNT(9, result); + CHECK(toString(result.errors[0]) == R"(Unknown global 'glob')"); + CHECK(toString(result.errors[1]) == R"('bar' type function errored at runtime: [string "foo"]:4: attempt to modify a readonly table)"); + CHECK(toString(result.errors[2]) == R"(Type function instance bar<"x"> is uninhabited)"); + } + else + { + // We are only checking first errors, others are mostly duplicates + LUAU_REQUIRE_ERROR_COUNT(8, result); + CHECK(toString(result.errors[0]) == R"('bar' type function errored at runtime: [string "foo"]:4: attempt to modify a readonly table)"); + CHECK(toString(result.errors[1]) == R"(Type function instance bar<"x"> is uninhabited)"); + } } TEST_CASE_FIXTURE(BuiltinsFixture, "udtf_math_reset") @@ -1075,10 +1117,23 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "udtf_calling_illegal_global") local function ok(idx: illegal): nil return idx end )"); - LUAU_REQUIRE_ERROR_COUNT(4, result); // There are 2 type function uninhabited error, 2 user defined type function error - UserDefinedTypeFunctionError* e = get(result.errors[0]); - REQUIRE(e); - CHECK(e->message == "'illegal' type function errored at runtime: [string \"illegal\"]:3: this function is not supported in type functions"); + if (FFlag::LuauUserTypeFunTypecheck) + { + // We are only checking first errors, others are mostly duplicates + LUAU_REQUIRE_ERROR_COUNT(5, result); + CHECK(toString(result.errors[0]) == R"(Unknown global 'gcinfo')"); + CHECK( + toString(result.errors[1]) == + R"('illegal' type function errored at runtime: [string "illegal"]:3: this function is not supported in type functions)" + ); + } + else + { + LUAU_REQUIRE_ERROR_COUNT(4, result); // There are 2 type function uninhabited error, 2 user defined type function error + UserDefinedTypeFunctionError* e = get(result.errors[0]); + REQUIRE(e); + CHECK(e->message == "'illegal' type function errored at runtime: [string \"illegal\"]:3: this function is not supported in type functions"); + } } TEST_CASE_FIXTURE(BuiltinsFixture, "udtf_recursion_and_gc") @@ -1172,7 +1227,7 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "no_type_methods_on_types") CheckResult result = check(R"( type function test(x) - return if types.is(x, "number") then types.string else types.boolean + return if (types :: any).is(x, "number") then types.string else types.boolean end local function ok(tbl: test): never return tbl end )"); @@ -1243,9 +1298,36 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "tag_field") )"); LUAU_REQUIRE_ERROR_COUNT(3, result); - CHECK(toString(result.errors[0]) == R"(Type pack '"number"' could not be converted into 'never'; at [0], "number" is not a subtype of never)"); - CHECK(toString(result.errors[1]) == R"(Type pack '"string"' could not be converted into 'never'; at [0], "string" is not a subtype of never)"); - CHECK(toString(result.errors[2]) == R"(Type pack '"table"' could not be converted into 'never'; at [0], "table" is not a subtype of never)"); + + if (FFlag::LuauImproveTypePathsInErrors) + { + + CHECK( + toString(result.errors[0]) == + "Type pack '\"number\"' could not be converted into 'never'; \n" + R"(this is because the 1st entry in the type pack is `"number"` in the former type and `never` in the latter type, and `"number"` is not a subtype of `never`)" + ); + CHECK( + toString(result.errors[1]) == + "Type pack '\"string\"' could not be converted into 'never'; \n" + R"(this is because the 1st entry in the type pack is `"string"` in the former type and `never` in the latter type, and `"string"` is not a subtype of `never`)" + ); + CHECK( + toString(result.errors[2]) == + "Type pack '\"table\"' could not be converted into 'never'; \n" + R"(this is because the 1st entry in the type pack is `"table"` in the former type and `never` in the latter type, and `"table"` is not a subtype of `never`)" + ); + } + else + { + CHECK( + toString(result.errors[0]) == R"(Type pack '"number"' could not be converted into 'never'; at [0], "number" is not a subtype of never)" + ); + CHECK( + toString(result.errors[1]) == R"(Type pack '"string"' could not be converted into 'never'; at [0], "string" is not a subtype of never)" + ); + CHECK(toString(result.errors[2]) == R"(Type pack '"table"' could not be converted into 'never'; at [0], "table" is not a subtype of never)"); + } } TEST_CASE_FIXTURE(BuiltinsFixture, "metatable_serialization") @@ -1293,8 +1375,12 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "implicit_export") ScopedFastFlag newSolver{FFlag::LuauSolverV2, true}; fileResolver.source["game/A"] = R"( -type function concat(a, b) - return types.singleton(a:value() .. b:value()) +type function concat(a: type, b: type) + local as = a:value() + local bs = b:value() + assert(typeof(as) == "string") + assert(typeof(bs) == "string") + return types.singleton(as .. bs) end export type Concat = concat local a: concat<'first', 'second'> @@ -1342,8 +1428,12 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "explicit_export") ScopedFastFlag newSolver{FFlag::LuauSolverV2, true}; fileResolver.source["game/A"] = R"( -export type function concat(a, b) - return types.singleton(a:value() .. b:value()) +export type function concat(a: type, b: type) + local as = a:value() + local bs = b:value() + assert(typeof(as) == "string") + assert(typeof(bs) == "string") + return types.singleton(as .. bs) end local a: concat<'first', 'second'> return {} @@ -1892,7 +1982,6 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "udtf_eqsat_opaque") TEST_CASE_FIXTURE(BuiltinsFixture, "udtf_singleton_equality_bool") { ScopedFastFlag newSolver{FFlag::LuauSolverV2, true}; - ScopedFastFlag luauTypeFunSingletonEquality{FFlag::LuauTypeFunSingletonEquality, true}; CheckResult result = check(R"( type function compare(arg) @@ -1909,7 +1998,6 @@ local function ok(idx: compare): false return idx end TEST_CASE_FIXTURE(BuiltinsFixture, "udtf_singleton_equality_string") { ScopedFastFlag newSolver{FFlag::LuauSolverV2, true}; - ScopedFastFlag luauTypeFunSingletonEquality{FFlag::LuauTypeFunSingletonEquality, true}; CheckResult result = check(R"( type function compare(arg) @@ -1926,7 +2014,6 @@ local function ok(idx: compare<"a">): false return idx end TEST_CASE_FIXTURE(BuiltinsFixture, "typeof_type_userdata_returns_type") { ScopedFastFlag solverV2{FFlag::LuauSolverV2, true}; - ScopedFastFlag luauUserTypeFunTypeofReturnsType{FFlag::LuauUserTypeFunTypeofReturnsType, true}; CheckResult result = check(R"( type function test(t) @@ -1982,4 +2069,38 @@ TEST_CASE_FIXTURE(ClassFixture, "udtf_class_parent_ops") LUAU_REQUIRE_NO_ERRORS(result); } +TEST_CASE_FIXTURE(BuiltinsFixture, "typecheck_success") +{ + // Needs new global initialization in the Fixture, but can't place the flag inside the base Fixture + if (!FFlag::LuauUserTypeFunTypecheck) + return; + + ScopedFastFlag newSolver{FFlag::LuauSolverV2, true}; + + CheckResult result = check(R"( +type function foo(x: type) + return types.singleton(x.tag) +end + )"); + + LUAU_REQUIRE_NO_ERRORS(result); +} + +TEST_CASE_FIXTURE(BuiltinsFixture, "typecheck_failure") +{ + // Needs new global initialization in the Fixture, but can't place the flag inside the base Fixture + if (!FFlag::LuauUserTypeFunTypecheck) + return; + + ScopedFastFlag newSolver{FFlag::LuauSolverV2, true}; + + CheckResult result = check(R"( +type function foo() + return types.singleton({1}) +end + )"); + + LUAU_REQUIRE_ERROR_COUNT(1, result); +} + TEST_SUITE_END(); diff --git a/tests/TypeInfer.aliases.test.cpp b/tests/TypeInfer.aliases.test.cpp index 3972fd6b..ca2da4e3 100644 --- a/tests/TypeInfer.aliases.test.cpp +++ b/tests/TypeInfer.aliases.test.cpp @@ -11,6 +11,7 @@ using namespace Luau; LUAU_FASTFLAG(LuauSolverV2) LUAU_FASTFLAG(LuauFixInfiniteRecursionInNormalization) +LUAU_FASTFLAG(LuauImproveTypePathsInErrors) TEST_SUITE_BEGIN("TypeAliases"); @@ -216,7 +217,11 @@ TEST_CASE_FIXTURE(Fixture, "generic_aliases") )"); LUAU_REQUIRE_ERROR_COUNT(1, result); - const std::string expected = R"(Type '{ v: string }' could not be converted into 'T'; at [read "v"], string is not exactly number)"; + const std::string expected = (FFlag::LuauImproveTypePathsInErrors) + ? "Type '{ v: string }' could not be converted into 'T'; \n" + "this is because accessing `v` results in `string` in the former type and `number` in the latter type, and " + "`string` is not exactly `number`" + : R"(Type '{ v: string }' could not be converted into 'T'; at [read "v"], string is not exactly number)"; CHECK(result.errors[0].location == Location{{4, 31}, {4, 44}}); CHECK_EQ(expected, toString(result.errors[0])); } @@ -236,7 +241,11 @@ TEST_CASE_FIXTURE(Fixture, "dependent_generic_aliases") LUAU_REQUIRE_ERROR_COUNT(1, result); const std::string expected = - R"(Type '{ t: { v: string } }' could not be converted into 'U'; at [read "t"][read "v"], string is not exactly number)"; + (FFlag::LuauImproveTypePathsInErrors) + ? "Type '{ t: { v: string } }' could not be converted into 'U'; \n" + "this is because accessing `t.v` results in `string` in the former type and `number` in the latter type, and `string` is not exactly " + "`number`" + : R"(Type '{ t: { v: string } }' could not be converted into 'U'; at [read "t"][read "v"], string is not exactly number)"; CHECK(result.errors[0].location == Location{{4, 31}, {4, 52}}); CHECK_EQ(expected, toString(result.errors[0])); diff --git a/tests/TypeInfer.builtins.test.cpp b/tests/TypeInfer.builtins.test.cpp index 96443aeb..e86a7271 100644 --- a/tests/TypeInfer.builtins.test.cpp +++ b/tests/TypeInfer.builtins.test.cpp @@ -12,7 +12,7 @@ using namespace Luau; LUAU_FASTFLAG(LuauSolverV2) LUAU_FASTFLAG(LuauTableCloneClonesType3) LUAU_FASTFLAG(LuauStringFormatErrorSuppression) -LUAU_FASTFLAG(LuauFreezeIgnorePersistent) +LUAU_FASTFLAG(LuauImproveTypePathsInErrors) TEST_SUITE_BEGIN("BuiltinTests"); @@ -146,7 +146,20 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "sort_with_bad_predicate") )"); LUAU_REQUIRE_ERROR_COUNT(1, result); - const std::string expected = R"(Type + const std::string expected = (FFlag::LuauImproveTypePathsInErrors) ? "Type\n\t" + "'(number, number) -> boolean'" + "\ncould not be converted into\n\t" + "'((string, string) -> boolean)?'" + "\ncaused by:\n" + " None of the union options are compatible. For example:\n" + "Type\n\t" + "'(number, number) -> boolean'" + "\ncould not be converted into\n\t" + "'(string, string) -> boolean'" + "\ncaused by:\n" + " Argument #1 type is not compatible.\n" + "Type 'string' could not be converted into 'number'" + : R"(Type '(number, number) -> boolean' could not be converted into '((string, string) -> boolean)?' @@ -985,7 +998,14 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "tonumber_returns_optional_number_type") )"); LUAU_REQUIRE_ERROR_COUNT(1, result); - if (FFlag::LuauSolverV2) + + if (FFlag::LuauSolverV2 && FFlag::LuauImproveTypePathsInErrors) + CHECK_EQ( + "Type 'number?' could not be converted into 'number'; \n" + "this is because the 2nd component of the union is `nil`, which is not a subtype of `number`", + toString(result.errors[0]) + ); + else if (FFlag::LuauSolverV2) CHECK_EQ( "Type 'number?' could not be converted into 'number'; type number?[1] (nil) is not a subtype of number (number)", toString(result.errors[0]) @@ -1256,8 +1276,6 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "table_freeze_errors_on_non_tables") TEST_CASE_FIXTURE(BuiltinsFixture, "table_freeze_persistent_skip") { - ScopedFastFlag luauFreezeIgnorePersistent{FFlag::LuauFreezeIgnorePersistent, true}; - CheckResult result = check(R"( table.freeze(table) )"); @@ -1267,8 +1285,6 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "table_freeze_persistent_skip") TEST_CASE_FIXTURE(BuiltinsFixture, "table_clone_persistent_skip") { - ScopedFastFlag luauFreezeIgnorePersistent{FFlag::LuauFreezeIgnorePersistent, true}; - CheckResult result = check(R"( table.clone(table) )"); diff --git a/tests/TypeInfer.classes.test.cpp b/tests/TypeInfer.classes.test.cpp index 53f1396d..0986575b 100644 --- a/tests/TypeInfer.classes.test.cpp +++ b/tests/TypeInfer.classes.test.cpp @@ -13,7 +13,8 @@ using namespace Luau; using std::nullopt; -LUAU_FASTFLAG(LuauSolverV2); +LUAU_FASTFLAG(LuauSolverV2) +LUAU_FASTFLAG(LuauImproveTypePathsInErrors) TEST_SUITE_BEGIN("TypeInferClasses"); @@ -545,7 +546,13 @@ local b: B = a LUAU_REQUIRE_ERRORS(result); - if (FFlag::LuauSolverV2) + if (FFlag::LuauSolverV2 && FFlag::LuauImproveTypePathsInErrors) + CHECK( + "Type 'A' could not be converted into 'B'; \n" + "this is because accessing `x` results in `ChildClass` in the former type and `BaseClass` in the latter type, and `ChildClass` is not " + "exactly `BaseClass`" == toString(result.errors.at(0)) + ); + else if (FFlag::LuauSolverV2) CHECK(toString(result.errors.at(0)) == "Type 'A' could not be converted into 'B'; at [read \"x\"], ChildClass is not exactly BaseClass"); else { diff --git a/tests/TypeInfer.definitions.test.cpp b/tests/TypeInfer.definitions.test.cpp index fb200533..a13de0f0 100644 --- a/tests/TypeInfer.definitions.test.cpp +++ b/tests/TypeInfer.definitions.test.cpp @@ -9,9 +9,7 @@ using namespace Luau; -LUAU_FASTFLAG(LuauClipNestedAndRecursiveUnion) LUAU_FASTINT(LuauTypeInferRecursionLimit) -LUAU_FASTFLAG(LuauPreventReentrantTypeFunctionReduction) LUAU_FASTFLAG(LuauDontForgetToReduceUnionFunc) TEST_SUITE_BEGIN("DefinitionTests"); @@ -545,8 +543,6 @@ TEST_CASE_FIXTURE(Fixture, "definition_file_has_source_module_name_set") TEST_CASE_FIXTURE(Fixture, "recursive_redefinition_reduces_rightfully") { - ScopedFastFlag _{FFlag::LuauClipNestedAndRecursiveUnion, true}; - LUAU_REQUIRE_NO_ERRORS(check(R"( local t: {[string]: string} = {} @@ -592,7 +588,6 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "cli_142285_reduce_minted_union_func") TEST_CASE_FIXTURE(Fixture, "vector3_overflow") { - ScopedFastFlag _{FFlag::LuauPreventReentrantTypeFunctionReduction, true}; // We set this to zero to ensure that we either run to completion or stack overflow here. ScopedFastInt sfi{FInt::LuauTypeInferRecursionLimit, 0}; diff --git a/tests/TypeInfer.functions.test.cpp b/tests/TypeInfer.functions.test.cpp index f13524fb..58815ac0 100644 --- a/tests/TypeInfer.functions.test.cpp +++ b/tests/TypeInfer.functions.test.cpp @@ -22,8 +22,8 @@ LUAU_FASTFLAG(LuauInstantiateInSubtyping) LUAU_FASTFLAG(LuauSolverV2) LUAU_FASTINT(LuauTarjanChildLimit) LUAU_FASTFLAG(DebugLuauEqSatSimplification) -LUAU_FASTFLAG(LuauSubtypingFixTailPack) LUAU_FASTFLAG(LuauUngeneralizedTypesForRecursiveFunctions) +LUAU_FASTFLAG(LuauImproveTypePathsInErrors) TEST_SUITE_BEGIN("TypeInferFunctions"); @@ -1306,7 +1306,16 @@ f(function(a, b, c, ...) return a + b end) LUAU_REQUIRE_ERRORS(result); std::string expected; - if (FFlag::LuauInstantiateInSubtyping) + if (FFlag::LuauInstantiateInSubtyping && FFlag::LuauImproveTypePathsInErrors) + { + expected = "Type\n\t" + "'(number, number, a) -> number'" + "\ncould not be converted into\n\t" + "'(number, number) -> number'" + "\ncaused by:\n" + " Argument count mismatch. Function expects 3 arguments, but only 2 are specified"; + } + else if (FFlag::LuauInstantiateInSubtyping) { expected = R"(Type '(number, number, a) -> number' @@ -1315,6 +1324,15 @@ could not be converted into caused by: Argument count mismatch. Function expects 3 arguments, but only 2 are specified)"; } + else if (FFlag::LuauImproveTypePathsInErrors) + { + expected = "Type\n\t" + "'(number, number, *error-type*) -> number'" + "\ncould not be converted into\n\t" + "'(number, number) -> number'" + "\ncaused by:\n" + " Argument count mismatch. Function expects 3 arguments, but only 2 are specified"; + } else { expected = R"(Type @@ -1519,7 +1537,14 @@ local b: B = a )"); LUAU_REQUIRE_ERROR_COUNT(1, result); - const std::string expected = R"(Type + const std::string expected = (FFlag::LuauImproveTypePathsInErrors) + ? "Type\n\t" + "'(number, number) -> string'" + "\ncould not be converted into\n\t" + "'(number) -> string'" + "\ncaused by:\n" + " Argument count mismatch. Function expects 2 arguments, but only 1 is specified" + : R"(Type '(number, number) -> string' could not be converted into '(number) -> string' @@ -1542,7 +1567,14 @@ local b: B = a )"); LUAU_REQUIRE_ERROR_COUNT(1, result); - const std::string expected = R"(Type + const std::string expected = (FFlag::LuauImproveTypePathsInErrors) ? "Type\n\t" + "'(number, number) -> string'" + "\ncould not be converted into\n\t" + "'(number, string) -> string'" + "\ncaused by:\n" + " Argument #2 type is not compatible.\n" + "Type 'string' could not be converted into 'number'" + : R"(Type '(number, number) -> string' could not be converted into '(number, string) -> string' @@ -1566,7 +1598,13 @@ local b: B = a )"); LUAU_REQUIRE_ERROR_COUNT(1, result); - const std::string expected = R"(Type + const std::string expected = (FFlag::LuauImproveTypePathsInErrors) ? "Type\n\t" + "'(number, number) -> number'" + "\ncould not be converted into\n\t" + "'(number, number) -> (number, boolean)'" + "\ncaused by:\n" + " Function only returns 1 value, but 2 are required here" + : R"(Type '(number, number) -> number' could not be converted into '(number, number) -> (number, boolean)' @@ -1589,7 +1627,14 @@ local b: B = a )"); LUAU_REQUIRE_ERROR_COUNT(1, result); - const std::string expected = R"(Type + const std::string expected = (FFlag::LuauImproveTypePathsInErrors) ? "Type\n\t" + "'(number, number) -> string'" + "\ncould not be converted into\n\t" + "'(number, number) -> number'" + "\ncaused by:\n" + " Return type is not compatible.\n" + "Type 'string' could not be converted into 'number'" + : R"(Type '(number, number) -> string' could not be converted into '(number, number) -> number' @@ -1613,7 +1658,14 @@ local b: B = a )"); LUAU_REQUIRE_ERROR_COUNT(1, result); - const std::string expected = R"(Type + const std::string expected = (FFlag::LuauImproveTypePathsInErrors) ? "Type\n\t" + "'(number, number) -> (number, string)'" + "\ncould not be converted into\n\t" + "'(number, number) -> (number, boolean)'" + "\ncaused by:\n" + " Return #2 type is not compatible.\n" + "Type 'string' could not be converted into 'boolean'" + : R"(Type '(number, number) -> (number, string)' could not be converted into '(number, number) -> (number, boolean)' @@ -1769,6 +1821,24 @@ end R"(Type function instance add depends on generic function parameters but does not appear in the function signature; this construct cannot be type-checked at this time)" ); } + else if (FFlag::LuauImproveTypePathsInErrors) + { + LUAU_REQUIRE_ERROR_COUNT(2, result); + CHECK_EQ(toString(result.errors[0]), R"(Type + '(string) -> string' +could not be converted into + '((number) -> number)?' +caused by: + None of the union options are compatible. For example: +Type + '(string) -> string' +could not be converted into + '(number) -> number' +caused by: + Argument #1 type is not compatible. +Type 'number' could not be converted into 'string')"); + CHECK_EQ(toString(result.errors[1]), R"(Type 'string' could not be converted into 'number')"); + } else { LUAU_REQUIRE_ERROR_COUNT(2, result); @@ -1812,15 +1882,31 @@ function t:b() return 2 end -- not OK )"); LUAU_REQUIRE_ERROR_COUNT(1, result); - CHECK_EQ( - R"(Type + + if (FFlag::LuauImproveTypePathsInErrors) + { + CHECK_EQ( + "Type\n\t" + "'(*error-type*) -> number'" + "\ncould not be converted into\n\t" + "'() -> number'\n" + "caused by:\n" + " Argument count mismatch. Function expects 1 argument, but none are specified", + toString(result.errors[0]) + ); + } + else + { + CHECK_EQ( + R"(Type '(*error-type*) -> number' could not be converted into '() -> number' caused by: Argument count mismatch. Function expects 1 argument, but none are specified)", - toString(result.errors[0]) - ); + toString(result.errors[0]) + ); + } } TEST_CASE_FIXTURE(Fixture, "too_few_arguments_variadic") @@ -2078,7 +2164,13 @@ z = y -- Not OK, so the line is colorable )"); LUAU_REQUIRE_ERROR_COUNT(1, result); - const std::string expected = R"(Type + const std::string expected = + (FFlag::LuauImproveTypePathsInErrors) + ? "Type\n\t" + R"('(("blue" | "red") -> ("blue" | "red") -> ("blue" | "red") -> boolean) & (("blue" | "red") -> ("blue") -> ("blue") -> false) & (("blue" | "red") -> ("red") -> ("red") -> false) & (("blue") -> ("blue") -> ("blue" | "red") -> false) & (("red") -> ("red") -> ("blue" | "red") -> false)')" + "\ncould not be converted into\n\t" + R"('("blue" | "red") -> ("blue" | "red") -> ("blue" | "red") -> false'; none of the intersection parts are compatible)" + : R"(Type '(("blue" | "red") -> ("blue" | "red") -> ("blue" | "red") -> boolean) & (("blue" | "red") -> ("blue") -> ("blue") -> false) & (("blue" | "red") -> ("red") -> ("red") -> false) & (("blue") -> ("blue") -> ("blue" | "red") -> false) & (("red") -> ("red") -> ("blue" | "red") -> false)' could not be converted into '("blue" | "red") -> ("blue" | "red") -> ("blue" | "red") -> false'; none of the intersection parts are compatible)"; @@ -2412,7 +2504,14 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "num_is_solved_before_num_or_str") )"); LUAU_REQUIRE_ERROR_COUNT(1, result); - if (FFlag::LuauSolverV2) + + if (FFlag::LuauSolverV2 && FFlag::LuauImproveTypePathsInErrors) + CHECK( + "Type pack 'string' could not be converted into 'number'; \n" + "this is because the 1st entry in the type pack is `string` in the former type and `number` in the latter type, and `string` is not a " + "subtype of `number`" == toString(result.errors.at(0)) + ); + else if (FFlag::LuauSolverV2) CHECK(toString(result.errors.at(0)) == "Type pack 'string' could not be converted into 'number'; at [0], string is not a subtype of number"); else CHECK_EQ("Type 'string' could not be converted into 'number'", toString(result.errors[0])); @@ -2437,7 +2536,13 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "num_is_solved_after_num_or_str") )"); LUAU_REQUIRE_ERROR_COUNT(1, result); - if (FFlag::LuauSolverV2) + if (FFlag::LuauSolverV2 && FFlag::LuauImproveTypePathsInErrors) + CHECK( + "Type pack 'string' could not be converted into 'number'; \n" + "this is because the 1st entry in the type pack is `string` in the former type and `number` in the latter type, and `string` is not a " + "subtype of `number`" == toString(result.errors.at(0)) + ); + else if (FFlag::LuauSolverV2) CHECK(toString(result.errors.at(0)) == "Type pack 'string' could not be converted into 'number'; at [0], string is not a subtype of number"); else CHECK_EQ("Type 'string' could not be converted into 'number'", toString(result.errors[0])); @@ -3033,8 +3138,6 @@ TEST_CASE_FIXTURE(Fixture, "hidden_variadics_should_not_break_subtyping") TEST_CASE_FIXTURE(BuiltinsFixture, "coroutine_wrap_result_call") { - ScopedFastFlag luauSubtypingFixTailPack{FFlag::LuauSubtypingFixTailPack, true}; - CheckResult result = check(R"( function foo(a, b) coroutine.wrap(a)(b) diff --git a/tests/TypeInfer.generics.test.cpp b/tests/TypeInfer.generics.test.cpp index 0a53836b..c61f689a 100644 --- a/tests/TypeInfer.generics.test.cpp +++ b/tests/TypeInfer.generics.test.cpp @@ -10,9 +10,10 @@ #include "ScopedFlags.h" #include "doctest.h" -LUAU_FASTFLAG(LuauInstantiateInSubtyping); -LUAU_FASTFLAG(LuauSolverV2); +LUAU_FASTFLAG(LuauInstantiateInSubtyping) +LUAU_FASTFLAG(LuauSolverV2) LUAU_FASTFLAG(LuauDeferBidirectionalInferenceForTableAssignment) +LUAU_FASTFLAG(LuauImproveTypePathsInErrors) using namespace Luau; @@ -875,7 +876,13 @@ y.a.c = y CHECK(mismatch); CHECK_EQ(toString(mismatch->givenType), "{ a: { c: T?, d: number }, b: number }"); CHECK_EQ(toString(mismatch->wantedType), "T"); - std::string reason = "at [read \"a\"][read \"d\"], number is not exactly string\n\tat [read \"b\"], number is not exactly string"; + std::string reason = + (FFlag::LuauImproveTypePathsInErrors) + ? "\nthis is because \n\t" + " * accessing `a.d` results in `number` in the former type and `string` in the latter type, and `number` is not exactly " + "`string`\n\t" + " * accessing `b` results in `number` in the former type and `string` in the latter type, and `number` is not exactly `string`" + : "at [read \"a\"][read \"d\"], number is not exactly string\n\tat [read \"b\"], number is not exactly string"; CHECK_EQ(mismatch->reason, reason); } else diff --git a/tests/TypeInfer.intersectionTypes.test.cpp b/tests/TypeInfer.intersectionTypes.test.cpp index ca92083b..de5745dd 100644 --- a/tests/TypeInfer.intersectionTypes.test.cpp +++ b/tests/TypeInfer.intersectionTypes.test.cpp @@ -9,7 +9,8 @@ using namespace Luau; -LUAU_FASTFLAG(LuauSolverV2); +LUAU_FASTFLAG(LuauSolverV2) +LUAU_FASTFLAG(LuauImproveTypePathsInErrors) TEST_SUITE_BEGIN("IntersectionTypes"); @@ -357,12 +358,21 @@ TEST_CASE_FIXTURE(Fixture, "table_intersection_write_sealed_indirect") else { LUAU_REQUIRE_ERROR_COUNT(4, result); - const std::string expected = R"(Type + + const std::string expected = (FFlag::LuauImproveTypePathsInErrors) + ? "Type\n\t" + "'(string, number) -> string'" + "\ncould not be converted into\n\t" + "'(string) -> string'\n" + "caused by:\n" + " Argument count mismatch. Function expects 2 arguments, but only 1 is specified" + : R"(Type '(string, number) -> string' could not be converted into '(string) -> string' caused by: Argument count mismatch. Function expects 2 arguments, but only 1 is specified)"; + CHECK_EQ(expected, toString(result.errors[0])); CHECK_EQ(toString(result.errors[1]), "Cannot add property 'z' to table 'X & Y'"); CHECK_EQ(toString(result.errors[2]), "Type 'number' could not be converted into 'string'"); @@ -387,7 +397,14 @@ TEST_CASE_FIXTURE(Fixture, "table_write_sealed_indirect") )"); LUAU_REQUIRE_ERROR_COUNT(4, result); - const std::string expected = R"(Type + const std::string expected = (FFlag::LuauImproveTypePathsInErrors) + ? "Type\n\t" + "'(string, number) -> string'" + "\ncould not be converted into\n\t" + "'(string) -> string'\n" + "caused by:\n" + " Argument count mismatch. Function expects 2 arguments, but only 1 is specified" + : R"(Type '(string, number) -> string' could not be converted into '(string) -> string' @@ -430,7 +447,20 @@ Type 'number' could not be converted into 'X')"; R"(Type 'number' could not be converted into 'X & Y & Z'; type number (number) is not a subtype of X & Y & Z[0] (X) type number (number) is not a subtype of X & Y & Z[1] (Y) type number (number) is not a subtype of X & Y & Z[2] (Z))"; - if (FFlag::LuauSolverV2) + + if (FFlag::LuauSolverV2 && FFlag::LuauImproveTypePathsInErrors) + { + const std::string expected = "Type " + "'number'" + " could not be converted into " + "'X & Y & Z'; \n" + "this is because \n\t" + " * the 1st component of the intersection is `X`, and `number` is not a subtype of `X`\n\t" + " * the 2nd component of the intersection is `Y`, and `number` is not a subtype of `Y`\n\t" + " * the 3rd component of the intersection is `Z`, and `number` is not a subtype of `Z`"; + CHECK_EQ(expected, toString(result.errors[0])); + } + else if (FFlag::LuauSolverV2) CHECK_EQ(dcrExprected, toString(result.errors[0])); else CHECK_EQ(expected, toString(result.errors[0])); @@ -450,7 +480,23 @@ end )"); LUAU_REQUIRE_ERROR_COUNT(1, result); - if (FFlag::LuauSolverV2) + + if (FFlag::LuauSolverV2 && FFlag::LuauImproveTypePathsInErrors) + { + const std::string expected = "Type pack " + "'X & Y & Z'" + " could not be converted into " + "'number'; \n" + "this is because \n\t" + " * in the 1st entry in the type pack has the 1st component of the intersection as `X` and the 1st entry in the " + "type pack is `number`, and `X` is not a subtype of `number`\n\t" + " * in the 1st entry in the type pack has the 2nd component of the intersection as `Y` and the 1st entry in the " + "type pack is `number`, and `Y` is not a subtype of `number`\n\t" + " * in the 1st entry in the type pack has the 3rd component of the intersection as `Z` and the 1st entry in the " + "type pack is `number`, and `Z` is not a subtype of `number`"; + CHECK_EQ(expected, toString(result.errors[0])); + } + else if (FFlag::LuauSolverV2) { CHECK_EQ( R"(Type pack 'X & Y & Z' could not be converted into 'number'; type X & Y & Z[0][0] (X) is not a subtype of number[0] (number) @@ -503,7 +549,19 @@ TEST_CASE_FIXTURE(Fixture, "intersect_bool_and_false") )"); LUAU_REQUIRE_ERROR_COUNT(1, result); - if (FFlag::LuauSolverV2) + + if (FFlag::LuauSolverV2 && FFlag::LuauImproveTypePathsInErrors) + { + const std::string expected = "Type " + "'boolean & false'" + " could not be converted into " + "'true'; \n" + "this is because \n\t" + " * the 1st component of the intersection is `boolean`, which is not a subtype of `true`\n\t" + " * the 2nd component of the intersection is `false`, which is not a subtype of `true`"; + CHECK_EQ(expected, toString(result.errors[0])); + } + else if (FFlag::LuauSolverV2) { CHECK_EQ( R"(Type 'boolean & false' could not be converted into 'true'; type boolean & false[0] (boolean) is not a subtype of true (true) @@ -527,8 +585,21 @@ TEST_CASE_FIXTURE(Fixture, "intersect_false_and_bool_and_false") )"); LUAU_REQUIRE_ERROR_COUNT(1, result); + // TODO: odd stringification of `false & (boolean & false)`.) - if (FFlag::LuauSolverV2) + if (FFlag::LuauSolverV2 && FFlag::LuauImproveTypePathsInErrors) + { + const std::string expected = "Type " + "'boolean & false & false'" + " could not be converted into " + "'true'; \n" + "this is because \n\t" + " * the 1st component of the intersection is `false`, which is not a subtype of `true`\n\t" + " * the 2nd component of the intersection is `boolean`, which is not a subtype of `true`\n\t" + " * the 3rd component of the intersection is `false`, which is not a subtype of `true`"; + CHECK_EQ(expected, toString(result.errors[0])); + } + else if (FFlag::LuauSolverV2) CHECK_EQ( R"(Type 'boolean & false & false' could not be converted into 'true'; type boolean & false & false[0] (false) is not a subtype of true (true) type boolean & false & false[1] (boolean) is not a subtype of true (true) @@ -550,7 +621,39 @@ TEST_CASE_FIXTURE(Fixture, "intersect_saturate_overloaded_functions") local z : (number) -> number = x -- Not OK end )"); - if (FFlag::LuauSolverV2) + + if (FFlag::LuauSolverV2 && FFlag::LuauImproveTypePathsInErrors) + { + const std::string expected1 = + "Type\n\t" + "'((number?) -> number?) & ((string?) -> string?)'" + "\ncould not be converted into\n\t" + "'(nil) -> nil'; \n" + "this is because \n\t" + " * in the 1st component of the intersection, the function returns the 1st entry in the type pack which has the 1st component of the " + "union as `number` and it returns the 1st entry in the type pack is `nil`, and `number` is not a subtype of `nil`\n\t" + " * in the 2nd component of the intersection, the function returns the 1st entry in the type pack which has the 1st component of the " + "union as `string` and it returns the 1st entry in the type pack is `nil`, and `string` is not a subtype of `nil`"; + + const std::string expected2 = + "Type\n\t" + "'((number?) -> number?) & ((string?) -> string?)'" + "\ncould not be converted into\n\t" + "'(number) -> number'; \n" + "this is because \n\t" + " * in the 1st component of the intersection, the function returns the 1st entry in the type pack which has the 2nd component of the " + "union as `nil` and it returns the 1st entry in the type pack is `number`, and `nil` is not a subtype of `number`\n\t" + " * in the 2nd component of the intersection, the function returns the 1st entry in the type pack which has the 1st component of the " + "union as `string` and it returns the 1st entry in the type pack is `number`, and `string` is not a subtype of `number`\n\t" + " * in the 2nd component of the intersection, the function returns the 1st entry in the type pack which has the 2nd component of the " + "union as `nil` and it returns the 1st entry in the type pack is `number`, and `nil` is not a subtype of `number`\n\t" + " * in the 2nd component of the intersection, the function takes the 1st entry in the type pack which is `string?` and it takes the 1st " + "entry in the type pack is `number`, and `string?` is not a supertype of `number`"; + + CHECK_EQ(expected1, toString(result.errors[0])); + CHECK_EQ(expected2, toString(result.errors[1])); + } + else if (FFlag::LuauSolverV2) { LUAU_REQUIRE_ERROR_COUNT(2, result); const std::string expected1 = R"(Type @@ -568,6 +671,15 @@ could not be converted into CHECK_EQ(expected1, toString(result.errors[0])); CHECK_EQ(expected2, toString(result.errors[1])); } + else if (FFlag::LuauImproveTypePathsInErrors) + { + LUAU_REQUIRE_ERROR_COUNT(1, result); + const std::string expected = R"(Type + '((number?) -> number?) & ((string?) -> string?)' +could not be converted into + '(number) -> number'; none of the intersection parts are compatible)"; + CHECK_EQ(expected, toString(result.errors[0])); + } else { LUAU_REQUIRE_ERROR_COUNT(1, result); @@ -592,11 +704,23 @@ TEST_CASE_FIXTURE(Fixture, "union_saturate_overloaded_functions") )"); LUAU_REQUIRE_ERROR_COUNT(1, result); - const std::string expected = R"(Type + + if (FFlag::LuauImproveTypePathsInErrors) + { + const std::string expected = "Type\n\t" + "'((number) -> number) & ((string) -> string)'" + "\ncould not be converted into\n\t" + "'(boolean | number) -> boolean | number'; none of the intersection parts are compatible"; + CHECK_EQ(expected, toString(result.errors[0])); + } + else + { + const std::string expected = R"(Type '((number) -> number) & ((string) -> string)' could not be converted into '(boolean | number) -> boolean | number'; none of the intersection parts are compatible)"; - CHECK_EQ(expected, toString(result.errors[0])); + CHECK_EQ(expected, toString(result.errors[0])); + } } TEST_CASE_FIXTURE(Fixture, "intersection_of_tables") @@ -609,16 +733,42 @@ TEST_CASE_FIXTURE(Fixture, "intersection_of_tables") )"); LUAU_REQUIRE_ERROR_COUNT(1, result); - const std::string expected = - (FFlag::LuauSolverV2) - ? R"(Type '{ p: number?, q: number?, r: number? } & { p: number?, q: string? }' could not be converted into '{ p: nil }'; type { p: number?, q: number?, r: number? } & { p: number?, q: string? }[0][read "p"][0] (number) is not exactly { p: nil }[read "p"] (nil) - type { p: number?, q: number?, r: number? } & { p: number?, q: string? }[1][read "p"][0] (number) is not exactly { p: nil }[read "p"] (nil))" - : + + if (FFlag::LuauSolverV2 && FFlag::LuauImproveTypePathsInErrors) + { + const std::string expected = "Type " + "'{ p: number?, q: number?, r: number? } & { p: number?, q: string? }'" + " could not be converted into " + "'{ p: nil }'; \n" + "this is because \n\t" + " * in the 1st component of the intersection, accessing `p` has the 1st component of the union as `number` and " + "accessing `p` results in `nil`, and `number` is not exactly `nil`\n\t" + " * in the 2nd component of the intersection, accessing `p` has the 1st component of the union as `number` and " + "accessing `p` results in `nil`, and `number` is not exactly `nil`"; + CHECK_EQ(expected, toString(result.errors[0])); + } + else if (FFlag::LuauImproveTypePathsInErrors) + { + const std::string expected = R"(Type + '{| p: number?, q: number?, r: number? |} & {| p: number?, q: string? |}' +could not be converted into + '{| p: nil |}'; none of the intersection parts are compatible)"; + CHECK_EQ(expected, toString(result.errors[0])); + } + else + { + const std::string expected = + (FFlag::LuauSolverV2) + ? R"(Type '{ p: number?, q: number?, r: number? } & { p: number?, q: string? }' could not be converted into '{ p: nil }'; type { p: number?, q: number?, r: number? } & { p: number?, q: string? }[0][read "p"][0] (number) is not exactly { p: nil }[read "p"] (nil) + type { p: number?, q: number?, r: number? } & { p: number?, q: string? }[1][read "p"][0] (number) is not exactly { p: nil }[read "p"] (nil))" + : + R"(Type '{| p: number?, q: number?, r: number? |} & {| p: number?, q: string? |}' could not be converted into '{| p: nil |}'; none of the intersection parts are compatible)"; - CHECK_EQ(expected, toString(result.errors[0])); + CHECK_EQ(expected, toString(result.errors[0])); + } } TEST_CASE_FIXTURE(Fixture, "intersection_of_tables_with_top_properties") @@ -630,7 +780,28 @@ TEST_CASE_FIXTURE(Fixture, "intersection_of_tables_with_top_properties") end )"); - if (FFlag::LuauSolverV2) + if (FFlag::LuauSolverV2 && FFlag::LuauImproveTypePathsInErrors) + { + const std::string expected = "Type\n\t" + "'{ p: number?, q: any } & { p: unknown, q: string? }'" + "\ncould not be converted into\n\t" + "'{ p: string?, q: number? }'; \n" + "this is because \n\t" + " * in the 1st component of the intersection, accessing `p` has the 1st component of the union as `number` and " + "accessing `p` results in `string?`, and `number` is not exactly `string?`\n\t" + " * in the 1st component of the intersection, accessing `p` results in `number?` and accessing `p` has the 1st " + "component of the union as `string`, and `number?` is not exactly `string`\n\t" + " * in the 1st component of the intersection, accessing `q` results in `any` and accessing `q` results in " + "`number?`, and `any` is not exactly `number?`\n\t" + " * in the 2nd component of the intersection, accessing `p` results in `unknown` and accessing `p` results in " + "`string?`, and `unknown` is not exactly `string?`\n\t" + " * in the 2nd component of the intersection, accessing `q` has the 1st component of the union as `string` and " + "accessing `q` results in `number?`, and `string` is not exactly `number?`\n\t" + " * in the 2nd component of the intersection, accessing `q` results in `string?` and accessing `q` has the 1st " + "component of the union as `number`, and `string?` is not exactly `number`"; + CHECK_EQ(expected, toString(result.errors[0])); + } + else if (FFlag::LuauSolverV2) { LUAU_REQUIRE_ERROR_COUNT(1, result); CHECK_EQ( @@ -646,6 +817,15 @@ could not be converted into toString(result.errors[0]) ); } + else if (FFlag::LuauImproveTypePathsInErrors) + { + LUAU_REQUIRE_ERROR_COUNT(1, result); + const std::string expected = R"(Type + '{| p: number?, q: any |} & {| p: unknown, q: string? |}' +could not be converted into + '{| p: string?, q: number? |}'; none of the intersection parts are compatible)"; + CHECK_EQ(expected, toString(result.errors[0])); + } else { LUAU_REQUIRE_ERROR_COUNT(1, result); @@ -678,7 +858,52 @@ TEST_CASE_FIXTURE(Fixture, "overloaded_functions_returning_intersections") end )"); - if (FFlag::LuauSolverV2) + if (FFlag::LuauSolverV2 && FFlag::LuauImproveTypePathsInErrors) + { + const std::string expected1 = + "Type\n\t" + "'((number?) -> { p: number } & { q: number }) & ((string?) -> { p: number } & { r: number })'" + "\ncould not be converted into\n\t" + "'(nil) -> { p: number, q: number, r: number }'; \n" + "this is because \n\t" + " * in the 1st component of the intersection, the function returns the 1st entry in the type pack which has the 1st component of the " + "intersection as `{ p: number }` and it returns the 1st entry in the type pack is `{ p: number, q: number, r: number }`, and `{ p: " + "number }` is not a subtype of `{ p: number, q: number, r: number }`\n\t" + " * in the 1st component of the intersection, the function returns the 1st entry in the type pack which has the 2nd component of the " + "intersection as `{ q: number }` and it returns the 1st entry in the type pack is `{ p: number, q: number, r: number }`, and `{ q: " + "number }` is not a subtype of `{ p: number, q: number, r: number }`\n\t" + " * in the 2nd component of the intersection, the function returns the 1st entry in the type pack which has the 1st component of the " + "intersection as `{ p: number }` and it returns the 1st entry in the type pack is `{ p: number, q: number, r: number }`, and `{ p: " + "number }` is not a subtype of `{ p: number, q: number, r: number }`\n\t" + " * in the 2nd component of the intersection, the function returns the 1st entry in the type pack which has the 2nd component of the " + "intersection as `{ r: number }` and it returns the 1st entry in the type pack is `{ p: number, q: number, r: number }`, and `{ r: " + "number }` is not a subtype of `{ p: number, q: number, r: number }`"; + + const std::string expected2 = + "Type\n\t" + "'((number?) -> { p: number } & { q: number }) & ((string?) -> { p: number } & { r: number })'" + "\ncould not be converted into\n\t" + "'(number?) -> { p: number, q: number, r: number }'; \n" + "this is because \n\t" + " * in the 1st component of the intersection, the function returns the 1st entry in the type pack which has the 1st component of the " + "intersection as `{ p: number }` and it returns the 1st entry in the type pack is `{ p: number, q: number, r: number }`, and `{ p: " + "number }` is not a subtype of `{ p: number, q: number, r: number }`\n\t" + " * in the 1st component of the intersection, the function returns the 1st entry in the type pack which has the 2nd component of the " + "intersection as `{ q: number }` and it returns the 1st entry in the type pack is `{ p: number, q: number, r: number }`, and `{ q: " + "number }` is not a subtype of `{ p: number, q: number, r: number }`\n\t" + " * in the 2nd component of the intersection, the function returns the 1st entry in the type pack which has the 1st component of the " + "intersection as `{ p: number }` and it returns the 1st entry in the type pack is `{ p: number, q: number, r: number }`, and `{ p: " + "number }` is not a subtype of `{ p: number, q: number, r: number }`\n\t" + " * in the 2nd component of the intersection, the function returns the 1st entry in the type pack which has the 2nd component of the " + "intersection as `{ r: number }` and it returns the 1st entry in the type pack is `{ p: number, q: number, r: number }`, and `{ r: " + "number }` is not a subtype of `{ p: number, q: number, r: number }`\n\t" + " * in the 2nd component of the intersection, the function takes the 1st entry in the type pack which is `string?` and it takes the 1st " + "entry in the type pack has the 1st component of the union as `number`, and `string?` is not a supertype of `number`"; + + CHECK_EQ(expected1, toString(result.errors[0])); + CHECK_EQ(expected2, toString(result.errors[1])); + } + else if (FFlag::LuauSolverV2) { LUAU_REQUIRE_ERROR_COUNT(2, result); CHECK_EQ( @@ -703,6 +928,17 @@ could not be converted into toString(result.errors[1]) ); } + else if (FFlag::LuauImproveTypePathsInErrors) + { + LUAU_REQUIRE_ERROR_COUNT(1, result); + CHECK_EQ( + R"(Type + '((number?) -> {| p: number |} & {| q: number |}) & ((string?) -> {| p: number |} & {| r: number |})' +could not be converted into + '(number?) -> {| p: number, q: number, r: number |}'; none of the intersection parts are compatible)", + toString(result.errors[0]) + ); + } else { LUAU_REQUIRE_ERROR_COUNT(1, result); @@ -730,6 +966,15 @@ TEST_CASE_FIXTURE(Fixture, "overloaded_functions_mentioning_generic") { LUAU_REQUIRE_ERROR_COUNT(0, result); } + else if (FFlag::LuauImproveTypePathsInErrors) + { + LUAU_REQUIRE_ERROR_COUNT(1, result); + const std::string expected = R"(Type + '((number?) -> a | number) & ((string?) -> a | string)' +could not be converted into + '(number?) -> a'; none of the intersection parts are compatible)"; + CHECK_EQ(expected, toString(result.errors[0])); + } else { LUAU_REQUIRE_ERROR_COUNT(1, result); @@ -757,6 +1002,15 @@ TEST_CASE_FIXTURE(Fixture, "overloaded_functions_mentioning_generics") { LUAU_REQUIRE_NO_ERRORS(result); } + else if (FFlag::LuauImproveTypePathsInErrors) + { + LUAU_REQUIRE_ERROR_COUNT(1, result); + const std::string expected = R"(Type + '((a?) -> a | b) & ((c?) -> b | c)' +could not be converted into + '(a?) -> (a & c) | b'; none of the intersection parts are compatible)"; + CHECK_EQ(expected, toString(result.errors[0])); + } else { LUAU_REQUIRE_ERROR_COUNT(1, result); @@ -778,7 +1032,35 @@ TEST_CASE_FIXTURE(Fixture, "overloaded_functions_mentioning_generic_packs") end end )"); - if (FFlag::LuauSolverV2) + + if (FFlag::LuauSolverV2 && FFlag::LuauImproveTypePathsInErrors) + { + const std::string expected1 = + "Type\n\t" + "'((number?, a...) -> (number?, b...)) & ((string?, a...) -> (string?, b...))'" + "\ncould not be converted into\n\t" + "'(nil, a...) -> (nil, b...)'; \n" + "this is because \n\t" + " * in the 1st component of the intersection, the function returns the 1st entry in the type pack which has the 1st component of the " + "union as `number` and it returns the 1st entry in the type pack is `nil`, and `number` is not a subtype of `nil`\n\t" + " * in the 2nd component of the intersection, the function returns the 1st entry in the type pack which has the 1st component of the " + "union as `string` and it returns the 1st entry in the type pack is `nil`, and `string` is not a subtype of `nil`"; + + const std::string expected2 = + "Type\n\t" + "'((number?, a...) -> (number?, b...)) & ((string?, a...) -> (string?, b...))'" + "\ncould not be converted into\n\t" + "'(nil, b...) -> (nil, a...)'; \n" + "this is because \n\t" + " * in the 1st component of the intersection, the function returns the 1st entry in the type pack which has the 1st component of the " + "union as `number` and it returns the 1st entry in the type pack is `nil`, and `number` is not a subtype of `nil`\n\t" + " * in the 2nd component of the intersection, the function returns the 1st entry in the type pack which has the 1st component of the " + "union as `string` and it returns the 1st entry in the type pack is `nil`, and `string` is not a subtype of `nil`"; + + CHECK_EQ(expected1, toString(result.errors[0])); + CHECK_EQ(expected2, toString(result.errors[1])); + } + else if (FFlag::LuauSolverV2) { LUAU_REQUIRE_ERROR_COUNT(2, result); CHECK_EQ( @@ -798,6 +1080,15 @@ could not be converted into toString(result.errors[1]) ); } + else if (FFlag::LuauImproveTypePathsInErrors) + { + LUAU_REQUIRE_ERROR_COUNT(1, result); + const std::string expected = R"(Type + '((number?, a...) -> (number?, b...)) & ((string?, a...) -> (string?, b...))' +could not be converted into + '(nil, b...) -> (nil, a...)'; none of the intersection parts are compatible)"; + CHECK_EQ(expected, toString(result.errors[0])); + } else { LUAU_REQUIRE_ERROR_COUNT(1, result); @@ -824,11 +1115,23 @@ TEST_CASE_FIXTURE(Fixture, "overloadeded_functions_with_unknown_result") )"); LUAU_REQUIRE_ERROR_COUNT(1, result); - const std::string expected = R"(Type + + if (FFlag::LuauImproveTypePathsInErrors) + { + const std::string expected = "Type\n\t" + "'((nil) -> unknown) & ((number) -> number)'" + "\ncould not be converted into\n\t" + "'(number?) -> number?'; none of the intersection parts are compatible"; + CHECK_EQ(expected, toString(result.errors[0])); + } + else + { + const std::string expected = R"(Type '((nil) -> unknown) & ((number) -> number)' could not be converted into '(number?) -> number?'; none of the intersection parts are compatible)"; - CHECK_EQ(expected, toString(result.errors[0])); + CHECK_EQ(expected, toString(result.errors[0])); + } } TEST_CASE_FIXTURE(Fixture, "overloadeded_functions_with_unknown_arguments") @@ -846,11 +1149,23 @@ TEST_CASE_FIXTURE(Fixture, "overloadeded_functions_with_unknown_arguments") )"); LUAU_REQUIRE_ERROR_COUNT(1, result); - const std::string expected = R"(Type + + if (FFlag::LuauImproveTypePathsInErrors) + { + const std::string expected = "Type\n\t" + "'((number) -> number?) & ((unknown) -> string?)'" + "\ncould not be converted into\n\t" + "'(number?) -> nil'; none of the intersection parts are compatible"; + CHECK_EQ(expected, toString(result.errors[0])); + } + else + { + const std::string expected = R"(Type '((number) -> number?) & ((unknown) -> string?)' could not be converted into '(number?) -> nil'; none of the intersection parts are compatible)"; - CHECK_EQ(expected, toString(result.errors[0])); + CHECK_EQ(expected, toString(result.errors[0])); + } } TEST_CASE_FIXTURE(Fixture, "overloadeded_functions_with_never_result") @@ -864,7 +1179,36 @@ TEST_CASE_FIXTURE(Fixture, "overloadeded_functions_with_never_result") end )"); - if (FFlag::LuauSolverV2) + if (FFlag::LuauSolverV2 && FFlag::LuauImproveTypePathsInErrors) + { + const std::string expected1 = + "Type\n\t" + "'((nil) -> never) & ((number) -> number)'" + "\ncould not be converted into\n\t" + "'(number?) -> number'; \n" + "this is because \n\t" + " * in the 1st component of the intersection, the function takes the 1st entry in the type pack which is `number` and it takes the 1st " + "entry in the type pack has the 2nd component of the union as `nil`, and `number` is not a supertype of `nil`\n\t" + " * in the 2nd component of the intersection, the function takes the 1st entry in the type pack which is `nil` and it takes the 1st " + "entry in the type pack has the 1st component of the union as `number`, and `nil` is not a supertype of `number`"; + + const std::string expected2 = + "Type\n\t" + "'((nil) -> never) & ((number) -> number)'" + "\ncould not be converted into\n\t" + "'(number?) -> never'; \n" + "this is because \n\t" + " * in the 1st component of the intersection, the function returns the 1st entry in the type pack which is `number` and it returns the " + "1st entry in the type pack is `never`, and `number` is not a subtype of `never`\n\t" + " * in the 1st component of the intersection, the function takes the 1st entry in the type pack which is `number` and it takes the 1st " + "entry in the type pack has the 2nd component of the union as `nil`, and `number` is not a supertype of `nil`\n\t" + " * in the 2nd component of the intersection, the function takes the 1st entry in the type pack which is `nil` and it takes the 1st " + "entry in the type pack has the 1st component of the union as `number`, and `nil` is not a supertype of `number`"; + + CHECK_EQ(expected1, toString(result.errors[0])); + CHECK_EQ(expected2, toString(result.errors[1])); + } + else if (FFlag::LuauSolverV2) { LUAU_REQUIRE_ERROR_COUNT(2, result); CHECK_EQ( @@ -885,6 +1229,15 @@ could not be converted into toString(result.errors[1]) ); } + else if (FFlag::LuauImproveTypePathsInErrors) + { + LUAU_REQUIRE_ERROR_COUNT(1, result); + const std::string expected = R"(Type + '((nil) -> never) & ((number) -> number)' +could not be converted into + '(number?) -> never'; none of the intersection parts are compatible)"; + CHECK_EQ(expected, toString(result.errors[0])); + } else { LUAU_REQUIRE_ERROR_COUNT(1, result); @@ -907,7 +1260,40 @@ TEST_CASE_FIXTURE(Fixture, "overloadeded_functions_with_never_arguments") end )"); - if (FFlag::LuauSolverV2) + if (FFlag::LuauSolverV2 && FFlag::LuauImproveTypePathsInErrors) + { + const std::string expected1 = + "Type\n\t" + "'((never) -> string?) & ((number) -> number?)'" + "\ncould not be converted into\n\t" + "'(never) -> nil'; \n" + "this is because \n\t" + " * in the 1st component of the intersection, the function returns the 1st entry in the type pack which has the 1st component of the " + "union as `number` and it returns the 1st entry in the type pack is `nil`, and `number` is not a subtype of `nil`\n\t" + " * in the 2nd component of the intersection, the function returns the 1st entry in the type pack which has the 1st component of the " + "union as `string` and it returns the 1st entry in the type pack is `nil`, and `string` is not a subtype of `nil`"; + + const std::string expected2 = + "Type\n\t" + "'((never) -> string?) & ((number) -> number?)'" + "\ncould not be converted into\n\t" + "'(number?) -> nil'; \n" + "this is because \n\t" + " * in the 1st component of the intersection, the function returns the 1st entry in the type pack which has the 1st component of the " + "union as `number` and it returns the 1st entry in the type pack is `nil`, and `number` is not a subtype of `nil`\n\t" + " * in the 1st component of the intersection, the function takes the 1st entry in the type pack which is `number` and it takes the 1st " + "entry in the type pack has the 2nd component of the union as `nil`, and `number` is not a supertype of `nil`\n\t" + " * in the 2nd component of the intersection, the function returns the 1st entry in the type pack which has the 1st component of the " + "union as `string` and it returns the 1st entry in the type pack is `nil`, and `string` is not a subtype of `nil`\n\t" + " * in the 2nd component of the intersection, the function takes the 1st entry in the type pack which is `never` and it takes the 1st " + "entry in the type pack has the 1st component of the union as `number`, and `never` is not a supertype of `number`\n\t" + " * in the 2nd component of the intersection, the function takes the 1st entry in the type pack which is `never` and it takes the 1st " + "entry in the type pack has the 2nd component of the union as `nil`, and `never` is not a supertype of `nil`"; + + CHECK_EQ(expected1, toString(result.errors[0])); + CHECK_EQ(expected2, toString(result.errors[1])); + } + else if (FFlag::LuauSolverV2) { LUAU_REQUIRE_ERROR_COUNT(2, result); const std::string expected1 = R"(Type @@ -926,6 +1312,15 @@ could not be converted into CHECK_EQ(expected1, toString(result.errors[0])); CHECK_EQ(expected2, toString(result.errors[1])); } + else if (FFlag::LuauImproveTypePathsInErrors) + { + LUAU_REQUIRE_ERROR_COUNT(1, result); + const std::string expected = R"(Type + '((never) -> string?) & ((number) -> number?)' +could not be converted into + '(number?) -> nil'; none of the intersection parts are compatible)"; + CHECK_EQ(expected, toString(result.errors[0])); + } else { LUAU_REQUIRE_ERROR_COUNT(1, result); @@ -950,11 +1345,23 @@ TEST_CASE_FIXTURE(Fixture, "overloadeded_functions_with_overlapping_results_and_ )"); LUAU_REQUIRE_ERROR_COUNT(1, result); - const std::string expected = R"(Type + + if (FFlag::LuauImproveTypePathsInErrors) + { + const std::string expected = "Type\n\t" + "'((number?) -> (...number)) & ((string?) -> number | string)'" + "\ncould not be converted into\n\t" + "'(number | string) -> (number, number?)'; none of the intersection parts are compatible"; + CHECK(expected == toString(result.errors[0])); + } + else + { + const std::string expected = R"(Type '((number?) -> (...number)) & ((string?) -> number | string)' could not be converted into '(number | string) -> (number, number?)'; none of the intersection parts are compatible)"; - CHECK_EQ(expected, toString(result.errors[0])); + CHECK_EQ(expected, toString(result.errors[0])); + } } TEST_CASE_FIXTURE(Fixture, "overloadeded_functions_with_weird_typepacks_1") @@ -1022,6 +1429,15 @@ TEST_CASE_FIXTURE(Fixture, "overloadeded_functions_with_weird_typepacks_3") { LUAU_REQUIRE_NO_ERRORS(result); } + else if (FFlag::LuauImproveTypePathsInErrors) + { + LUAU_REQUIRE_ERROR_COUNT(1, result); + const std::string expected = R"(Type + '(() -> (a...)) & (() -> (number?, a...))' +could not be converted into + '() -> number'; none of the intersection parts are compatible)"; + CHECK_EQ(expected, toString(result.errors[0])); + } else { LUAU_REQUIRE_ERROR_COUNT(1, result); @@ -1045,7 +1461,21 @@ TEST_CASE_FIXTURE(Fixture, "overloadeded_functions_with_weird_typepacks_4") )"); LUAU_REQUIRE_ERROR_COUNT(1, result); - if (FFlag::LuauSolverV2) + + if (FFlag::LuauSolverV2 && FFlag::LuauImproveTypePathsInErrors) + { + const std::string expected = "Type\n\t" + "'((a...) -> ()) & ((number, a...) -> number)'" + "\ncould not be converted into\n\t" + "'((a...) -> ()) & ((number, a...) -> number)'; \n" + "this is because \n\t" + " * in the 1st component of the intersection, the function returns is `()` in the former type and `number` in " + "the latter type, and `()` is not a subtype of `number`\n\t" + " * in the 2nd component of the intersection, the function takes a tail of `a...` and in the 1st component of " + "the intersection, the function takes a tail of `a...`, and `a...` is not a supertype of `a...`"; + CHECK(expected == toString(result.errors[0])); + } + else if (FFlag::LuauSolverV2) { CHECK_EQ( R"(Type @@ -1056,6 +1486,16 @@ could not be converted into toString(result.errors[0]) ); } + else if (FFlag::LuauImproveTypePathsInErrors) + { + CHECK_EQ( + R"(Type + '((a...) -> ()) & ((number, a...) -> number)' +could not be converted into + '(number?) -> ()'; none of the intersection parts are compatible)", + toString(result.errors[0]) + ); + } else { CHECK_EQ( diff --git a/tests/TypeInfer.modules.test.cpp b/tests/TypeInfer.modules.test.cpp index ce4490fa..5ee5aba2 100644 --- a/tests/TypeInfer.modules.test.cpp +++ b/tests/TypeInfer.modules.test.cpp @@ -12,6 +12,7 @@ LUAU_FASTFLAG(LuauInstantiateInSubtyping) LUAU_FASTFLAG(LuauSolverV2) +LUAU_FASTFLAG(LuauImproveTypePathsInErrors) using namespace Luau; @@ -461,7 +462,14 @@ local b: B.T = a CheckResult result = frontend.check("game/C"); LUAU_REQUIRE_ERROR_COUNT(1, result); - if (FFlag::LuauSolverV2) + if (FFlag::LuauSolverV2 && FFlag::LuauImproveTypePathsInErrors) + { + const std::string expected = "Type 'T' from 'game/A' could not be converted into 'T' from 'game/B'; \n" + "this is because accessing `x` results in `number` in the former type and `string` in the latter type, and " + "`number` is not exactly `string`"; + CHECK(expected == toString(result.errors[0])); + } + else if (FFlag::LuauSolverV2) { CHECK( toString(result.errors.at(0)) == @@ -507,7 +515,14 @@ local b: B.T = a CheckResult result = frontend.check("game/D"); LUAU_REQUIRE_ERROR_COUNT(1, result); - if (FFlag::LuauSolverV2) + if (FFlag::LuauSolverV2 && FFlag::LuauImproveTypePathsInErrors) + { + const std::string expected = "Type 'T' from 'game/B' could not be converted into 'T' from 'game/C'; \n" + "this is because accessing `x` results in `number` in the former type and `string` in the latter type, and " + "`number` is not exactly `string`"; + CHECK(expected == toString(result.errors[0])); + } + else if (FFlag::LuauSolverV2) { CHECK( toString(result.errors.at(0)) == diff --git a/tests/TypeInfer.negations.test.cpp b/tests/TypeInfer.negations.test.cpp index a21751ec..ed591c61 100644 --- a/tests/TypeInfer.negations.test.cpp +++ b/tests/TypeInfer.negations.test.cpp @@ -79,4 +79,5 @@ end )"); LUAU_REQUIRE_NO_ERRORS(result); } + TEST_SUITE_END(); diff --git a/tests/TypeInfer.operators.test.cpp b/tests/TypeInfer.operators.test.cpp index 32338a68..28281ad1 100644 --- a/tests/TypeInfer.operators.test.cpp +++ b/tests/TypeInfer.operators.test.cpp @@ -17,7 +17,6 @@ using namespace Luau; LUAU_FASTFLAG(LuauSolverV2) -LUAU_FASTFLAG(LuauDoNotGeneralizeInTypeFunctions) TEST_SUITE_BEGIN("TypeInferOperators"); @@ -815,8 +814,6 @@ TEST_CASE_FIXTURE(Fixture, "strict_binary_op_where_lhs_unknown") TEST_CASE_FIXTURE(BuiltinsFixture, "and_binexps_dont_unify") { - ScopedFastFlag _{FFlag::LuauDoNotGeneralizeInTypeFunctions, true}; - // `t` will be inferred to be of type `{ { test: unknown } }` which is // reasonable, in that it's empty with no bounds on its members. Optimally // we might emit an error here that the `print(...)` expression is diff --git a/tests/TypeInfer.provisional.test.cpp b/tests/TypeInfer.provisional.test.cpp index 5cafedbf..537091f0 100644 --- a/tests/TypeInfer.provisional.test.cpp +++ b/tests/TypeInfer.provisional.test.cpp @@ -11,14 +11,15 @@ using namespace Luau; -LUAU_FASTFLAG(LuauSolverV2); -LUAU_FASTFLAG(DebugLuauEqSatSimplification); -LUAU_FASTFLAG(LuauStoreCSTData); -LUAU_FASTINT(LuauNormalizeCacheLimit); -LUAU_FASTINT(LuauTarjanChildLimit); -LUAU_FASTINT(LuauTypeInferIterationLimit); -LUAU_FASTINT(LuauTypeInferRecursionLimit); -LUAU_FASTINT(LuauTypeInferTypePackLoopLimit); +LUAU_FASTFLAG(LuauSolverV2) +LUAU_FASTFLAG(DebugLuauEqSatSimplification) +LUAU_FASTFLAG(LuauStoreCSTData) +LUAU_FASTINT(LuauNormalizeCacheLimit) +LUAU_FASTINT(LuauTarjanChildLimit) +LUAU_FASTINT(LuauTypeInferIterationLimit) +LUAU_FASTINT(LuauTypeInferRecursionLimit) +LUAU_FASTINT(LuauTypeInferTypePackLoopLimit) +LUAU_FASTFLAG(LuauImproveTypePathsInErrors) TEST_SUITE_BEGIN("ProvisionalTests"); @@ -873,7 +874,15 @@ TEST_CASE_FIXTURE(Fixture, "assign_table_with_refined_property_with_a_similar_ty else { LUAU_REQUIRE_ERROR_COUNT(1, result); - const std::string expected = R"(Type + const std::string expected = (FFlag::LuauImproveTypePathsInErrors) ? + R"(Type + '{| x: number? |}' +could not be converted into + '{| x: number |}' +caused by: + Property 'x' is not compatible. +Type 'number?' could not be converted into 'number' in an invariant context)" + : R"(Type '{| x: number? |}' could not be converted into '{| x: number |}' diff --git a/tests/TypeInfer.singletons.test.cpp b/tests/TypeInfer.singletons.test.cpp index 3aa4efee..91192eb1 100644 --- a/tests/TypeInfer.singletons.test.cpp +++ b/tests/TypeInfer.singletons.test.cpp @@ -6,7 +6,8 @@ using namespace Luau; -LUAU_FASTFLAG(LuauSolverV2); +LUAU_FASTFLAG(LuauSolverV2) +LUAU_FASTFLAG(LuauImproveTypePathsInErrors) TEST_SUITE_BEGIN("TypeSingletons"); @@ -364,7 +365,16 @@ TEST_CASE_FIXTURE(Fixture, "table_properties_type_error_escapes") )"); LUAU_REQUIRE_ERROR_COUNT(1, result); - if (FFlag::LuauSolverV2) + + if (FFlag::LuauSolverV2 && FFlag::LuauImproveTypePathsInErrors) + { + const std::string expected = "Type\n\t" + "'{ [\"\\n\"]: number }'" + "\ncould not be converted into\n\t" + "'{ [\"<>\"]: number }'"; + CHECK(expected == toString(result.errors[0])); + } + else if (FFlag::LuauSolverV2) CHECK( "Type\n" " '{ [\"\\n\"]: number }'\n" @@ -440,12 +450,23 @@ TEST_CASE_FIXTURE(Fixture, "parametric_tagged_union_alias") LUAU_REQUIRE_ERROR_COUNT(1, result); - const std::string expectedError = R"(Type + if (FFlag::LuauImproveTypePathsInErrors) + { + const std::string expectedError = "Type\n\t" + "'{ result: string, success: boolean }'" + "\ncould not be converted into\n\t" + "'Err | Ok'"; + CHECK(toString(result.errors[0]) == expectedError); + } + else + { + const std::string expectedError = R"(Type '{ result: string, success: boolean }' could not be converted into 'Err | Ok')"; - CHECK(toString(result.errors[0]) == expectedError); + CHECK(toString(result.errors[0]) == expectedError); + } } TEST_CASE_FIXTURE(Fixture, "if_then_else_expression_singleton_options") diff --git a/tests/TypeInfer.tables.test.cpp b/tests/TypeInfer.tables.test.cpp index 3d7fdaa8..f4744067 100644 --- a/tests/TypeInfer.tables.test.cpp +++ b/tests/TypeInfer.tables.test.cpp @@ -4,6 +4,7 @@ #include "Luau/Error.h" #include "Luau/Frontend.h" #include "Luau/ToString.h" +#include "Luau/TypeChecker2.h" #include "Luau/TypeInfer.h" #include "Luau/Type.h" @@ -17,19 +18,18 @@ using namespace Luau; LUAU_FASTFLAG(LuauSolverV2) + LUAU_FASTFLAG(LuauInstantiateInSubtyping) LUAU_FASTFLAG(LuauFixIndexerSubtypingOrdering) LUAU_FASTFLAG(LuauTrackInteriorFreeTypesOnScope) LUAU_FASTFLAG(LuauTrackInteriorFreeTablesOnScope) -LUAU_FASTFLAG(LuauDontInPlaceMutateTableType) -LUAU_FASTFLAG(LuauAllowNonSharedTableTypesInLiteral) LUAU_FASTFLAG(LuauFollowTableFreeze) LUAU_FASTFLAG(LuauPrecalculateMutatedFreeTypes2) LUAU_FASTFLAG(LuauDeferBidirectionalInferenceForTableAssignment) LUAU_FASTFLAG(LuauBidirectionalInferenceUpcast) LUAU_FASTFLAG(DebugLuauAssertOnForcedConstraint) LUAU_FASTFLAG(LuauSearchForRefineableType) -LUAU_FASTFLAG(LuauDoNotGeneralizeInTypeFunctions) +LUAU_FASTFLAG(LuauImproveTypePathsInErrors) TEST_SUITE_BEGIN("TableTests"); @@ -922,7 +922,16 @@ TEST_CASE_FIXTURE(Fixture, "sealed_table_indexers_must_unify") LUAU_REQUIRE_ERROR_COUNT(1, result); - if (FFlag::LuauSolverV2) + if (FFlag::LuauSolverV2 && FFlag::LuauImproveTypePathsInErrors) + { + CHECK( + "Type pack '{number}' could not be converted into '{string}'; \n" + "this is because in the 1st entry in the type pack, the result of indexing is `number` in the former type and `string` in the latter " + "type, " + "and `number` is not exactly `string`" == toString(result.errors[0]) + ); + } + else if (FFlag::LuauSolverV2) { // CLI-114879 - Error path reporting is not great CHECK( @@ -1805,7 +1814,16 @@ TEST_CASE_FIXTURE(Fixture, "table_subtyping_with_missing_props_dont_report_multi LUAU_REQUIRE_ERROR_COUNT(1, result); - if (FFlag::LuauSolverV2) + if (FFlag::LuauSolverV2 && FFlag::LuauImproveTypePathsInErrors) + { + CHECK_EQ( + "Type pack '{ x: number }' could not be converted into '{ x: number, y: number, z: number }'; \n" + "this is because the 1st entry in the type pack is `{ x: number }` in the former type and `{ x: number, y: number, z: number }` in the " + "latter type, and `{ x: number }` is not a subtype of `{ x: number, y: number, z: number }`", + toString(result.errors[0]) + ); + } + else if (FFlag::LuauSolverV2) { CHECK_EQ( "Type pack '{ x: number }' could not be converted into '{ x: number, y: number, z: number }';" @@ -2435,7 +2453,15 @@ local b: B = a LUAU_REQUIRE_ERRORS(result); - if (FFlag::LuauSolverV2) + if (FFlag::LuauSolverV2 && FFlag::LuauImproveTypePathsInErrors) + { + CHECK( + "Type 'A' could not be converted into 'B'; \n" + "this is because accessing `y` results in `number` in the former type and `string` in the latter type, and `number` is not exactly " + "`string`" == toString(result.errors.at(0)) + ); + } + else if (FFlag::LuauSolverV2) CHECK(toString(result.errors.at(0)) == R"(Type 'A' could not be converted into 'B'; at [read "y"], number is not exactly string)"); else { @@ -2462,7 +2488,15 @@ local b: B = a LUAU_REQUIRE_ERRORS(result); - if (FFlag::LuauSolverV2) + if (FFlag::LuauSolverV2 && FFlag::LuauImproveTypePathsInErrors) + { + CHECK( + "Type 'A' could not be converted into 'B'; \n" + "this is because accessing `b.y` results in `number` in the former type and `string` in the latter type, and `number` is not exactly " + "`string`" == toString(result.errors.at(0)) + ); + } + else if (FFlag::LuauSolverV2) CHECK(toString(result.errors.at(0)) == R"(Type 'A' could not be converted into 'B'; at [read "b"][read "y"], number is not exactly string)"); else { @@ -2489,7 +2523,17 @@ local b2 = setmetatable({ x = 2, y = 4 }, { __call = function(s, t) end }); local c2: typeof(a2) = b2 )"); - const std::string expected1 = R"(Type 'b1' could not be converted into 'a1' + const std::string expected1 = (FFlag::LuauImproveTypePathsInErrors) ? + R"(Type 'b1' could not be converted into 'a1' +caused by: + Type + '{ x: number, y: string }' +could not be converted into + '{ x: number, y: number }' +caused by: + Property 'y' is not compatible. +Type 'string' could not be converted into 'number' in an invariant context)" + : R"(Type 'b1' could not be converted into 'a1' caused by: Type '{ x: number, y: string }' @@ -2498,7 +2542,20 @@ could not be converted into caused by: Property 'y' is not compatible. Type 'string' could not be converted into 'number' in an invariant context)"; - const std::string expected2 = R"(Type 'b2' could not be converted into 'a2' + const std::string expected2 = (FFlag::LuauImproveTypePathsInErrors) ? + R"(Type 'b2' could not be converted into 'a2' +caused by: + Type + '{ __call: (a, b) -> () }' +could not be converted into + '{ __call: (a) -> () }' +caused by: + Property '__call' is not compatible. +Type + '(a, b) -> ()' +could not be converted into + '(a) -> ()'; different number of generic type parameters)" + : R"(Type 'b2' could not be converted into 'a2' caused by: Type '{ __call: (a, b) -> () }' @@ -2511,7 +2568,23 @@ Type could not be converted into '(a) -> ()'; different number of generic type parameters)"; - if (FFlag::LuauSolverV2) + if (FFlag::LuauSolverV2 && FFlag::LuauImproveTypePathsInErrors) + { + // The assignment of c2 to b2 is, surprisingly, allowed under the new + // solver for two reasons: + // + // First, both of the __call functions have hidden ...any arguments + // because their exact definition is available. + // + // Second, nil <: unknown, so we consider that parameter to be optional. + LUAU_REQUIRE_ERROR_COUNT(1, result); + CHECK( + "Type 'b1' could not be converted into 'a1'; \n" + "this is because in the table portion, accessing `y` results in `string` in the former type and `number` in the latter type, and " + "`string` is not exactly `number`" == toString(result.errors[0]) + ); + } + else if (FFlag::LuauSolverV2) { // The assignment of c2 to b2 is, surprisingly, allowed under the new // solver for two reasons: @@ -2576,7 +2649,15 @@ TEST_CASE_FIXTURE(Fixture, "error_detailed_indexer_key") LUAU_REQUIRE_ERRORS(result); - if (FFlag::LuauSolverV2) + if (FFlag::LuauSolverV2 && FFlag::LuauImproveTypePathsInErrors) + { + CHECK( + "Type 'A' could not be converted into 'B'; \n" + "this is because the index type is `number` in the former type and `string` in the latter type, and `number` is not exactly `string`" == + toString(result.errors[0]) + ); + } + else if (FFlag::LuauSolverV2) { CHECK("Type 'A' could not be converted into 'B'; at indexer(), number is not exactly string" == toString(result.errors[0])); } @@ -2602,7 +2683,15 @@ TEST_CASE_FIXTURE(Fixture, "error_detailed_indexer_value") LUAU_REQUIRE_ERRORS(result); - if (FFlag::LuauSolverV2) + if (FFlag::LuauSolverV2 && FFlag::LuauImproveTypePathsInErrors) + { + CHECK( + "Type 'A' could not be converted into 'B'; \n" + "this is because the result of indexing is `number` in the former type and `string` in the latter type, and `number` is not exactly " + "`string`" == toString(result.errors[0]) + ); + } + else if (FFlag::LuauSolverV2) { CHECK("Type 'A' could not be converted into 'B'; at indexResult(), number is not exactly string" == toString(result.errors[0])); } @@ -2651,7 +2740,13 @@ local y: number = tmp.p.y LUAU_REQUIRE_ERROR_COUNT(1, result); - if (FFlag::LuauSolverV2) + if (FFlag::LuauSolverV2 && FFlag::LuauImproveTypePathsInErrors) + CHECK( + "Type 'tmp' could not be converted into 'HasSuper'; \n" + "this is because accessing `p` results in `{ x: number, y: number }` in the former type and `Super` in the latter type, and `{ x: " + "number, y: number }` is not exactly `Super`" == toString(result.errors[0]) + ); + else if (FFlag::LuauSolverV2) CHECK( "Type 'tmp' could not be converted into 'HasSuper'; at [read \"p\"], { x: number, y: number } is not exactly Super" == toString(result.errors[0]) @@ -3615,7 +3710,14 @@ TEST_CASE_FIXTURE(Fixture, "mixed_tables_with_implicit_numbered_keys") local t: { [string]: number } = { 5, 6, 7 } )"); - if (FFlag::LuauSolverV2) + if (FFlag::LuauSolverV2 && FFlag::LuauImproveTypePathsInErrors) + { + std::string expected = + "Type '{number}' could not be converted into '{ [string]: number }'; \n" + "this is because the index type is `number` in the former type and `string` in the latter type, and `number` is not exactly `string`"; + CHECK(toString(result.errors[0]) == expected); + } + else if (FFlag::LuauSolverV2) { LUAU_REQUIRE_ERROR_COUNT(1, result); CHECK( @@ -3757,6 +3859,36 @@ TEST_CASE_FIXTURE(Fixture, "scalar_is_not_a_subtype_of_a_compatible_polymorphic_ CHECK("typeof(string)" == toString(tm4->givenType)); CHECK("t1 where t1 = { read absolutely_no_scalar_has_this_method: (t1) -> (a...) }" == toString(tm4->wantedType)); } + else if (FFlag::LuauImproveTypePathsInErrors) + { + LUAU_REQUIRE_ERROR_COUNT(3, result); + + const std::string expected1 = + R"(Type 'string' could not be converted into 't1 where t1 = {- absolutely_no_scalar_has_this_method: (t1) -> (a...) -}' +caused by: + The former's metatable does not satisfy the requirements. +Table type 'typeof(string)' not compatible with type 't1 where t1 = {- absolutely_no_scalar_has_this_method: (t1) -> (a...) -}' because the former is missing field 'absolutely_no_scalar_has_this_method')"; + CHECK_EQ(expected1, toString(result.errors[0])); + + const std::string expected2 = + R"(Type '"bar"' could not be converted into 't1 where t1 = {- absolutely_no_scalar_has_this_method: (t1) -> (a...) -}' +caused by: + The former's metatable does not satisfy the requirements. +Table type 'typeof(string)' not compatible with type 't1 where t1 = {- absolutely_no_scalar_has_this_method: (t1) -> (a...) -}' because the former is missing field 'absolutely_no_scalar_has_this_method')"; + CHECK_EQ(expected2, toString(result.errors[1])); + + const std::string expected3 = R"(Type + '"bar" | "baz"' +could not be converted into + 't1 where t1 = {- absolutely_no_scalar_has_this_method: (t1) -> (a...) -}' +caused by: + Not all union options are compatible. +Type '"bar"' could not be converted into 't1 where t1 = {- absolutely_no_scalar_has_this_method: (t1) -> (a...) -}' +caused by: + The former's metatable does not satisfy the requirements. +Table type 'typeof(string)' not compatible with type 't1 where t1 = {- absolutely_no_scalar_has_this_method: (t1) -> (a...) -}' because the former is missing field 'absolutely_no_scalar_has_this_method')"; + CHECK_EQ(expected3, toString(result.errors[2])); + } else { LUAU_REQUIRE_ERROR_COUNT(3, result); @@ -4338,11 +4470,25 @@ TEST_CASE_FIXTURE(Fixture, "identify_all_problematic_table_fields") LUAU_REQUIRE_ERROR_COUNT(1, result); - std::string expected = - "Type '{ a: string, b: boolean, c: number }' could not be converted into 'T'; at [read \"a\"], string is not exactly number" - "\n\tat [read \"b\"], boolean is not exactly string" - "\n\tat [read \"c\"], number is not exactly boolean"; - CHECK(toString(result.errors[0]) == expected); + + if (FFlag::LuauImproveTypePathsInErrors) + { + std::string expected = + "Type '{ a: string, b: boolean, c: number }' could not be converted into 'T'; \n" + "this is because \n\t" + " * accessing `a` results in `string` in the former type and `number` in the latter type, and `string` is not exactly `number`\n\t" + " * accessing `b` results in `boolean` in the former type and `string` in the latter type, and `boolean` is not exactly `string`\n\t" + " * accessing `c` results in `number` in the former type and `boolean` in the latter type, and `number` is not exactly `boolean`"; + CHECK(toString(result.errors[0]) == expected); + } + else + { + std::string expected = + "Type '{ a: string, b: boolean, c: number }' could not be converted into 'T'; at [read \"a\"], string is not exactly number" + "\n\tat [read \"b\"], boolean is not exactly string" + "\n\tat [read \"c\"], number is not exactly boolean"; + CHECK(toString(result.errors[0]) == expected); + } } TEST_CASE_FIXTURE(Fixture, "read_and_write_only_table_properties_are_unsupported") @@ -4968,12 +5114,30 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "subtyping_with_a_metatable_table_path") )"); LUAU_REQUIRE_ERROR_COUNT(1, result); - CHECK_EQ( - "Type pack '{ @metatable { }, { } & { } }' could not be converted into 'Class'; at [0].metatable(), { } is not a subtype of nil\n" - "\ttype { @metatable { }, { } & { } }[0].table()[0] ({ }) is not a subtype of Class[0].table() (nil)\n" - "\ttype { @metatable { }, { } & { } }[0].table()[1] ({ }) is not a subtype of Class[0].table() (nil)", - toString(result.errors[0]) - ); + + if (FFlag::LuauImproveTypePathsInErrors) + { + CHECK_EQ( + "Type pack '{ @metatable { }, { } & { } }' could not be converted into 'Class'; \n" + "this is because \n\t" + " * in the 1st entry in the type pack, the metatable portion is `{ }` in the former type and `nil` in the latter type, and `{ }` " + "is not a subtype of `nil`\n\t" + " * in the 1st entry in the type pack, the table portion has the 1st component of the intersection as `{ }` and in the 1st entry " + "in the type pack, the table portion is `nil`, and `{ }` is not a subtype of `nil`\n\t" + " * in the 1st entry in the type pack, the table portion has the 2nd component of the intersection as `{ }` and in the 1st entry " + "in the type pack, the table portion is `nil`, and `{ }` is not a subtype of `nil`", + toString(result.errors[0]) + ); + } + else + { + CHECK_EQ( + "Type pack '{ @metatable { }, { } & { } }' could not be converted into 'Class'; at [0].metatable(), { } is not a subtype of nil\n" + "\ttype { @metatable { }, { } & { } }[0].table()[0] ({ }) is not a subtype of Class[0].table() (nil)\n" + "\ttype { @metatable { }, { } & { } }[0].table()[1] ({ }) is not a subtype of Class[0].table() (nil)", + toString(result.errors[0]) + ); + } } TEST_CASE_FIXTURE(BuiltinsFixture, "metatable_union_type") @@ -5071,7 +5235,6 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "multiple_fields_in_literal") { ScopedFastFlag sffs[] = { {FFlag::LuauSolverV2, true}, - {FFlag::LuauDontInPlaceMutateTableType, true}, }; auto result = check(R"( @@ -5098,11 +5261,7 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "multiple_fields_in_literal") TEST_CASE_FIXTURE(BuiltinsFixture, "multiple_fields_from_fuzzer") { - ScopedFastFlag sffs[] = { - {FFlag::LuauSolverV2, true}, - {FFlag::LuauDontInPlaceMutateTableType, true}, - {FFlag::LuauAllowNonSharedTableTypesInLiteral, true}, - }; + ScopedFastFlag _{FFlag::LuauSolverV2, true}; // This would trigger an assert previously, so we really only care that // there are errors (and there will be: lots of syntax errors). @@ -5113,11 +5272,7 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "multiple_fields_from_fuzzer") TEST_CASE_FIXTURE(BuiltinsFixture, "write_only_table_field_duplicate") { - ScopedFastFlag sffs[] = { - {FFlag::LuauSolverV2, true}, - {FFlag::LuauDontInPlaceMutateTableType, true}, - {FFlag::LuauAllowNonSharedTableTypesInLiteral, true}, - }; + ScopedFastFlag _{FFlag::LuauSolverV2, true}; auto result = check(R"( type WriteOnlyTable = { write x: number } @@ -5345,7 +5500,6 @@ TEST_CASE_FIXTURE(Fixture, "deeply_nested_classish_inference") { ScopedFastFlag sffs[] = { {FFlag::LuauSolverV2, true}, - {FFlag::LuauDoNotGeneralizeInTypeFunctions, true}, {FFlag::LuauSearchForRefineableType, true}, {FFlag::DebugLuauAssertOnForcedConstraint, true}, }; @@ -5367,5 +5521,56 @@ 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::LuauImproveTypePathsInErrors, true}, + }; + + auto result = check(R"( + type File = { + type: "file", + name: string, + content: string?, + } + + type Dir = { + type: "dir", + name: string, + children: { File | Dir }?, + } + + + type DirectoryChildren = { File | Dir } + + local newtree: DirectoryChildren = { + { + type = "dir", + name = "src", + children = { + { + type = "file", + path = "main.luau", -- I accidentally assign "path" instead of "name", causing a huge scary TypeError + } + } + } + } + )"); + + LUAU_REQUIRE_ERROR_COUNT(1, result); + std::string expected = "Type\n\t" + "'{Dir | File | { children: ({Dir | File | { content: string?, path: string, type: \"file\" }} | {Dir | File})?, name: " + "string, type: \"dir\" }}'" + "\ncould not be converted into\n\t" + "'DirectoryChildren'; \n" + "this is because in the result of indexing has the 3rd component of the union as `{ children: ({Dir | File | { content: " + "string?, path: string, type: \"file\" }} | {Dir | File})?, name: string, type: \"dir\" }` and the result of indexing is " + "`Dir | File`, and `{ children: ({Dir | File | { content: string?, path: string, type: \"file\" }} | {Dir | File})?, " + "name: string, type: \"dir\" }` is not exactly `Dir | File`"; + + CHECK_EQ(expected, toString(result.errors[0])); +} + TEST_SUITE_END(); diff --git a/tests/TypeInfer.test.cpp b/tests/TypeInfer.test.cpp index 6e367ce1..a8d82ab9 100644 --- a/tests/TypeInfer.test.cpp +++ b/tests/TypeInfer.test.cpp @@ -29,6 +29,7 @@ LUAU_FASTFLAG(LuauNewNonStrictWarnOnUnknownGlobals) LUAU_FASTFLAG(LuauInferLocalTypesInMultipleAssignments) LUAU_FASTFLAG(LuauUnifyMetatableWithAny) LUAU_FASTFLAG(LuauExtraFollows) +LUAU_FASTFLAG(LuauImproveTypePathsInErrors) using namespace Luau; @@ -1134,7 +1135,29 @@ TEST_CASE_FIXTURE(Fixture, "cli_50041_committing_txnlog_in_apollo_client_error") end )"); - if (FFlag::LuauInstantiateInSubtyping) + if (FFlag::LuauInstantiateInSubtyping && FFlag::LuauImproveTypePathsInErrors) + { + LUAU_REQUIRE_ERROR_COUNT(1, result); + const std::string expected = + "Type 'Policies' from 'MainModule' could not be converted into 'Policies' from 'MainModule'" + "\ncaused by:\n" + " Property 'getStoreFieldName' is not compatible.\n" + "Type\n\t" + "'(Policies, FieldSpecifier & {| from: number? |}) -> (a, b...)'" + "\ncould not be converted into\n\t" + "'(Policies, FieldSpecifier) -> string'" + "\ncaused by:\n" + " Argument #2 type is not compatible.\n" + "Type\n\t" + "'FieldSpecifier'" + "\ncould not be converted into\n\t" + "'FieldSpecifier & {| from: number? |}'" + "\ncaused by:\n" + " Not all intersection parts are compatible.\n" + "Table type 'FieldSpecifier' not compatible with type '{| from: number? |}' because the former has extra field 'fieldName'"; + CHECK_EQ(expected, toString(result.errors[0])); + } + else if (FFlag::LuauInstantiateInSubtyping) { // though this didn't error before the flag, it seems as though it should error since fields of a table are invariant. // the user's intent would likely be that these "method" fields would be read-only, but without an annotation, accepting this should be @@ -1911,4 +1934,17 @@ end )"); } +TEST_CASE_FIXTURE(Fixture, "concat_string_with_string_union") +{ + ScopedFastFlag _{FFlag::LuauSolverV2, true}; + LUAU_REQUIRE_NO_ERRORS(check(R"( + local function foo(n : number): string return "" end + local function bar(n: number, m: string) end + local function concat_stuff(x, y) + local z = foo(x) + bar(y, z) + end + )")); +} + TEST_SUITE_END(); diff --git a/tests/TypeInfer.typePacks.test.cpp b/tests/TypeInfer.typePacks.test.cpp index 858c3052..c0c0e18a 100644 --- a/tests/TypeInfer.typePacks.test.cpp +++ b/tests/TypeInfer.typePacks.test.cpp @@ -9,8 +9,10 @@ using namespace Luau; -LUAU_FASTFLAG(LuauSolverV2); -LUAU_FASTFLAG(LuauInstantiateInSubtyping); +LUAU_FASTFLAG(LuauSolverV2) + +LUAU_FASTFLAG(LuauInstantiateInSubtyping) +LUAU_FASTFLAG(LuauImproveTypePathsInErrors) TEST_SUITE_BEGIN("TypePackTests"); @@ -951,7 +953,19 @@ a = b LUAU_REQUIRE_ERROR_COUNT(1, result); - if (FFlag::LuauSolverV2) + if (FFlag::LuauSolverV2 && FFlag::LuauImproveTypePathsInErrors) + { + + const std::string expected = "Type\n\t" + "'() -> (number, ...boolean)'" + "\ncould not be converted into\n\t" + "'() -> (number, ...string)'; \n" + "this is because it returns a tail of the variadic `boolean` in the former type and `string` in the latter " + "type, and `boolean` is not a subtype of `string`"; + + CHECK(expected == toString(result.errors[0])); + } + else if (FFlag::LuauSolverV2) { const std::string expected = "Type\n" " '() -> (number, ...boolean)'\n" @@ -960,6 +974,16 @@ a = b CHECK(expected == toString(result.errors[0])); } + else if (FFlag::LuauImproveTypePathsInErrors) + { + const std::string expected = R"(Type + '() -> (number, ...boolean)' +could not be converted into + '() -> (number, ...string)' +caused by: + Type 'boolean' could not be converted into 'string')"; + CHECK_EQ(expected, toString(result.errors[0])); + } else { const std::string expected = R"(Type @@ -1093,7 +1117,12 @@ TEST_CASE_FIXTURE(Fixture, "unify_variadic_tails_in_arguments_free") )"); LUAU_REQUIRE_ERROR_COUNT(1, result); - if (FFlag::LuauSolverV2) + if (FFlag::LuauSolverV2 && FFlag::LuauImproveTypePathsInErrors) + CHECK( + toString(result.errors.at(0)) == "Type pack '...number' could not be converted into 'boolean'; \nthis is because it has a tail of " + "`...number`, which is not a subtype of `boolean`" + ); + else if (FFlag::LuauSolverV2) CHECK( toString(result.errors.at(0)) == "Type pack '...number' could not be converted into 'boolean'; type ...number.tail() (...number) is not a subtype of boolean (boolean)" diff --git a/tests/TypeInfer.unionTypes.test.cpp b/tests/TypeInfer.unionTypes.test.cpp index 247894d1..628dfbdd 100644 --- a/tests/TypeInfer.unionTypes.test.cpp +++ b/tests/TypeInfer.unionTypes.test.cpp @@ -10,6 +10,8 @@ using namespace Luau; LUAU_FASTFLAG(LuauSolverV2) +LUAU_FASTFLAG(LuauImproveTypePathsInErrors) + TEST_SUITE_BEGIN("UnionTypes"); TEST_CASE_FIXTURE(Fixture, "fuzzer_union_with_one_part_assertion") @@ -538,7 +540,19 @@ end LUAU_REQUIRE_ERROR_COUNT(1, result); - if (FFlag::LuauSolverV2) + if (FFlag::LuauSolverV2 && FFlag::LuauImproveTypePathsInErrors) + { + + CHECK_EQ( + toString(result.errors[0]), + "Type 'X | Y | Z' could not be converted into '{ w: number }'; \n" + "this is because \n\t" + " * the 1st component of the union is `X`, which is not a subtype of `{ w: number }`\n\t" + " * the 2nd component of the union is `Y`, which is not a subtype of `{ w: number }`\n\t" + " * the 3rd component of the union is `Z`, which is not a subtype of `{ w: number }`" + ); + } + else if (FFlag::LuauSolverV2) { CHECK_EQ( toString(result.errors[0]), @@ -667,11 +681,23 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "table_union_write_indirect") LUAU_REQUIRE_ERROR_COUNT(1, result); // NOTE: union normalization will improve this message - const std::string expected = R"(Type + + if (FFlag::LuauImproveTypePathsInErrors) + { + const std::string expected = "Type\n\t" + "'(string) -> number'" + "\ncould not be converted into\n\t" + "'((number) -> string) | ((number) -> string)'; none of the union options are compatible"; + CHECK_EQ(expected, toString(result.errors[0])); + } + else + { + const std::string expected = R"(Type '(string) -> number' could not be converted into '((number) -> string) | ((number) -> string)'; none of the union options are compatible)"; - CHECK_EQ(expected, toString(result.errors[0])); + CHECK_EQ(expected, toString(result.errors[0])); + } } TEST_CASE_FIXTURE(Fixture, "union_true_and_false") @@ -757,11 +783,23 @@ TEST_CASE_FIXTURE(Fixture, "union_of_functions_mentioning_generic_typepacks") )"); LUAU_REQUIRE_ERROR_COUNT(1, result); - const std::string expected = R"(Type + + if (FFlag::LuauImproveTypePathsInErrors) + { + const std::string expected = "Type\n\t" + "'(number, a...) -> (number?, a...)'" + "\ncould not be converted into\n\t" + "'((number) -> number) | ((number?, a...) -> (number?, a...))'; none of the union options are compatible"; + CHECK_EQ(expected, toString(result.errors[0])); + } + else + { + const std::string expected = R"(Type '(number, a...) -> (number?, a...)' could not be converted into '((number) -> number) | ((number?, a...) -> (number?, a...))'; none of the union options are compatible)"; - CHECK_EQ(expected, toString(result.errors[0])); + CHECK_EQ(expected, toString(result.errors[0])); + } } TEST_CASE_FIXTURE(Fixture, "union_of_functions_with_mismatching_arg_arities") @@ -776,11 +814,23 @@ TEST_CASE_FIXTURE(Fixture, "union_of_functions_with_mismatching_arg_arities") )"); LUAU_REQUIRE_ERROR_COUNT(1, result); - const std::string expected = R"(Type + + if (FFlag::LuauImproveTypePathsInErrors) + { + const std::string expected = "Type\n\t" + "'(number) -> number?'" + "\ncould not be converted into\n\t" + "'((number) -> nil) | ((number, string?) -> number)'; none of the union options are compatible"; + CHECK_EQ(expected, toString(result.errors[0])); + } + else + { + const std::string expected = R"(Type '(number) -> number?' could not be converted into '((number) -> nil) | ((number, string?) -> number)'; none of the union options are compatible)"; - CHECK_EQ(expected, toString(result.errors[0])); + CHECK_EQ(expected, toString(result.errors[0])); + } } TEST_CASE_FIXTURE(Fixture, "union_of_functions_with_mismatching_result_arities") @@ -795,11 +845,23 @@ TEST_CASE_FIXTURE(Fixture, "union_of_functions_with_mismatching_result_arities") )"); LUAU_REQUIRE_ERROR_COUNT(1, result); - const std::string expected = R"(Type + + if (FFlag::LuauImproveTypePathsInErrors) + { + const std::string expected = "Type\n\t" + "'() -> number | string'" + "\ncould not be converted into\n\t" + "'(() -> (string, string)) | (() -> number)'; none of the union options are compatible"; + CHECK_EQ(expected, toString(result.errors[0])); + } + else + { + const std::string expected = R"(Type '() -> number | string' could not be converted into '(() -> (string, string)) | (() -> number)'; none of the union options are compatible)"; - CHECK_EQ(expected, toString(result.errors[0])); + CHECK_EQ(expected, toString(result.errors[0])); + } } TEST_CASE_FIXTURE(Fixture, "union_of_functions_with_variadics") @@ -814,11 +876,23 @@ TEST_CASE_FIXTURE(Fixture, "union_of_functions_with_variadics") )"); LUAU_REQUIRE_ERROR_COUNT(1, result); - const std::string expected = R"(Type + + if (FFlag::LuauImproveTypePathsInErrors) + { + const std::string expected = "Type\n\t" + "'(...nil) -> (...number?)'" + "\ncould not be converted into\n\t" + "'((...string?) -> (...number)) | ((...string?) -> nil)'; none of the union options are compatible"; + CHECK_EQ(expected, toString(result.errors[0])); + } + else + { + const std::string expected = R"(Type '(...nil) -> (...number?)' could not be converted into '((...string?) -> (...number)) | ((...string?) -> nil)'; none of the union options are compatible)"; - CHECK_EQ(expected, toString(result.errors[0])); + CHECK_EQ(expected, toString(result.errors[0])); + } } TEST_CASE_FIXTURE(Fixture, "union_of_functions_with_mismatching_arg_variadics") @@ -831,13 +905,29 @@ TEST_CASE_FIXTURE(Fixture, "union_of_functions_with_mismatching_arg_variadics") )"); LUAU_REQUIRE_ERROR_COUNT(1, result); - if (FFlag::LuauSolverV2) + if (FFlag::LuauSolverV2 && FFlag::LuauImproveTypePathsInErrors) + { + const std::string expected = "Type\n\t" + "'(number) -> ()'" + "\ncould not be converted into\n\t" + "'((...number?) -> ()) | ((number?) -> ())'"; + CHECK(expected == toString(result.errors[0])); + } + else if (FFlag::LuauSolverV2) { CHECK(R"(Type '(number) -> ()' could not be converted into '((...number?) -> ()) | ((number?) -> ())')" == toString(result.errors[0])); } + else if (FFlag::LuauImproveTypePathsInErrors) + { + const std::string expected = R"(Type + '(number) -> ()' +could not be converted into + '((...number?) -> ()) | ((number?) -> ())'; none of the union options are compatible)"; + CHECK_EQ(expected, toString(result.errors[0])); + } else { const std::string expected = R"(Type @@ -860,11 +950,23 @@ TEST_CASE_FIXTURE(Fixture, "union_of_functions_with_mismatching_result_variadics )"); LUAU_REQUIRE_ERROR_COUNT(1, result); - const std::string expected = R"(Type + + if (FFlag::LuauImproveTypePathsInErrors) + { + const std::string expected = "Type\n\t" + "'() -> (number?, ...number)'" + "\ncould not be converted into\n\t" + "'(() -> (...number)) | (() -> number)'; none of the union options are compatible"; + CHECK_EQ(expected, toString(result.errors[0])); + } + else + { + const std::string expected = R"(Type '() -> (number?, ...number)' could not be converted into '(() -> (...number)) | (() -> number)'; none of the union options are compatible)"; - CHECK_EQ(expected, toString(result.errors[0])); + CHECK_EQ(expected, toString(result.errors[0])); + } } TEST_CASE_FIXTURE(Fixture, "less_greedy_unification_with_union_types") @@ -991,4 +1093,17 @@ TEST_CASE_FIXTURE(Fixture, "suppress_errors_for_prop_lookup_of_a_union_that_incl LUAU_REQUIRE_NO_ERRORS(result); } +TEST_CASE_FIXTURE(BuiltinsFixture, "handle_multiple_optionals") +{ + CheckResult result = check(R"( + function f(a: string??) + if a then + print(a:sub(1,1)) + end + end + )"); + + LUAU_REQUIRE_NO_ERRORS(result); +} + TEST_SUITE_END(); diff --git a/tests/TypePath.test.cpp b/tests/TypePath.test.cpp index b281dcab..facd62ad 100644 --- a/tests/TypePath.test.cpp +++ b/tests/TypePath.test.cpp @@ -554,6 +554,14 @@ TEST_CASE("chain") CHECK(toString(PathBuilder().index(0).mt().build()) == "[0].metatable()"); } +TEST_CASE("human_property_then_metatable_portion") +{ + ScopedFastFlag sff{FFlag::LuauSolverV2, true}; + + CHECK(toStringHuman(PathBuilder().readProp("a").mt().build()) == "accessing `a` has the metatable portion as "); + CHECK(toStringHuman(PathBuilder().writeProp("a").mt().build()) == "writing to `a` has the metatable portion as "); +} + TEST_SUITE_END(); // TypePathToString TEST_SUITE_BEGIN("TypePathBuilder");