diff --git a/Analysis/include/Luau/FileResolver.h b/Analysis/include/Luau/FileResolver.h index d3fc6ad3..3a4c58a6 100644 --- a/Analysis/include/Luau/FileResolver.h +++ b/Analysis/include/Luau/FileResolver.h @@ -1,8 +1,9 @@ // This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details #pragma once -#include +#include #include +#include #include namespace Luau @@ -32,15 +33,71 @@ struct ModuleInfo bool optional = false; }; +struct RequireAlias +{ + std::string alias; // Unprefixed alias name (no leading `@`). + std::vector tags = {}; +}; + +struct RequireNode +{ + virtual ~RequireNode() {} + + // Get the path component representing this node. + virtual std::string getPathComponent() const = 0; + + // Get the displayed user-facing label for this node, defaults to getPathComponent() + virtual std::string getLabel() const + { + return getPathComponent(); + } + + // Get tags to attach to this node's RequireSuggestion (defaults to none). + virtual std::vector getTags() const + { + return {}; + } + + // TODO: resolvePathToNode() can ultimately be replaced with a call into + // require-by-string's path resolution algorithm. This will first require + // generalizing that algorithm to work with a virtual file system. + virtual std::unique_ptr resolvePathToNode(const std::string& path) const = 0; + + // Get children of this node, if any (if this node represents a directory). + virtual std::vector> getChildren() const = 0; + + // A list of the aliases available to this node. + virtual std::vector getAvailableAliases() const = 0; +}; + struct RequireSuggestion { std::string label; std::string fullPath; + std::vector tags; }; using RequireSuggestions = std::vector; +struct RequireSuggester +{ + virtual ~RequireSuggester() {} + std::optional getRequireSuggestions(const ModuleName& requirer, const std::optional& pathString) const; + +protected: + virtual std::unique_ptr getNode(const ModuleName& name) const = 0; + +private: + std::optional getRequireSuggestionsImpl(const ModuleName& requirer, const std::optional& path) const; +}; + struct FileResolver { + FileResolver() = default; + FileResolver(std::shared_ptr requireSuggester) + : requireSuggester(std::move(requireSuggester)) + { + } + virtual ~FileResolver() {} virtual std::optional readSource(const ModuleName& name) = 0; @@ -60,10 +117,10 @@ struct FileResolver return std::nullopt; } - virtual std::optional getRequireSuggestions(const ModuleName& requirer, const std::optional& pathString) const - { - return std::nullopt; - } + // Make non-virtual when removing FFlagLuauImproveRequireByStringAutocomplete. + virtual std::optional getRequireSuggestions(const ModuleName& requirer, const std::optional& pathString) const; + + std::shared_ptr requireSuggester; }; struct NullFileResolver : FileResolver diff --git a/Analysis/include/Luau/FragmentAutocomplete.h b/Analysis/include/Luau/FragmentAutocomplete.h index 292cb4c8..305bc06d 100644 --- a/Analysis/include/Luau/FragmentAutocomplete.h +++ b/Analysis/include/Luau/FragmentAutocomplete.h @@ -15,6 +15,28 @@ namespace Luau { struct FrontendOptions; +enum class FragmentAutocompleteWaypoint +{ + ParseFragmentEnd, + CloneModuleStart, + CloneModuleEnd, + DfgBuildEnd, + CloneAndSquashScopeStart, + CloneAndSquashScopeEnd, + ConstraintSolverStart, + ConstraintSolverEnd, + TypecheckFragmentEnd, + AutocompleteEnd, + COUNT, +}; + +class IFragmentAutocompleteReporter +{ +public: + virtual void reportWaypoint(FragmentAutocompleteWaypoint) = 0; + virtual void reportFragmentString(std::string_view) = 0; +}; + enum class FragmentTypeCheckStatus { SkipAutocomplete, @@ -70,7 +92,8 @@ std::pair typecheckFragment( const Position& cursorPos, std::optional opts, std::string_view src, - std::optional fragmentEndPosition + std::optional fragmentEndPosition, + IFragmentAutocompleteReporter* reporter = nullptr ); FragmentAutocompleteResult fragmentAutocomplete( @@ -80,7 +103,8 @@ FragmentAutocompleteResult fragmentAutocomplete( Position cursorPosition, std::optional opts, StringCompletionCallback callback, - std::optional fragmentEndPosition = std::nullopt + std::optional fragmentEndPosition = std::nullopt, + IFragmentAutocompleteReporter* reporter = nullptr ); enum class FragmentAutocompleteStatus @@ -102,6 +126,7 @@ struct FragmentContext const ParseResult& freshParse; std::optional opts; std::optional DEPRECATED_fragmentEndPosition; + IFragmentAutocompleteReporter* reporter = nullptr; }; /** diff --git a/Analysis/include/Luau/Quantify.h b/Analysis/include/Luau/Quantify.h index bae3751d..8478ddb2 100644 --- a/Analysis/include/Luau/Quantify.h +++ b/Analysis/include/Luau/Quantify.h @@ -31,13 +31,4 @@ struct OrderedMap } }; -struct QuantifierResult -{ - TypeId result; - OrderedMap insertedGenerics; - OrderedMap insertedGenericPacks; -}; - -std::optional quantify(TypeArena* arena, TypeId ty, Scope* scope); - } // namespace Luau diff --git a/Analysis/src/AutocompleteCore.cpp b/Analysis/src/AutocompleteCore.cpp index 924e4d3e..46369612 100644 --- a/Analysis/src/AutocompleteCore.cpp +++ b/Analysis/src/AutocompleteCore.cpp @@ -25,6 +25,8 @@ LUAU_FASTINT(LuauTypeInferIterationLimit) LUAU_FASTINT(LuauTypeInferRecursionLimit) LUAU_FASTFLAGVARIABLE(DebugLuauMagicVariableNames) +LUAU_FASTFLAG(LuauExposeRequireByStringAutocomplete) + LUAU_FASTFLAGVARIABLE(LuauAutocompleteRefactorsForIncrementalAutocomplete) LUAU_FASTFLAGVARIABLE(LuauAutocompleteUsesModuleForTypeCompatibility) @@ -1519,10 +1521,14 @@ static std::optional convertRequireSuggestionsToAutocomple return std::nullopt; AutocompleteEntryMap result; - for (const RequireSuggestion& suggestion : *suggestions) + for (RequireSuggestion& suggestion : *suggestions) { AutocompleteEntry entry = {AutocompleteEntryKind::RequirePath}; entry.insertText = std::move(suggestion.fullPath); + if (FFlag::LuauExposeRequireByStringAutocomplete) + { + entry.tags = std::move(suggestion.tags); + } result[std::move(suggestion.label)] = std::move(entry); } return result; diff --git a/Analysis/src/Clone.cpp b/Analysis/src/Clone.cpp index d048422d..5ad56e16 100644 --- a/Analysis/src/Clone.cpp +++ b/Analysis/src/Clone.cpp @@ -1,6 +1,7 @@ // This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details #include "Luau/Clone.h" +#include "Luau/Common.h" #include "Luau/NotNull.h" #include "Luau/Type.h" #include "Luau/TypePack.h" @@ -12,6 +13,8 @@ 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) namespace Luau { @@ -477,7 +480,7 @@ private: class FragmentAutocompleteTypeCloner final : public TypeCloner { - Scope* freeTypeReplacementScope = nullptr; + Scope* replacementForNullScope = nullptr; public: FragmentAutocompleteTypeCloner( @@ -487,12 +490,12 @@ public: NotNull packs, TypeId forceTy, TypePackId forceTp, - Scope* freeTypeReplacementScope + Scope* replacementForNullScope ) : TypeCloner(arena, builtinTypes, types, packs, forceTy, forceTp) - , freeTypeReplacementScope(freeTypeReplacementScope) + , replacementForNullScope(replacementForNullScope) { - LUAU_ASSERT(freeTypeReplacementScope); + LUAU_ASSERT(replacementForNullScope); } TypeId shallowClone(TypeId ty) override @@ -512,12 +515,18 @@ public: generic->scope = nullptr; else if (auto free = getMutable(target)) { - free->scope = freeTypeReplacementScope; + free->scope = replacementForNullScope; + } + else if (auto tt = getMutable(target)) + { + if (FFlag::LuauClonedTableAndFunctionTypesMustHaveScopes) + tt->scope = replacementForNullScope; } else if (auto fn = getMutable(target)) - fn->scope = nullptr; - else if (auto table = getMutable(target)) - table->scope = nullptr; + { + if (FFlag::LuauClonedTableAndFunctionTypesMustHaveScopes) + fn->scope = replacementForNullScope; + } (*types)[ty] = target; queue.emplace_back(target); @@ -538,7 +547,7 @@ public: if (auto generic = getMutable(target)) generic->scope = nullptr; else if (auto free = getMutable(target)) - free->scope = freeTypeReplacementScope; + free->scope = replacementForNullScope; (*packs)[tp] = target; queue.emplace_back(target); @@ -728,7 +737,7 @@ Binding cloneIncremental(const Binding& binding, TypeArena& dest, CloneState& cl b.deprecatedSuggestion = binding.deprecatedSuggestion; b.documentationSymbol = binding.documentationSymbol; b.location = binding.location; - b.typeId = cloner.clone(binding.typeId); + b.typeId = FFlag::LuauDoNotClonePersistentBindings && binding.typeId->persistent ? binding.typeId : cloner.clone(binding.typeId); return b; } diff --git a/Analysis/src/ConstraintGenerator.cpp b/Analysis/src/ConstraintGenerator.cpp index 686a78de..e775c59b 100644 --- a/Analysis/src/ConstraintGenerator.cpp +++ b/Analysis/src/ConstraintGenerator.cpp @@ -38,10 +38,12 @@ LUAU_FASTFLAG(DebugLuauGreedyGeneralization) LUAU_FASTFLAGVARIABLE(LuauTrackInteriorFreeTypesOnScope) LUAU_FASTFLAGVARIABLE(LuauDeferBidirectionalInferenceForTableAssignment) LUAU_FASTFLAGVARIABLE(LuauUngeneralizedTypesForRecursiveFunctions) +LUAU_FASTFLAGVARIABLE(LuauGlobalSelfAssignmentCycle) LUAU_FASTFLAG(LuauFreeTypesMustHaveBounds) LUAU_FASTFLAGVARIABLE(LuauInferLocalTypesInMultipleAssignments) LUAU_FASTFLAGVARIABLE(LuauDoNotLeakNilInRefinement) +LUAU_FASTFLAGVARIABLE(LuauExtraFollows) namespace Luau { @@ -2082,7 +2084,7 @@ InferencePack ConstraintGenerator::checkPack(const ScopePtr& scope, AstExprCall* { std::vector unpackedTypes; if (args.size() > 0) - target = args[0]; + target = FFlag::LuauExtraFollows ? follow(args[0]) : args[0]; else { target = arena->addType(BlockedType{}); @@ -2891,6 +2893,13 @@ void ConstraintGenerator::visitLValue(const ScopePtr& scope, AstExprGlobal* glob DefId def = dfg->getDef(global); rootScope->lvalueTypes[def] = rhsType; + if (FFlag::LuauGlobalSelfAssignmentCycle) + { + // Ignore possible self-assignment, it doesn't create a new constraint + if (annotatedTy == follow(rhsType)) + return; + } + // Sketchy: We're specifically looking for BlockedTypes that were // initially created by ConstraintGenerator::prepopulateGlobalScope. if (auto bt = get(follow(*annotatedTy)); bt && !bt->getOwner()) diff --git a/Analysis/src/ConstraintSolver.cpp b/Analysis/src/ConstraintSolver.cpp index f4032557..dbe3c767 100644 --- a/Analysis/src/ConstraintSolver.cpp +++ b/Analysis/src/ConstraintSolver.cpp @@ -37,6 +37,7 @@ LUAU_FASTFLAG(LuauTrackInteriorFreeTypesOnScope) LUAU_FASTFLAGVARIABLE(LuauTrackInteriorFreeTablesOnScope) LUAU_FASTFLAGVARIABLE(LuauPrecalculateMutatedFreeTypes2) LUAU_FASTFLAGVARIABLE(DebugLuauGreedyGeneralization) +LUAU_FASTFLAG(LuauSearchForRefineableType) namespace Luau { @@ -907,26 +908,16 @@ bool ConstraintSolver::tryDispatch(const GeneralizationConstraint& c, NotNull(generalizedType)) return block(generalizedType, constraint); - std::optional generalized; - std::optional generalizedTy = generalize(NotNull{arena}, builtinTypes, constraint->scope, generalizedTypes, c.sourceType); - if (generalizedTy) - generalized = QuantifierResult{*generalizedTy}; // FIXME insertedGenerics and insertedGenericPacks - else + if (!generalizedTy) reportError(CodeTooComplex{}, constraint->location); - if (generalized) + if (generalizedTy) { if (get(generalizedType)) - bind(constraint, generalizedType, generalized->result); + bind(constraint, generalizedType, *generalizedTy); else - unify(constraint, generalizedType, generalized->result); - - for (auto [free, gen] : generalized->insertedGenerics.pairings) - unify(constraint, free, gen); - - for (auto [free, gen] : generalized->insertedGenericPacks.pairings) - unify(constraint, free, gen); + unify(constraint, generalizedType, *generalizedTy); } else { @@ -1356,15 +1347,29 @@ void ConstraintSolver::fillInDiscriminantTypes(NotNull constra if (!ty) continue; - // If the discriminant type has been transmuted, we need to unblock them. - if (!isBlocked(*ty)) + if (FFlag::LuauSearchForRefineableType) { + if (isBlocked(*ty)) + // We bind any unused discriminants to the `*no-refine*` type indicating that it can be safely ignored. + emplaceType(asMutable(follow(*ty)), builtinTypes->noRefineType); + + // We also need to unconditionally unblock these types, otherwise + // you end up with funky looking "Blocked on *no-refine*." unblock(*ty, constraint->location); - continue; } + else + { - // We bind any unused discriminants to the `*no-refine*` type indicating that it can be safely ignored. - emplaceType(asMutable(follow(*ty)), builtinTypes->noRefineType); + // If the discriminant type has been transmuted, we need to unblock them. + if (!isBlocked(*ty)) + { + unblock(*ty, constraint->location); + continue; + } + + // We bind any unused discriminants to the `*no-refine*` type indicating that it can be safely ignored. + emplaceType(asMutable(follow(*ty)), builtinTypes->noRefineType); + } } } diff --git a/Analysis/src/FileResolver.cpp b/Analysis/src/FileResolver.cpp new file mode 100644 index 00000000..acad7b34 --- /dev/null +++ b/Analysis/src/FileResolver.cpp @@ -0,0 +1,172 @@ +// This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details +#include "Luau/FileResolver.h" + +#include "Luau/Common.h" +#include "Luau/StringUtils.h" + +#include +#include +#include +#include +#include + +LUAU_FASTFLAGVARIABLE(LuauExposeRequireByStringAutocomplete) +LUAU_FASTFLAGVARIABLE(LuauEscapeCharactersInRequireSuggestions) +LUAU_FASTFLAGVARIABLE(LuauHideImpossibleRequireSuggestions) + +namespace Luau +{ + +static std::optional processRequireSuggestions(std::optional suggestions) +{ + if (!suggestions) + return suggestions; + + if (FFlag::LuauEscapeCharactersInRequireSuggestions) + { + for (RequireSuggestion& suggestion : *suggestions) + { + suggestion.fullPath = escape(suggestion.fullPath); + } + } + + return suggestions; +} + +static RequireSuggestions makeSuggestionsFromAliases(std::vector aliases) +{ + RequireSuggestions result; + for (RequireAlias& alias : aliases) + { + RequireSuggestion suggestion; + suggestion.label = "@" + std::move(alias.alias); + suggestion.fullPath = suggestion.label; + suggestion.tags = std::move(alias.tags); + result.push_back(std::move(suggestion)); + } + return result; +} + +static RequireSuggestions makeSuggestionsForFirstComponent(std::unique_ptr node) +{ + RequireSuggestions result = makeSuggestionsFromAliases(node->getAvailableAliases()); + result.push_back(RequireSuggestion{"./", "./", {}}); + result.push_back(RequireSuggestion{"../", "../", {}}); + return result; +} + +static RequireSuggestions makeSuggestionsFromNode(std::unique_ptr node, const std::string_view path, bool isPartialPath) +{ + LUAU_ASSERT(!path.empty()); + + RequireSuggestions result; + + const size_t lastSlashInPath = path.find_last_of('/'); + + if (lastSlashInPath != std::string_view::npos) + { + // Add a suggestion for the parent directory + RequireSuggestion parentSuggestion; + parentSuggestion.label = ".."; + + // TODO: after exposing require-by-string's path normalization API, this + // if-else can be replaced. Instead, we can simply normalize the result + // of inserting ".." at the end of the current path. + if (lastSlashInPath >= 2 && path.substr(lastSlashInPath - 2, 3) == "../") + { + parentSuggestion.fullPath = path.substr(0, lastSlashInPath + 1); + parentSuggestion.fullPath += ".."; + } + else + { + parentSuggestion.fullPath = path.substr(0, lastSlashInPath); + } + + result.push_back(std::move(parentSuggestion)); + } + + std::string fullPathPrefix; + if (isPartialPath) + { + // ./path/to/chi -> ./path/to/ + fullPathPrefix += path.substr(0, lastSlashInPath + 1); + } + else + { + if (path.back() == '/') + { + // ./path/to/ -> ./path/to/ + fullPathPrefix += path; + } + else + { + // ./path/to -> ./path/to/ + fullPathPrefix += path; + fullPathPrefix += "/"; + } + } + + for (const std::unique_ptr& child : node->getChildren()) + { + if (!child) + continue; + + std::string pathComponent = child->getPathComponent(); + if (FFlag::LuauHideImpossibleRequireSuggestions) + { + // If path component contains a slash, it cannot be required by string. + // There's no point suggesting it. + if (pathComponent.find('/') != std::string::npos) + continue; + } + + RequireSuggestion suggestion; + suggestion.label = isPartialPath || path.back() == '/' ? child->getLabel() : "/" + child->getLabel(); + suggestion.fullPath = fullPathPrefix + std::move(pathComponent); + suggestion.tags = child->getTags(); + result.push_back(std::move(suggestion)); + } + + return result; +} + +std::optional RequireSuggester::getRequireSuggestionsImpl(const ModuleName& requirer, const std::optional& path) + const +{ + if (!path) + return std::nullopt; + + std::unique_ptr requirerNode = getNode(requirer); + if (!requirerNode) + return std::nullopt; + + const size_t slashPos = path->find_last_of('/'); + + if (slashPos == std::string::npos) + return makeSuggestionsForFirstComponent(std::move(requirerNode)); + + // If path already points at a Node, return the Node's children as paths. + if (std::unique_ptr node = requirerNode->resolvePathToNode(*path)) + return makeSuggestionsFromNode(std::move(node), *path, /* isPartialPath = */ false); + + // Otherwise, recover a partial path and use this to generate suggestions. + if (std::unique_ptr partialNode = requirerNode->resolvePathToNode(path->substr(0, slashPos))) + return makeSuggestionsFromNode(std::move(partialNode), *path, /* isPartialPath = */ true); + + return std::nullopt; +} + +std::optional RequireSuggester::getRequireSuggestions(const ModuleName& requirer, const std::optional& path) const +{ + return processRequireSuggestions(getRequireSuggestionsImpl(requirer, path)); +} + +std::optional FileResolver::getRequireSuggestions(const ModuleName& requirer, const std::optional& path) const +{ + if (!FFlag::LuauExposeRequireByStringAutocomplete) + return std::nullopt; + + return requireSuggester ? requireSuggester->getRequireSuggestions(requirer, path) : std::nullopt; +} + +} // namespace Luau diff --git a/Analysis/src/FragmentAutocomplete.cpp b/Analysis/src/FragmentAutocomplete.cpp index 4c531954..ef8cce21 100644 --- a/Analysis/src/FragmentAutocomplete.cpp +++ b/Analysis/src/FragmentAutocomplete.cpp @@ -30,13 +30,15 @@ LUAU_FASTFLAG(LuauAutocompleteRefactorsForIncrementalAutocomplete) LUAU_FASTFLAGVARIABLE(LuauIncrementalAutocompleteBugfixes) LUAU_FASTFLAGVARIABLE(LuauMixedModeDefFinderTraversesTypeOf) -LUAU_FASTFLAG(LuauBetterReverseDependencyTracking) LUAU_FASTFLAGVARIABLE(LuauCloneIncrementalModule) LUAU_FASTFLAGVARIABLE(LogFragmentsFromAutocomplete) LUAU_FASTFLAGVARIABLE(LuauBetterCursorInCommentDetection) LUAU_FASTFLAGVARIABLE(LuauAllFreeTypesHaveScopes) +LUAU_FASTFLAGVARIABLE(LuauFragmentAcSupportsReporter) LUAU_FASTFLAGVARIABLE(LuauPersistConstraintGenerationScopes) LUAU_FASTFLAG(LuauModuleHoldsAstRoot) +LUAU_FASTFLAGVARIABLE(LuauCloneTypeAliasBindings) +LUAU_FASTFLAGVARIABLE(LuauCloneReturnTypePack) namespace { @@ -100,11 +102,19 @@ struct MixedModeIncrementalTCDefFinder : public AstVisitor return FFlag::LuauMixedModeDefFinderTraversesTypeOf; } + bool visit(AstStatTypeAlias* alias) override + { + if (FFlag::LuauCloneTypeAliasBindings) + declaredAliases.insert(std::string(alias->name.value)); + return true; + } + // ast defs is just a mapping from expr -> def in general // will get built up by the dfg builder // localDefs, we need to copy over std::vector> referencedLocalDefs; + DenseHashSet declaredAliases{""}; }; void cloneAndSquashScopes_DEPRECATED( @@ -181,6 +191,10 @@ void cloneAndSquashScopes( scopes.emplace_back(current); } + MixedModeIncrementalTCDefFinder finder; + + if (FFlag::LuauCloneTypeAliasBindings) + program->visit(&finder); // in reverse order (we need to clone the parents and override defs as we go down the list) for (auto it = scopes.rbegin(); it != scopes.rend(); ++it) { @@ -191,6 +205,21 @@ void cloneAndSquashScopes( // Clone the rvalueRefinements for (const auto& [def, ty] : curr->rvalueRefinements) destScope->rvalueRefinements[def] = Luau::cloneIncremental(ty, *destArena, cloneState, destScope); + + if (FFlag::LuauCloneTypeAliasBindings) + { + for (const auto& [n, tf] : curr->exportedTypeBindings) + { + if (!finder.declaredAliases.contains(n)) + destScope->exportedTypeBindings[n] = Luau::cloneIncremental(tf, *destArena, cloneState, destScope); + } + + for (const auto& [n, tf] : curr->privateTypeBindings) + { + if (!finder.declaredAliases.contains(n)) + destScope->privateTypeBindings[n] = Luau::cloneIncremental(tf, *destArena, cloneState, destScope); + } + } for (const auto& [n, m] : curr->importedTypeBindings) { std::unordered_map importedBindingTypes; @@ -206,10 +235,11 @@ void cloneAndSquashScopes( } } + if (!FFlag::LuauCloneTypeAliasBindings) + program->visit(&finder); // The above code associates defs with TypeId's in the scope // so that lookup to locals will succeed. - MixedModeIncrementalTCDefFinder finder; - program->visit(&finder); + std::vector> locals = std::move(finder.referencedLocalDefs); for (auto [loc, expr] : locals) { @@ -218,6 +248,10 @@ void cloneAndSquashScopes( destScope->lvalueTypes[dfg->getDef(expr)] = Luau::cloneIncremental(binding->typeId, *destArena, cloneState, destScope); } } + + if (FFlag::LuauCloneReturnTypePack && destScope->returnType) + destScope->returnType = Luau::cloneIncremental(destScope->returnType, *destArena, cloneState, destScope); + return; } @@ -572,6 +606,22 @@ void mixedModeCompatibility( } } +static void reportWaypoint(IFragmentAutocompleteReporter* reporter, FragmentAutocompleteWaypoint type) +{ + if (!FFlag::LuauFragmentAcSupportsReporter || !reporter) + return; + + reporter->reportWaypoint(type); +} + +static void reportFragmentString(IFragmentAutocompleteReporter* reporter, std::string_view fragment) +{ + if (!FFlag::LuauFragmentAcSupportsReporter || !reporter) + return; + + reporter->reportFragmentString(fragment); +} + FragmentTypeCheckResult typecheckFragment_( Frontend& frontend, AstStatBlock* root, @@ -579,13 +629,15 @@ FragmentTypeCheckResult typecheckFragment_( const ScopePtr& closestScope, const Position& cursorPos, std::unique_ptr astAllocator, - const FrontendOptions& opts + const FrontendOptions& opts, + IFragmentAutocompleteReporter* reporter ) { LUAU_TIMETRACE_SCOPE("Luau::typecheckFragment_", "FragmentAutocomplete"); freeze(stale->internalTypes); freeze(stale->interfaceTypes); + reportWaypoint(reporter, FragmentAutocompleteWaypoint::CloneModuleStart); CloneState cloneState{frontend.builtinTypes}; std::shared_ptr freshChildOfNearestScope = std::make_shared(closestScope); ModulePtr incrementalModule = nullptr; @@ -596,6 +648,7 @@ FragmentTypeCheckResult typecheckFragment_( else incrementalModule = copyModule(stale, std::move(astAllocator)); + reportWaypoint(reporter, FragmentAutocompleteWaypoint::CloneModuleEnd); incrementalModule->checkedInNewSolver = true; unfreeze(incrementalModule->internalTypes); unfreeze(incrementalModule->interfaceTypes); @@ -623,6 +676,7 @@ FragmentTypeCheckResult typecheckFragment_( /// 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); @@ -644,6 +698,7 @@ FragmentTypeCheckResult typecheckFragment_( {} }; + reportWaypoint(reporter, FragmentAutocompleteWaypoint::CloneAndSquashScopeStart); if (FFlag::LuauCloneIncrementalModule) { incrementalModule->scopes.emplace_back(root->location, freshChildOfNearestScope); @@ -657,6 +712,8 @@ FragmentTypeCheckResult typecheckFragment_( cloneAndSquashScopes_DEPRECATED( cloneState, closestScope.get(), stale, NotNull{&incrementalModule->internalTypes}, NotNull{&dfg}, root, freshChildOfNearestScope.get() ); + + reportWaypoint(reporter, FragmentAutocompleteWaypoint::CloneAndSquashScopeEnd); cg.visitFragmentRoot(freshChildOfNearestScope, root); if (FFlag::LuauPersistConstraintGenerationScopes) @@ -680,6 +737,7 @@ FragmentTypeCheckResult typecheckFragment_( LUAU_ASSERT(back == freshChildOfNearestScope.get()); closestScope->children.pop_back(); } + reportWaypoint(reporter, FragmentAutocompleteWaypoint::ConstraintSolverStart); if (FFlag::LuauAllFreeTypesHaveScopes) { @@ -719,6 +777,8 @@ FragmentTypeCheckResult typecheckFragment_( 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 @@ -735,17 +795,15 @@ std::pair typecheckFragment( const Position& cursorPos, std::optional opts, std::string_view src, - std::optional fragmentEndPosition + std::optional fragmentEndPosition, + IFragmentAutocompleteReporter* reporter ) { LUAU_TIMETRACE_SCOPE("Luau::typecheckFragment", "FragmentAutocomplete"); LUAU_TIMETRACE_ARGUMENT("name", moduleName.c_str()); - if (FFlag::LuauBetterReverseDependencyTracking) - { - if (!frontend.allModuleDependenciesValid(moduleName, opts && opts->forAutocomplete)) - return {FragmentTypeCheckStatus::SkipAutocomplete, {}}; - } + if (!frontend.allModuleDependenciesValid(moduleName, opts && opts->forAutocomplete)) + return {FragmentTypeCheckStatus::SkipAutocomplete, {}}; FrontendModuleResolver& resolver = getModuleResolver(frontend, opts); ModulePtr module = resolver.getModule(moduleName); @@ -778,6 +836,7 @@ std::pair typecheckFragment( } tryParse = parseFragment(sourceModule->root, sourceModule->names.get(), src, cursorPos, fragmentEndPosition); + reportWaypoint(reporter, FragmentAutocompleteWaypoint::ParseFragmentEnd); } if (!tryParse) @@ -791,8 +850,9 @@ 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); + typecheckFragment_(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}; } @@ -813,7 +873,14 @@ FragmentAutocompleteStatusResult tryFragmentAutocomplete( try { Luau::FragmentAutocompleteResult fragmentAutocomplete = Luau::fragmentAutocomplete( - frontend, context.newSrc, moduleName, cursorPosition, context.opts, std::move(stringCompletionCB), context.DEPRECATED_fragmentEndPosition + frontend, + context.newSrc, + moduleName, + cursorPosition, + context.opts, + std::move(stringCompletionCB), + context.DEPRECATED_fragmentEndPosition, + FFlag::LuauFragmentAcSupportsReporter ? context.reporter : nullptr ); return {FragmentAutocompleteStatus::Success, std::move(fragmentAutocomplete)}; } @@ -832,7 +899,8 @@ FragmentAutocompleteResult fragmentAutocomplete( Position cursorPosition, std::optional opts, StringCompletionCallback callback, - std::optional fragmentEndPosition + std::optional fragmentEndPosition, + IFragmentAutocompleteReporter* reporter ) { LUAU_ASSERT(FFlag::LuauAutocompleteRefactorsForIncrementalAutocomplete); @@ -853,10 +921,11 @@ FragmentAutocompleteResult fragmentAutocomplete( return {}; } - auto [tcStatus, tcResult] = typecheckFragment(frontend, moduleName, cursorPosition, opts, src, fragmentEndPosition); + auto [tcStatus, tcResult] = typecheckFragment(frontend, moduleName, cursorPosition, opts, src, fragmentEndPosition, reporter); if (tcStatus == FragmentTypeCheckStatus::SkipAutocomplete) return {}; + reportWaypoint(reporter, FragmentAutocompleteWaypoint::TypecheckFragmentEnd); auto globalScope = (opts && opts->forAutocomplete) ? frontend.globalsForAutocomplete.globalScope.get() : frontend.globals.globalScope.get(); if (FFlag::LogFragmentsFromAutocomplete) logLuau(src); @@ -873,6 +942,7 @@ FragmentAutocompleteResult fragmentAutocomplete( callback ); + reportWaypoint(reporter, FragmentAutocompleteWaypoint::AutocompleteEnd); return {std::move(tcResult.incrementalModule), tcResult.freshScope.get(), std::move(arenaForFragmentAutocomplete), std::move(result)}; } diff --git a/Analysis/src/Frontend.cpp b/Analysis/src/Frontend.cpp index 98d38099..318db0b6 100644 --- a/Analysis/src/Frontend.cpp +++ b/Analysis/src/Frontend.cpp @@ -49,7 +49,6 @@ LUAU_DYNAMIC_FASTFLAGVARIABLE(LuauRunCustomModuleChecks, false) LUAU_FASTFLAGVARIABLE(LuauModuleHoldsAstRoot) -LUAU_FASTFLAGVARIABLE(LuauBetterReverseDependencyTracking) LUAU_FASTFLAGVARIABLE(LuauFixMultithreadTypecheck) LUAU_FASTFLAG(StudioReportLuauAny2) @@ -1035,14 +1034,11 @@ bool Frontend::parseGraph( buildQueue.push_back(top->name); - if (FFlag::LuauBetterReverseDependencyTracking) + // at this point we know all valid dependencies are processed into SourceNodes + for (const ModuleName& dep : top->requireSet) { - // at this point we know all valid dependencies are processed into SourceNodes - for (const ModuleName& dep : top->requireSet) - { - if (auto it = sourceNodes.find(dep); it != sourceNodes.end()) - it->second->dependents.insert(top->name); - } + if (auto it = sourceNodes.find(dep); it != sourceNodes.end()) + it->second->dependents.insert(top->name); } } else @@ -1331,51 +1327,35 @@ void Frontend::recordItemResult(const BuildQueueItem& item) if (item.exception) std::rethrow_exception(item.exception); - if (FFlag::LuauBetterReverseDependencyTracking) + bool replacedModule = false; + if (item.options.forAutocomplete) { - bool replacedModule = false; - if (item.options.forAutocomplete) - { - replacedModule = moduleResolverForAutocomplete.setModule(item.name, item.module); - item.sourceNode->dirtyModuleForAutocomplete = false; - } - else - { - replacedModule = moduleResolver.setModule(item.name, item.module); - item.sourceNode->dirtyModule = false; - } - - if (replacedModule) - { - LUAU_TIMETRACE_SCOPE("Frontend::invalidateDependentModules", "Frontend"); - LUAU_TIMETRACE_ARGUMENT("name", item.name.c_str()); - traverseDependents( - item.name, - [forAutocomplete = item.options.forAutocomplete](SourceNode& sourceNode) - { - bool traverseSubtree = !sourceNode.hasInvalidModuleDependency(forAutocomplete); - sourceNode.setInvalidModuleDependency(true, forAutocomplete); - return traverseSubtree; - } - ); - } - - item.sourceNode->setInvalidModuleDependency(false, item.options.forAutocomplete); + replacedModule = moduleResolverForAutocomplete.setModule(item.name, item.module); + item.sourceNode->dirtyModuleForAutocomplete = false; } else { - if (item.options.forAutocomplete) - { - moduleResolverForAutocomplete.setModule(item.name, item.module); - item.sourceNode->dirtyModuleForAutocomplete = false; - } - else - { - moduleResolver.setModule(item.name, item.module); - item.sourceNode->dirtyModule = false; - } + replacedModule = moduleResolver.setModule(item.name, item.module); + item.sourceNode->dirtyModule = false; } + if (replacedModule) + { + LUAU_TIMETRACE_SCOPE("Frontend::invalidateDependentModules", "Frontend"); + LUAU_TIMETRACE_ARGUMENT("name", item.name.c_str()); + traverseDependents( + item.name, + [forAutocomplete = item.options.forAutocomplete](SourceNode& sourceNode) + { + bool traverseSubtree = !sourceNode.hasInvalidModuleDependency(forAutocomplete); + sourceNode.setInvalidModuleDependency(true, forAutocomplete); + return traverseSubtree; + } + ); + } + + item.sourceNode->setInvalidModuleDependency(false, item.options.forAutocomplete); + stats.timeCheck += item.stats.timeCheck; stats.timeLint += item.stats.timeLint; @@ -1464,7 +1444,6 @@ ScopePtr Frontend::getModuleEnvironment(const SourceModule& module, const Config bool Frontend::allModuleDependenciesValid(const ModuleName& name, bool forAutocomplete) const { - LUAU_ASSERT(FFlag::LuauBetterReverseDependencyTracking); auto it = sourceNodes.find(name); return it != sourceNodes.end() && !it->second->hasInvalidModuleDependency(forAutocomplete); } @@ -1486,72 +1465,27 @@ void Frontend::markDirty(const ModuleName& name, std::vector* marked LUAU_TIMETRACE_SCOPE("Frontend::markDirty", "Frontend"); LUAU_TIMETRACE_ARGUMENT("name", name.c_str()); - if (FFlag::LuauBetterReverseDependencyTracking) - { - traverseDependents( - name, - [markedDirty](SourceNode& sourceNode) - { - if (markedDirty) - markedDirty->push_back(sourceNode.name); - - if (sourceNode.dirtySourceModule && sourceNode.dirtyModule && sourceNode.dirtyModuleForAutocomplete) - return false; - - sourceNode.dirtySourceModule = true; - sourceNode.dirtyModule = true; - sourceNode.dirtyModuleForAutocomplete = true; - - return true; - } - ); - } - else - { - if (sourceNodes.count(name) == 0) - return; - - std::unordered_map> reverseDeps; - for (const auto& module : sourceNodes) + traverseDependents( + name, + [markedDirty](SourceNode& sourceNode) { - for (const auto& dep : module.second->requireSet) - reverseDeps[dep].push_back(module.first); - } - - std::vector queue{name}; - - while (!queue.empty()) - { - ModuleName next = std::move(queue.back()); - queue.pop_back(); - - LUAU_ASSERT(sourceNodes.count(next) > 0); - SourceNode& sourceNode = *sourceNodes[next]; - if (markedDirty) - markedDirty->push_back(next); + markedDirty->push_back(sourceNode.name); if (sourceNode.dirtySourceModule && sourceNode.dirtyModule && sourceNode.dirtyModuleForAutocomplete) - continue; + return false; sourceNode.dirtySourceModule = true; sourceNode.dirtyModule = true; sourceNode.dirtyModuleForAutocomplete = true; - if (0 == reverseDeps.count(next)) - continue; - - sourceModules.erase(next); - - const std::vector& dependents = reverseDeps[next]; - queue.insert(queue.end(), dependents.begin(), dependents.end()); + return true; } - } + ); } void Frontend::traverseDependents(const ModuleName& name, std::function processSubtree) { - LUAU_ASSERT(FFlag::LuauBetterReverseDependencyTracking); LUAU_TIMETRACE_SCOPE("Frontend::traverseDependents", "Frontend"); if (sourceNodes.count(name) == 0) @@ -2012,14 +1946,11 @@ std::pair Frontend::getSourceNode(const ModuleName& sourceNode->name = sourceModule->name; sourceNode->humanReadableName = sourceModule->humanReadableName; - if (FFlag::LuauBetterReverseDependencyTracking) + // clear all prior dependents. we will re-add them after parsing the rest of the graph + for (const auto& [moduleName, _] : sourceNode->requireLocations) { - // clear all prior dependents. we will re-add them after parsing the rest of the graph - for (const auto& [moduleName, _] : sourceNode->requireLocations) - { - if (auto depIt = sourceNodes.find(moduleName); depIt != sourceNodes.end()) - depIt->second->dependents.erase(sourceNode->name); - } + if (auto depIt = sourceNodes.find(moduleName); depIt != sourceNodes.end()) + depIt->second->dependents.erase(sourceNode->name); } sourceNode->requireSet.clear(); @@ -2147,17 +2078,9 @@ bool FrontendModuleResolver::setModule(const ModuleName& moduleName, ModulePtr m { std::scoped_lock lock(moduleMutex); - if (FFlag::LuauBetterReverseDependencyTracking) - { - bool replaced = modules.count(moduleName) > 0; - modules[moduleName] = std::move(module); - return replaced; - } - else - { - modules[moduleName] = std::move(module); - return false; - } + bool replaced = modules.count(moduleName) > 0; + modules[moduleName] = std::move(module); + return replaced; } void FrontendModuleResolver::clearModules() diff --git a/Analysis/src/Quantify.cpp b/Analysis/src/Quantify.cpp index daa61fd5..89f0ebf7 100644 --- a/Analysis/src/Quantify.cpp +++ b/Analysis/src/Quantify.cpp @@ -107,134 +107,4 @@ void quantify(TypeId ty, TypeLevel level) ftv->genericPacks.insert(ftv->genericPacks.end(), q.genericPacks.begin(), q.genericPacks.end()); } -struct PureQuantifier : Substitution -{ - Scope* scope; - OrderedMap insertedGenerics; - OrderedMap insertedGenericPacks; - bool seenMutableType = false; - bool seenGenericType = false; - - PureQuantifier(TypeArena* arena, Scope* scope) - : Substitution(TxnLog::empty(), arena) - , scope(scope) - { - } - - bool isDirty(TypeId ty) override - { - LUAU_ASSERT(ty == follow(ty)); - - if (auto ftv = get(ty)) - { - bool result = subsumes(scope, ftv->scope); - seenMutableType |= result; - return result; - } - else if (auto ttv = get(ty)) - { - if (ttv->state == TableState::Free) - seenMutableType = true; - else if (ttv->state == TableState::Generic) - seenGenericType = true; - - return (ttv->state == TableState::Unsealed || ttv->state == TableState::Free) && subsumes(scope, ttv->scope); - } - - return false; - } - - bool isDirty(TypePackId tp) override - { - if (auto ftp = get(tp)) - { - return subsumes(scope, ftp->scope); - } - - return false; - } - - TypeId clean(TypeId ty) override - { - if (auto ftv = get(ty)) - { - TypeId result = arena->addType(GenericType{scope}); - insertedGenerics.push(ty, result); - return result; - } - else if (auto ttv = get(ty)) - { - TypeId result = arena->addType(TableType{}); - TableType* resultTable = getMutable(result); - LUAU_ASSERT(resultTable); - - *resultTable = *ttv; - resultTable->level = TypeLevel{}; - resultTable->scope = scope; - - if (ttv->state == TableState::Free) - { - resultTable->state = TableState::Generic; - insertedGenerics.push(ty, result); - } - else if (ttv->state == TableState::Unsealed) - resultTable->state = TableState::Sealed; - - return result; - } - - return ty; - } - - TypePackId clean(TypePackId tp) override - { - if (auto ftp = get(tp)) - { - TypePackId result = arena->addTypePack(TypePackVar{GenericTypePack{scope}}); - insertedGenericPacks.push(tp, result); - return result; - } - - return tp; - } - - bool ignoreChildren(TypeId ty) override - { - if (get(ty)) - return true; - - return ty->persistent; - } - bool ignoreChildren(TypePackId ty) override - { - return ty->persistent; - } -}; - -std::optional quantify(TypeArena* arena, TypeId ty, Scope* scope) -{ - PureQuantifier quantifier{arena, scope}; - std::optional result = quantifier.substitute(ty); - if (!result) - return std::nullopt; - - FunctionType* ftv = getMutable(*result); - LUAU_ASSERT(ftv); - ftv->scope = scope; - - for (auto k : quantifier.insertedGenerics.keys) - { - TypeId g = quantifier.insertedGenerics.pairings[k]; - if (get(g)) - ftv->generics.push_back(g); - } - - for (auto k : quantifier.insertedGenericPacks.keys) - ftv->genericPacks.push_back(quantifier.insertedGenericPacks.pairings[k]); - - ftv->hasNoFreeOrGenericTypes = ftv->generics.empty() && ftv->genericPacks.empty() && !quantifier.seenGenericType && !quantifier.seenMutableType; - - return std::optional({*result, std::move(quantifier.insertedGenerics), std::move(quantifier.insertedGenericPacks)}); -} - } // namespace Luau diff --git a/Analysis/src/Transpiler.cpp b/Analysis/src/Transpiler.cpp index ab272587..75d5c5e1 100644 --- a/Analysis/src/Transpiler.cpp +++ b/Analysis/src/Transpiler.cpp @@ -12,7 +12,7 @@ LUAU_FASTFLAG(LuauStoreCSTData) LUAU_FASTFLAG(LuauExtendStatEndPosWithSemicolon) -LUAU_FASTFLAG(LuauAstTypeGroup2) +LUAU_FASTFLAG(LuauAstTypeGroup3) LUAU_FASTFLAG(LuauFixDoBlockEndLocation) namespace @@ -330,7 +330,7 @@ struct Printer_DEPRECATED else if (typeCount == 1) { bool shouldParenthesize = unconditionallyParenthesize && (list.types.size == 0 || !list.types.data[0]->is()); - if (FFlag::LuauAstTypeGroup2 ? shouldParenthesize : unconditionallyParenthesize) + if (FFlag::LuauAstTypeGroup3 ? shouldParenthesize : unconditionallyParenthesize) writer.symbol("("); // Only variadic tail @@ -343,7 +343,7 @@ struct Printer_DEPRECATED visualizeTypeAnnotation(*list.types.data[0]); } - if (FFlag::LuauAstTypeGroup2 ? shouldParenthesize : unconditionallyParenthesize) + if (FFlag::LuauAstTypeGroup3 ? shouldParenthesize : unconditionallyParenthesize) writer.symbol(")"); } else @@ -1349,7 +1349,7 @@ struct Printer else if (typeCount == 1) { bool shouldParenthesize = unconditionallyParenthesize && (list.types.size == 0 || !list.types.data[0]->is()); - if (FFlag::LuauAstTypeGroup2 ? shouldParenthesize : unconditionallyParenthesize) + if (FFlag::LuauAstTypeGroup3 ? shouldParenthesize : unconditionallyParenthesize) writer.symbol("("); // Only variadic tail @@ -1362,7 +1362,7 @@ struct Printer visualizeTypeAnnotation(*list.types.data[0]); } - if (FFlag::LuauAstTypeGroup2 ? shouldParenthesize : unconditionallyParenthesize) + if (FFlag::LuauAstTypeGroup3 ? shouldParenthesize : unconditionallyParenthesize) writer.symbol(")"); } else diff --git a/Analysis/src/TypeFunction.cpp b/Analysis/src/TypeFunction.cpp index 9b5f5ef7..09648017 100644 --- a/Analysis/src/TypeFunction.cpp +++ b/Analysis/src/TypeFunction.cpp @@ -53,6 +53,8 @@ LUAU_FASTFLAGVARIABLE(LuauDoNotGeneralizeInTypeFunctions) LUAU_FASTFLAGVARIABLE(LuauPreventReentrantTypeFunctionReduction) LUAU_FASTFLAGVARIABLE(LuauIntersectNotNil) LUAU_FASTFLAGVARIABLE(LuauSkipNoRefineDuringRefinement) +LUAU_FASTFLAGVARIABLE(LuauDontForgetToReduceUnionFunc) +LUAU_FASTFLAGVARIABLE(LuauSearchForRefineableType) namespace Luau { @@ -630,6 +632,9 @@ static std::optional> tryDistributeTypeFunct {}, }); + if (FFlag::LuauDontForgetToReduceUnionFunc && ctx->solver) + ctx->pushConstraint(ReduceConstraint{resultTy}); + return {{resultTy, Reduction::MaybeOk, {}, {}}}; } @@ -1934,6 +1939,33 @@ struct FindRefinementBlockers : TypeOnceVisitor } }; +struct ContainsRefinableType : TypeOnceVisitor +{ + bool found = false; + ContainsRefinableType() : TypeOnceVisitor(/* skipBoundTypes */ true) {} + + + bool visit(TypeId ty) override { + // Default case: if we find *some* type that's worth refining against, + // then we can claim that this type contains a refineable type. + found = true; + return false; + } + + bool visit(TypeId Ty, const NoRefineType&) override { + // No refine types aren't interesting + return false; + } + + bool visit(TypeId ty, const TableType&) override { return !found; } + bool visit(TypeId ty, const MetatableType&) override { return !found; } + bool visit(TypeId ty, const FunctionType&) override { return !found; } + bool visit(TypeId ty, const UnionType&) override { return !found; } + bool visit(TypeId ty, const IntersectionType&) override { return !found; } + bool visit(TypeId ty, const NegationType&) override { return !found; } + +}; + TypeFunctionReductionResult refineTypeFunction( TypeId instance, const std::vector& typeParams, @@ -2007,14 +2039,28 @@ TypeFunctionReductionResult refineTypeFunction( } else { - if (FFlag::LuauSkipNoRefineDuringRefinement) - if (get(discriminant)) - return {target, {}}; - if (auto nt = get(discriminant)) + if (FFlag::LuauSearchForRefineableType) { - if (get(follow(nt->ty))) + // If the discriminant type is only: + // - The `*no-refine*` type or, + // - tables, metatables, unions, intersections, functions, or negations _containing_ `*no-refine*`. + // There's no point in refining against it. + ContainsRefinableType crt; + crt.traverse(discriminant); + if (!crt.found) return {target, {}}; } + else + { + if (FFlag::LuauSkipNoRefineDuringRefinement) + if (get(discriminant)) + return {target, {}}; + if (auto nt = get(discriminant)) + { + if (get(follow(nt->ty))) + return {target, {}}; + } + } // If the target type is a table, then simplification already implements the logic to deal with refinements properly since the // type of the discriminant is guaranteed to only ever be an (arbitrarily-nested) table of a single property type. diff --git a/Analysis/src/Unifier2.cpp b/Analysis/src/Unifier2.cpp index 7bfa7cb4..a224c4bc 100644 --- a/Analysis/src/Unifier2.cpp +++ b/Analysis/src/Unifier2.cpp @@ -19,6 +19,7 @@ LUAU_FASTINT(LuauTypeInferRecursionLimit) LUAU_FASTFLAGVARIABLE(LuauUnifyMetatableWithAny) +LUAU_FASTFLAG(LuauExtraFollows) namespace Luau { @@ -282,7 +283,7 @@ bool Unifier2::unifyFreeWithType(TypeId subTy, TypeId superTy) if (superArgTail) return doDefault(); - const IntersectionType* upperBoundIntersection = get(subFree->upperBound); + const IntersectionType* upperBoundIntersection = get(FFlag::LuauExtraFollows ? upperBound : subFree->upperBound); if (!upperBoundIntersection) return doDefault(); diff --git a/Ast/src/Parser.cpp b/Ast/src/Parser.cpp index a7c81dd9..65c0b24b 100644 --- a/Ast/src/Parser.cpp +++ b/Ast/src/Parser.cpp @@ -20,12 +20,11 @@ LUAU_FASTINTVARIABLE(LuauParseErrorLimit, 100) LUAU_FASTFLAGVARIABLE(LuauSolverV2) LUAU_FASTFLAGVARIABLE(LuauAllowComplexTypesInGenericParams) LUAU_FASTFLAGVARIABLE(LuauErrorRecoveryForTableTypes) -LUAU_FASTFLAGVARIABLE(LuauErrorRecoveryForClassNames) LUAU_FASTFLAGVARIABLE(LuauFixFunctionNameStartPosition) LUAU_FASTFLAGVARIABLE(LuauExtendStatEndPosWithSemicolon) LUAU_FASTFLAGVARIABLE(LuauStoreCSTData) LUAU_FASTFLAGVARIABLE(LuauPreserveUnionIntersectionNodeForLeadingTokenSingleType) -LUAU_FASTFLAGVARIABLE(LuauAstTypeGroup2) +LUAU_FASTFLAGVARIABLE(LuauAstTypeGroup3) LUAU_FASTFLAGVARIABLE(ParserNoErrorLimit) LUAU_FASTFLAGVARIABLE(LuauFixDoBlockEndLocation) @@ -1307,30 +1306,17 @@ AstStat* Parser::parseDeclaration(const Location& start, const AstArray propName = parseNameOpt("property name"); + Location propStart = lexer.current().location; + std::optional propName = parseNameOpt("property name"); - if (!propName) - break; + if (!propName) + break; - expectAndConsume(':', "property type annotation"); - AstType* propType = parseType(); - props.push_back( - AstDeclaredClassProp{propName->name, propName->location, propType, false, Location(propStart, lexer.previousLocation())} - ); - } - else - { - Location propStart = lexer.current().location; - Name propName = parseName("property name"); - expectAndConsume(':', "property type annotation"); - AstType* propType = parseType(); - props.push_back( - AstDeclaredClassProp{propName.name, propName.location, propType, false, Location(propStart, lexer.previousLocation())} - ); - } + expectAndConsume(':', "property type annotation"); + AstType* propType = parseType(); + props.push_back( + AstDeclaredClassProp{propName->name, propName->location, propType, false, Location(propStart, lexer.previousLocation())} + ); } } @@ -1732,11 +1718,13 @@ std::pair Parser::parseReturnType() if (lexer.current().type != Lexeme::SkinnyArrow && resultNames.empty()) { // If it turns out that it's just '(A)', it's possible that there are unions/intersections to follow, so fold over it. - if (FFlag::LuauAstTypeGroup2) + if (FFlag::LuauAstTypeGroup3) { - if (result.size() == 1 && varargAnnotation == nullptr) + if (result.size() == 1) { - AstType* returnType = parseTypeSuffix(allocator.alloc(location, result[0]), begin.location); + // TODO(CLI-140667): stop parsing type suffix when varargAnnotation != nullptr - this should be a parse error + AstType* inner = varargAnnotation == nullptr ? allocator.alloc(location, result[0]) : result[0]; + AstType* returnType = parseTypeSuffix(inner, begin.location); // If parseType parses nothing, then returnType->location.end only points at the last non-type-pack // type to successfully parse. We need the span of the whole annotation. @@ -2062,7 +2050,7 @@ AstTypeOrPack Parser::parseFunctionType(bool allowPack, const AstArray return {{}, allocator.alloc(begin.location, AstTypeList{paramTypes, nullptr})}; else { - if (FFlag::LuauAstTypeGroup2) + if (FFlag::LuauAstTypeGroup3) return {allocator.alloc(Location(parameterStart.location, closeArgsLocation), params[0]), {}}; else return {params[0], {}}; @@ -3572,7 +3560,7 @@ AstArray Parser::parseTypeParams(Position* openingPosition, TempV // the next lexeme is one that follows a type // (&, |, ?), then assume that this was actually a // parenthesized type. - if (FFlag::LuauAstTypeGroup2) + if (FFlag::LuauAstTypeGroup3) { auto parenthesizedType = explicitTypePack->typeList.types.data[0]; parameters.push_back( diff --git a/Compiler/src/Compiler.cpp b/Compiler/src/Compiler.cpp index 5ef2bf93..565fdd3e 100644 --- a/Compiler/src/Compiler.cpp +++ b/Compiler/src/Compiler.cpp @@ -26,6 +26,8 @@ LUAU_FASTINTVARIABLE(LuauCompileInlineThreshold, 25) LUAU_FASTINTVARIABLE(LuauCompileInlineThresholdMaxBoost, 300) LUAU_FASTINTVARIABLE(LuauCompileInlineDepth, 5) +LUAU_FASTFLAGVARIABLE(LuauSeparateCompilerTypeInfo) + namespace Luau { @@ -4269,20 +4271,40 @@ void compileOrThrow(BytecodeBuilder& bytecode, const ParseResult& parseResult, c } // computes type information for all functions based on type annotations - if (options.typeInfoLevel >= 1) - buildTypeMap( - compiler.functionTypes, - compiler.localTypes, - compiler.exprTypes, - root, - options.vectorType, - compiler.userdataTypes, - compiler.builtinTypes, - compiler.builtins, - compiler.globals, - options.libraryMemberTypeCb, - bytecode - ); + if (FFlag::LuauSeparateCompilerTypeInfo) + { + if (options.typeInfoLevel >= 1 || options.optimizationLevel >= 2) + buildTypeMap( + compiler.functionTypes, + compiler.localTypes, + compiler.exprTypes, + root, + options.vectorType, + compiler.userdataTypes, + compiler.builtinTypes, + compiler.builtins, + compiler.globals, + options.libraryMemberTypeCb, + bytecode + ); + } + else + { + if (options.typeInfoLevel >= 1) + buildTypeMap( + compiler.functionTypes, + compiler.localTypes, + compiler.exprTypes, + root, + options.vectorType, + compiler.userdataTypes, + compiler.builtinTypes, + compiler.builtins, + compiler.globals, + options.libraryMemberTypeCb, + bytecode + ); + } for (AstExprFunction* expr : functions) { diff --git a/Sources.cmake b/Sources.cmake index 1c312cb9..fcb84aeb 100644 --- a/Sources.cmake +++ b/Sources.cmake @@ -266,6 +266,7 @@ target_sources(Luau.Analysis PRIVATE Analysis/src/EmbeddedBuiltinDefinitions.cpp Analysis/src/Error.cpp Analysis/src/EqSatSimplification.cpp + Analysis/src/FileResolver.cpp Analysis/src/FragmentAutocomplete.cpp Analysis/src/Frontend.cpp Analysis/src/Generalization.cpp diff --git a/VM/include/lua.h b/VM/include/lua.h index 303d7162..06f6bd4b 100644 --- a/VM/include/lua.h +++ b/VM/include/lua.h @@ -327,9 +327,12 @@ LUA_API void lua_setuserdatadtor(lua_State* L, int tag, lua_Destructor dtor); LUA_API lua_Destructor lua_getuserdatadtor(lua_State* L, int tag); // alternative access for metatables already registered with luaL_newmetatable -LUA_API void lua_setuserdatametatable(lua_State* L, int tag, int idx); +// used by lua_newuserdatataggedwithmetatable to create tagged userdata with the associated metatable assigned +LUA_API void lua_setuserdatametatable(lua_State* L, int tag); LUA_API void lua_getuserdatametatable(lua_State* L, int tag); +LUA_API void lua_setuserdatametatable_DEPRECATED(lua_State* L, int tag, int idx); // Deprecated for incorrect behavior with 'idx != -1' + LUA_API void lua_setlightuserdataname(lua_State* L, int tag, const char* name); LUA_API const char* lua_getlightuserdataname(lua_State* L, int tag); diff --git a/VM/src/lapi.cpp b/VM/src/lapi.cpp index a956fa94..77e3b013 100644 --- a/VM/src/lapi.cpp +++ b/VM/src/lapi.cpp @@ -1470,7 +1470,16 @@ lua_Destructor lua_getuserdatadtor(lua_State* L, int tag) return L->global->udatagc[tag]; } -void lua_setuserdatametatable(lua_State* L, int tag, int idx) +void lua_setuserdatametatable(lua_State* L, int tag) +{ + api_check(L, unsigned(tag) < LUA_UTAG_LIMIT); + api_check(L, !L->global->udatamt[tag]); // reassignment not supported + api_check(L, ttistable(L->top - 1)); + L->global->udatamt[tag] = hvalue(L->top - 1); + L->top--; +} + +void lua_setuserdatametatable_DEPRECATED(lua_State* L, int tag, int idx) { api_check(L, unsigned(tag) < LUA_UTAG_LIMIT); api_check(L, !L->global->udatamt[tag]); // reassignment not supported diff --git a/tests/AnyTypeSummary.test.cpp b/tests/AnyTypeSummary.test.cpp index 471c4bb1..99a02a36 100644 --- a/tests/AnyTypeSummary.test.cpp +++ b/tests/AnyTypeSummary.test.cpp @@ -20,7 +20,7 @@ LUAU_FASTFLAG(DebugLuauMagicTypes) LUAU_FASTFLAG(StudioReportLuauAny2) LUAU_FASTFLAG(LuauTrackInteriorFreeTypesOnScope) LUAU_FASTFLAG(LuauStoreCSTData) -LUAU_FASTFLAG(LuauAstTypeGroup2) +LUAU_FASTFLAG(LuauAstTypeGroup3) LUAU_FASTFLAG(LuauDeferBidirectionalInferenceForTableAssignment) LUAU_FASTFLAG(LuauSkipNoRefineDuringRefinement) @@ -76,7 +76,7 @@ export type t8 = t0 &((true | any)->('')) LUAU_ASSERT(module->ats.typeInfo.size() == 1); LUAU_ASSERT(module->ats.typeInfo[0].code == Pattern::Alias); - if (FFlag::LuauStoreCSTData && FFlag::LuauAstTypeGroup2) + if (FFlag::LuauStoreCSTData && FFlag::LuauAstTypeGroup3) { LUAU_ASSERT(module->ats.typeInfo[0].node == "export type t8 = t0& (( true | any)->(''))"); } @@ -84,7 +84,7 @@ export type t8 = t0 &((true | any)->('')) { LUAU_ASSERT(module->ats.typeInfo[0].node == "export type t8 = t0 &(( true | any)->(''))"); } - else if (FFlag::LuauAstTypeGroup2) + else if (FFlag::LuauAstTypeGroup3) { LUAU_ASSERT(module->ats.typeInfo[0].node == "export type t8 = t0& ((true | any)->(''))"); } diff --git a/tests/AstJsonEncoder.test.cpp b/tests/AstJsonEncoder.test.cpp index 7dff66d7..9e79a5f7 100644 --- a/tests/AstJsonEncoder.test.cpp +++ b/tests/AstJsonEncoder.test.cpp @@ -11,7 +11,7 @@ using namespace Luau; -LUAU_FASTFLAG(LuauAstTypeGroup2) +LUAU_FASTFLAG(LuauAstTypeGroup3) struct JsonEncoderFixture { @@ -473,7 +473,7 @@ TEST_CASE_FIXTURE(JsonEncoderFixture, "encode_annotation") { AstStat* statement = expectParseStatement("type T = ((number) -> (string | nil)) & ((string) -> ())"); - if (FFlag::LuauAstTypeGroup2) + if (FFlag::LuauAstTypeGroup3) { std::string_view expected = R"({"type":"AstStatTypeAlias","location":"0,0 - 0,56","name":"T","generics":[],"genericPacks":[],"value":{"type":"AstTypeIntersection","location":"0,9 - 0,56","types":[{"type":"AstTypeGroup","location":"0,9 - 0,37","inner":{"type":"AstTypeFunction","location":"0,10 - 0,36","attributes":[],"generics":[],"genericPacks":[],"argTypes":{"type":"AstTypeList","types":[{"type":"AstTypeReference","location":"0,11 - 0,17","name":"number","nameLocation":"0,11 - 0,17","parameters":[]}]},"argNames":[],"returnTypes":{"type":"AstTypeList","types":[{"type":"AstTypeGroup","location":"0,22 - 0,36","inner":{"type":"AstTypeUnion","location":"0,23 - 0,35","types":[{"type":"AstTypeReference","location":"0,23 - 0,29","name":"string","nameLocation":"0,23 - 0,29","parameters":[]},{"type":"AstTypeReference","location":"0,32 - 0,35","name":"nil","nameLocation":"0,32 - 0,35","parameters":[]}]}}]}}},{"type":"AstTypeGroup","location":"0,40 - 0,56","inner":{"type":"AstTypeFunction","location":"0,41 - 0,55","attributes":[],"generics":[],"genericPacks":[],"argTypes":{"type":"AstTypeList","types":[{"type":"AstTypeReference","location":"0,42 - 0,48","name":"string","nameLocation":"0,42 - 0,48","parameters":[]}]},"argNames":[],"returnTypes":{"type":"AstTypeList","types":[]}}}]},"exported":false})"; diff --git a/tests/Autocomplete.test.cpp b/tests/Autocomplete.test.cpp index 6a8bca05..6ab06f47 100644 --- a/tests/Autocomplete.test.cpp +++ b/tests/Autocomplete.test.cpp @@ -1,5 +1,6 @@ // This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details #include "Luau/Autocomplete.h" +#include "Luau/AutocompleteTypes.h" #include "Luau/BuiltinDefinitions.h" #include "Luau/TypeInfer.h" #include "Luau/Type.h" @@ -17,6 +18,8 @@ LUAU_FASTFLAG(LuauTraceTypesInNonstrictMode2) LUAU_FASTFLAG(LuauSetMetatableDoesNotTimeTravel) LUAU_FASTINT(LuauTypeInferRecursionLimit) +LUAU_FASTFLAG(LuauExposeRequireByStringAutocomplete) + using namespace Luau; static std::optional nullCallback(std::string tag, std::optional ptr, std::optional contents) @@ -3752,6 +3755,73 @@ TEST_CASE_FIXTURE(ACFixture, "string_contents_is_available_to_callback") CHECK(isCorrect); } +TEST_CASE_FIXTURE(ACBuiltinsFixture, "require_by_string") +{ + ScopedFastFlag sff{FFlag::LuauExposeRequireByStringAutocomplete, true}; + + fileResolver.source["MainModule"] = R"( + local info = "MainModule serves as the root directory" + )"; + + fileResolver.source["MainModule/Folder"] = R"( + local info = "MainModule/Folder serves as a subdirectory" + )"; + + fileResolver.source["MainModule/Folder/Requirer"] = R"( + local res0 = require("@") + + local res1 = require(".") + local res2 = require("./") + local res3 = require("./Sib") + + local res4 = require("..") + local res5 = require("../") + local res6 = require("../Sib") + )"; + + fileResolver.source["MainModule/Folder/SiblingDependency"] = R"( + return {"result"} + )"; + + fileResolver.source["MainModule/ParentDependency"] = R"( + return {"result"} + )"; + + struct RequireCompletion + { + std::string label; + std::string insertText; + }; + + auto checkEntries = [](const AutocompleteEntryMap& entryMap, const std::vector& completions) + { + CHECK(completions.size() == entryMap.size()); + for (const auto& completion : completions) + { + CHECK(entryMap.count(completion.label)); + CHECK(entryMap.at(completion.label).insertText == completion.insertText); + } + }; + + AutocompleteResult acResult; + acResult = autocomplete("MainModule/Folder/Requirer", Position{1, 31}); + checkEntries(acResult.entryMap, {{"@defaultalias", "@defaultalias"}, {"./", "./"}, {"../", "../"}}); + + acResult = autocomplete("MainModule/Folder/Requirer", Position{3, 31}); + checkEntries(acResult.entryMap, {{"@defaultalias", "@defaultalias"}, {"./", "./"}, {"../", "../"}}); + acResult = autocomplete("MainModule/Folder/Requirer", Position{4, 32}); + checkEntries(acResult.entryMap, {{"..", "."}, {"Requirer", "./Requirer"}, {"SiblingDependency", "./SiblingDependency"}}); + acResult = autocomplete("MainModule/Folder/Requirer", Position{5, 35}); + checkEntries(acResult.entryMap, {{"..", "."}, {"Requirer", "./Requirer"}, {"SiblingDependency", "./SiblingDependency"}}); + + acResult = autocomplete("MainModule/Folder/Requirer", Position{7, 32}); + checkEntries(acResult.entryMap, {{"@defaultalias", "@defaultalias"}, {"./", "./"}, {"../", "../"}}); + acResult = autocomplete("MainModule/Folder/Requirer", Position{8, 33}); + checkEntries(acResult.entryMap, {{"..", "../.."}, {"Folder", "../Folder"}, {"ParentDependency", "../ParentDependency"}}); + acResult = autocomplete("MainModule/Folder/Requirer", Position{9, 36}); + checkEntries(acResult.entryMap, {{"..", "../.."}, {"Folder", "../Folder"}, {"ParentDependency", "../ParentDependency"}}); +} + TEST_CASE_FIXTURE(ACFixture, "autocomplete_response_perf1" * doctest::timeout(0.5)) { if (FFlag::LuauSolverV2) diff --git a/tests/Conformance.test.cpp b/tests/Conformance.test.cpp index 073fcf35..0b343771 100644 --- a/tests/Conformance.test.cpp +++ b/tests/Conformance.test.cpp @@ -477,9 +477,8 @@ void setupUserdataHelpers(lua_State* L) { // create metatable with all the metamethods luaL_newmetatable(L, "vec2"); - luaL_getmetatable(L, "vec2"); lua_pushvalue(L, -1); - lua_setuserdatametatable(L, kTagVec2, -1); + lua_setuserdatametatable(L, kTagVec2); lua_pushcfunction(L, lua_vec2_index, nullptr); lua_setfield(L, -2, "__index"); @@ -2255,12 +2254,12 @@ TEST_CASE("UserdataApi") // tagged user data with fast metatable access luaL_newmetatable(L, "udata3"); - luaL_getmetatable(L, "udata3"); - lua_setuserdatametatable(L, 50, -1); + lua_pushvalue(L, -1); + lua_setuserdatametatable(L, 50); luaL_newmetatable(L, "udata4"); - luaL_getmetatable(L, "udata4"); - lua_setuserdatametatable(L, 51, -1); + lua_pushvalue(L, -1); + lua_setuserdatametatable(L, 51); void* ud7 = lua_newuserdatatagged(L, 16, 50); lua_getuserdatametatable(L, 50); diff --git a/tests/Fixture.cpp b/tests/Fixture.cpp index dcb228a3..a63784ec 100644 --- a/tests/Fixture.cpp +++ b/tests/Fixture.cpp @@ -5,6 +5,7 @@ #include "Luau/BuiltinDefinitions.h" #include "Luau/Common.h" #include "Luau/Constraint.h" +#include "Luau/FileResolver.h" #include "Luau/ModuleResolver.h" #include "Luau/NotNull.h" #include "Luau/Parser.h" @@ -34,6 +35,110 @@ extern std::optional randomSeed; // tests/main.cpp namespace Luau { +static std::string getNodeName(const TestRequireNode* node) +{ + std::string name; + size_t lastSlash = node->moduleName.find_last_of('/'); + if (lastSlash != std::string::npos) + name = node->moduleName.substr(lastSlash + 1); + else + name = node->moduleName; + + return name; +} + +std::string TestRequireNode::getLabel() const +{ + return getNodeName(this); +} + +std::string TestRequireNode::getPathComponent() const +{ + return getNodeName(this); +} + +static std::vector splitStringBySlashes(std::string_view str) +{ + std::vector components; + size_t pos = 0; + size_t nextPos = str.find_first_of('/', pos); + if (nextPos == std::string::npos) + { + components.push_back(str); + return components; + } + while (nextPos != std::string::npos) + { + components.push_back(str.substr(pos, nextPos - pos)); + pos = nextPos + 1; + nextPos = str.find_first_of('/', pos); + } + components.push_back(str.substr(pos)); + return components; +} + +std::unique_ptr TestRequireNode::resolvePathToNode(const std::string& path) const +{ + std::vector components = splitStringBySlashes(path); + LUAU_ASSERT((components.empty() || components[0] == "." || components[0] == "..") && "Path must begin with ./ or ../ in test"); + + std::vector normalizedComponents = splitStringBySlashes(moduleName); + normalizedComponents.pop_back(); + LUAU_ASSERT(!normalizedComponents.empty() && "Must have a root module"); + + for (std::string_view component : components) + { + if (component == "..") + { + if (normalizedComponents.empty()) + LUAU_ASSERT(!"Cannot go above root module in test"); + else + normalizedComponents.pop_back(); + } + else if (!component.empty() && component != ".") + { + normalizedComponents.emplace_back(component); + } + } + + std::string moduleName; + for (size_t i = 0; i < normalizedComponents.size(); i++) + { + if (i > 0) + moduleName += '/'; + moduleName += normalizedComponents[i]; + } + + if (allSources->count(moduleName) == 0) + return nullptr; + + return std::make_unique(moduleName, allSources); +} + +std::vector> TestRequireNode::getChildren() const +{ + std::vector> result; + for (const auto& entry : *allSources) + { + if (std::string_view(entry.first).substr(0, moduleName.size()) == moduleName && entry.first.size() > moduleName.size() && + entry.first[moduleName.size()] == '/' && entry.first.find('/', moduleName.size() + 1) == std::string::npos) + { + result.push_back(std::make_unique(entry.first, allSources)); + } + } + return result; +} + +std::vector TestRequireNode::getAvailableAliases() const +{ + return {{"defaultalias"}}; +} + +std::unique_ptr TestRequireSuggester::getNode(const ModuleName& name) const +{ + return std::make_unique(name, &resolver->source); +} + std::optional TestFileResolver::resolveModuleInfo(const ModuleName& currentModuleName, const AstExpr& pathExpr) { if (auto name = pathExprToModuleName(currentModuleName, pathExpr)) diff --git a/tests/Fixture.h b/tests/Fixture.h index 60643839..1ad89b34 100644 --- a/tests/Fixture.h +++ b/tests/Fixture.h @@ -37,10 +37,45 @@ namespace Luau struct TypeChecker; +struct TestRequireNode : RequireNode +{ + TestRequireNode(ModuleName moduleName, std::unordered_map* allSources) + : moduleName(std::move(moduleName)) + , allSources(allSources) + { + } + + std::string getLabel() const override; + std::string getPathComponent() const override; + std::unique_ptr resolvePathToNode(const std::string& path) const override; + std::vector> getChildren() const override; + std::vector getAvailableAliases() const override; + + ModuleName moduleName; + std::unordered_map* allSources; +}; + +struct TestFileResolver; +struct TestRequireSuggester : RequireSuggester +{ + TestRequireSuggester(TestFileResolver* resolver) + : resolver(resolver) + { + } + + std::unique_ptr getNode(const ModuleName& name) const override; + TestFileResolver* resolver; +}; + struct TestFileResolver : FileResolver , ModuleResolver { + TestFileResolver() + : FileResolver(std::make_shared(this)) + { + } + std::optional resolveModuleInfo(const ModuleName& currentModuleName, const AstExpr& pathExpr) override; const ModulePtr getModule(const ModuleName& moduleName) const override; diff --git a/tests/FragmentAutocomplete.test.cpp b/tests/FragmentAutocomplete.test.cpp index 078ae806..45b50936 100644 --- a/tests/FragmentAutocomplete.test.cpp +++ b/tests/FragmentAutocomplete.test.cpp @@ -9,6 +9,7 @@ #include "Luau/Common.h" #include "Luau/Frontend.h" #include "Luau/AutocompleteTypes.h" +#include "Luau/ToString.h" #include "Luau/Type.h" #include "ScopedFlags.h" @@ -36,6 +37,12 @@ LUAU_FASTFLAG(LuauAutocompleteUsesModuleForTypeCompatibility) LUAU_FASTFLAG(LuauBetterCursorInCommentDetection) LUAU_FASTFLAG(LuauAllFreeTypesHaveScopes) LUAU_FASTFLAG(LuauModuleHoldsAstRoot) +LUAU_FASTFLAG(LuauTrackInteriorFreeTypesOnScope) +LUAU_FASTFLAG(LuauClonedTableAndFunctionTypesMustHaveScopes) +LUAU_FASTFLAG(LuauDisableNewSolverAssertsInMixedMode) +LUAU_FASTFLAG(LuauCloneTypeAliasBindings) +LUAU_FASTFLAG(LuauDoNotClonePersistentBindings) +LUAU_FASTFLAG(LuauCloneReturnTypePack) static std::optional nullCallback(std::string tag, std::optional ptr, std::optional contents) { @@ -65,16 +72,17 @@ struct FragmentAutocompleteFixtureImpl : BaseType { static_assert(std::is_base_of_v, "BaseType must be a descendant of Fixture"); - - ScopedFastFlag sffs[7] = { - {FFlag::LuauAutocompleteRefactorsForIncrementalAutocomplete, true}, - {FFlag::LuauIncrementalAutocompleteBugfixes, true}, - {FFlag::LuauBetterReverseDependencyTracking, true}, - {FFlag::LuauFreeTypesMustHaveBounds, true}, - {FFlag::LuauCloneIncrementalModule, true}, - {FFlag::LuauAllFreeTypesHaveScopes, true}, - {FFlag::LuauModuleHoldsAstRoot, true} - }; + ScopedFastFlag luauAutocompleteRefactorsForIncrementalAutocomplete{FFlag::LuauAutocompleteRefactorsForIncrementalAutocomplete, true}; + ScopedFastFlag luauIncrementalAutocompleteBugfixes{FFlag::LuauIncrementalAutocompleteBugfixes, true}; + ScopedFastFlag luauFreeTypesMustHaveBounds{FFlag::LuauFreeTypesMustHaveBounds, true}; + ScopedFastFlag luauCloneIncrementalModule{FFlag::LuauCloneIncrementalModule, true}; + ScopedFastFlag luauAllFreeTypesHaveScopes{FFlag::LuauAllFreeTypesHaveScopes, true}; + ScopedFastFlag luauModuleHoldsAstRoot{FFlag::LuauModuleHoldsAstRoot, true}; + ScopedFastFlag luauClonedTableAndFunctionTypesMustHaveScopes{FFlag::LuauClonedTableAndFunctionTypesMustHaveScopes, true}; + ScopedFastFlag luauDisableNewSolverAssertsInMixedMode{FFlag::LuauDisableNewSolverAssertsInMixedMode, true}; + ScopedFastFlag luauCloneTypeAliasBindings{FFlag::LuauCloneTypeAliasBindings, true}; + ScopedFastFlag luauDoNotClonePersistentBindings{FFlag::LuauDoNotClonePersistentBindings, true}; + ScopedFastFlag luauCloneReturnTypePack{FFlag::LuauCloneReturnTypePack, true}; FragmentAutocompleteFixtureImpl() : BaseType(true) @@ -2060,6 +2068,59 @@ return m autocompleteFragmentInBothSolvers(source, updated, Position{6, 2}, [](FragmentAutocompleteStatusResult& _) {}); } +TEST_CASE_FIXTURE(FragmentAutocompleteBuiltinsFixture, "duped_alias") +{ + const std::string source = R"( +type a = typeof({}) + +)"; + const std::string dest = R"( +type a = typeof({}) +l +)"; + + // Re-parsing and typechecking a type alias in the fragment that was defined in the base module will assert in ConstraintGenerator::checkAliases + // unless we don't clone it This will let the incremental pass re-generate the type binding, and we will expect to see it in the type bindings + autocompleteFragmentInBothSolvers( + source, + dest, + Position{2, 2}, + [](FragmentAutocompleteStatusResult& frag) + { + REQUIRE(frag.result); + Scope* sc = frag.result->freshScope; + CHECK(1 == sc->privateTypeBindings.count("a")); + } + ); +} + +TEST_CASE_FIXTURE(FragmentAutocompleteBuiltinsFixture, "mutually_recursive_alias") +{ + const std::string source = R"( +type U = {f : number, g : U} + +)"; + const std::string dest = R"( +type U = {f : number, g : V} +type V = {h : number, i : U?} +)"; + + // Re-parsing and typechecking a type alias in the fragment that was defined in the base module will assert in ConstraintGenerator::checkAliases + // unless we don't clone it This will let the incremental pass re-generate the type binding, and we will expect to see it in the type bindings + autocompleteFragmentInBothSolvers( + source, + dest, + Position{2, 30}, + [](FragmentAutocompleteStatusResult& frag) + { + REQUIRE(frag.result->freshScope); + Scope* scope = frag.result->freshScope; + CHECK(1 == scope->privateTypeBindings.count("U")); + CHECK(1 == scope->privateTypeBindings.count("V")); + } + ); +} + TEST_CASE_FIXTURE(FragmentAutocompleteBuiltinsFixture, "generalization_crash_when_old_solver_freetypes_have_no_bounds_set") { ScopedFastFlag sff{FFlag::LuauFreeTypesMustHaveBounds, true}; @@ -2224,4 +2285,60 @@ z:a autocompleteFragmentInBothSolvers(source, dest, Position{8, 3}, [](FragmentAutocompleteStatusResult& _) {}); } +TEST_CASE_FIXTURE(FragmentAutocompleteBuiltinsFixture, "interior_free_types_assertion_caused_by_free_type_inheriting_null_scope_from_table") +{ + ScopedFastFlag sff{FFlag::LuauTrackInteriorFreeTypesOnScope, true}; + const std::string source = R"(--!strict +local foo +local a = foo() + +local e = foo().x + +local f = foo().y + + +)"; + + const std::string dest = R"(--!strict +local foo +local a = foo() + +local e = foo().x + +local f = foo().y + +z = a.P.E +)"; + + autocompleteFragmentInBothSolvers(source, dest, Position{8, 9}, [](FragmentAutocompleteStatusResult& _) {}); +} + +TEST_CASE_FIXTURE(FragmentAutocompleteBuiltinsFixture, "NotNull_nil_scope_assertion_caused_by_free_type_inheriting_null_scope_from_table") +{ + ScopedFastFlag sff{FFlag::LuauTrackInteriorFreeTypesOnScope, false}; + const std::string source = R"(--!strict +local foo +local a = foo() + +local e = foo().x + +local f = foo().y + + +)"; + + const std::string dest = R"(--!strict +local foo +local a = foo() + +local e = foo().x + +local f = foo().y + +z = a.P.E +)"; + + autocompleteFragmentInBothSolvers(source, dest, Position{8, 9}, [](FragmentAutocompleteStatusResult& _) {}); +} + TEST_SUITE_END(); diff --git a/tests/Frontend.test.cpp b/tests/Frontend.test.cpp index da8a2230..566e770e 100644 --- a/tests/Frontend.test.cpp +++ b/tests/Frontend.test.cpp @@ -17,7 +17,6 @@ LUAU_FASTFLAG(LuauSolverV2); LUAU_FASTFLAG(DebugLuauFreezeArena) LUAU_FASTFLAG(DebugLuauMagicTypes) LUAU_FASTFLAG(LuauSelectivelyRetainDFGArena) -LUAU_FASTFLAG(LuauBetterReverseDependencyTracking) LUAU_FASTFLAG(LuauModuleHoldsAstRoot) namespace @@ -1589,8 +1588,6 @@ return {x = a, y = b, z = c} TEST_CASE_FIXTURE(FrontendFixture, "test_traverse_dependents") { - ScopedFastFlag dependencyTracking{FFlag::LuauBetterReverseDependencyTracking, true}; - fileResolver.source["game/Gui/Modules/A"] = "return {hello=5, world=true}"; fileResolver.source["game/Gui/Modules/B"] = R"( return require(game:GetService('Gui').Modules.A) @@ -1623,8 +1620,6 @@ TEST_CASE_FIXTURE(FrontendFixture, "test_traverse_dependents") TEST_CASE_FIXTURE(FrontendFixture, "test_traverse_dependents_early_exit") { - ScopedFastFlag dependencyTracking{FFlag::LuauBetterReverseDependencyTracking, true}; - fileResolver.source["game/Gui/Modules/A"] = "return {hello=5, world=true}"; fileResolver.source["game/Gui/Modules/B"] = R"( return require(game:GetService('Gui').Modules.A) @@ -1652,8 +1647,6 @@ TEST_CASE_FIXTURE(FrontendFixture, "test_traverse_dependents_early_exit") TEST_CASE_FIXTURE(FrontendFixture, "test_dependents_stored_on_node_as_graph_updates") { - ScopedFastFlag dependencyTracking{FFlag::LuauBetterReverseDependencyTracking, true}; - auto updateSource = [&](const std::string& name, const std::string& source) { fileResolver.source[name] = source; @@ -1768,7 +1761,6 @@ TEST_CASE_FIXTURE(FrontendFixture, "test_dependents_stored_on_node_as_graph_upda TEST_CASE_FIXTURE(FrontendFixture, "test_invalid_dependency_tracking_per_module_resolver") { - ScopedFastFlag dependencyTracking{FFlag::LuauBetterReverseDependencyTracking, true}; ScopedFastFlag newSolver{FFlag::LuauSolverV2, false}; fileResolver.source["game/Gui/Modules/A"] = "return {hello=5, world=true}"; diff --git a/tests/Parser.test.cpp b/tests/Parser.test.cpp index de986cd4..59805c59 100644 --- a/tests/Parser.test.cpp +++ b/tests/Parser.test.cpp @@ -18,11 +18,10 @@ LUAU_FASTINT(LuauParseErrorLimit) LUAU_FASTFLAG(LuauSolverV2) LUAU_FASTFLAG(LuauAllowComplexTypesInGenericParams) LUAU_FASTFLAG(LuauErrorRecoveryForTableTypes) -LUAU_FASTFLAG(LuauErrorRecoveryForClassNames) LUAU_FASTFLAG(LuauFixFunctionNameStartPosition) LUAU_FASTFLAG(LuauExtendStatEndPosWithSemicolon) LUAU_FASTFLAG(LuauPreserveUnionIntersectionNodeForLeadingTokenSingleType) -LUAU_FASTFLAG(LuauAstTypeGroup2) +LUAU_FASTFLAG(LuauAstTypeGroup3) LUAU_FASTFLAG(LuauFixDoBlockEndLocation) namespace @@ -372,7 +371,7 @@ TEST_CASE_FIXTURE(Fixture, "return_type_is_an_intersection_type_if_led_with_one_ AstTypeIntersection* returnAnnotation = annotation->returnTypes.types.data[0]->as(); REQUIRE(returnAnnotation != nullptr); - if (FFlag::LuauAstTypeGroup2) + if (FFlag::LuauAstTypeGroup3) CHECK(returnAnnotation->types.data[0]->as()); else CHECK(returnAnnotation->types.data[0]->as()); @@ -2127,8 +2126,6 @@ TEST_CASE_FIXTURE(Fixture, "variadic_definition_parsing") TEST_CASE_FIXTURE(Fixture, "missing_declaration_prop") { - ScopedFastFlag luauErrorRecoveryForClassNames{FFlag::LuauErrorRecoveryForClassNames, true}; - matchParseError( R"( declare class Foo @@ -2451,7 +2448,7 @@ TEST_CASE_FIXTURE(Fixture, "leading_union_intersection_with_single_type_preserve TEST_CASE_FIXTURE(Fixture, "parse_simple_ast_type_group") { - ScopedFastFlag _{FFlag::LuauAstTypeGroup2, true}; + ScopedFastFlag _{FFlag::LuauAstTypeGroup3, true}; AstStatBlock* stat = parse(R"( type Foo = (string) @@ -2469,7 +2466,7 @@ TEST_CASE_FIXTURE(Fixture, "parse_simple_ast_type_group") TEST_CASE_FIXTURE(Fixture, "parse_nested_ast_type_group") { - ScopedFastFlag _{FFlag::LuauAstTypeGroup2, true}; + ScopedFastFlag _{FFlag::LuauAstTypeGroup3, true}; AstStatBlock* stat = parse(R"( type Foo = ((string)) @@ -2490,7 +2487,7 @@ TEST_CASE_FIXTURE(Fixture, "parse_nested_ast_type_group") TEST_CASE_FIXTURE(Fixture, "parse_return_type_ast_type_group") { - ScopedFastFlag _{FFlag::LuauAstTypeGroup2, true}; + ScopedFastFlag _{FFlag::LuauAstTypeGroup3, true}; AstStatBlock* stat = parse(R"( type Foo = () -> (string) @@ -3813,7 +3810,7 @@ TEST_CASE_FIXTURE(Fixture, "grouped_function_type") auto unionTy = paramTy.type->as(); LUAU_ASSERT(unionTy); CHECK_EQ(unionTy->types.size, 2); - if (FFlag::LuauAstTypeGroup2) + if (FFlag::LuauAstTypeGroup3) { auto groupTy = unionTy->types.data[0]->as(); // (() -> ()) REQUIRE(groupTy); @@ -3926,5 +3923,16 @@ TEST_CASE_FIXTURE(Fixture, "stat_end_includes_semicolon_position") CHECK_EQ(Position{3, 22}, stat3->location.end); } +TEST_CASE_FIXTURE(Fixture, "parsing_type_suffix_for_return_type_with_variadic") +{ + ParseResult result = tryParse(R"( + function foo(): (string, ...number) | boolean + end + )"); + + // TODO(CLI-140667): this should produce a ParseError in future when we fix the invalid syntax + CHECK(result.errors.size() == 0); +} + TEST_SUITE_END(); diff --git a/tests/Transpiler.test.cpp b/tests/Transpiler.test.cpp index f179876c..397ae378 100644 --- a/tests/Transpiler.test.cpp +++ b/tests/Transpiler.test.cpp @@ -14,7 +14,7 @@ using namespace Luau; LUAU_FASTFLAG(LuauStoreCSTData) LUAU_FASTFLAG(LuauExtendStatEndPosWithSemicolon) -LUAU_FASTFLAG(LuauAstTypeGroup2); +LUAU_FASTFLAG(LuauAstTypeGroup3); LUAU_FASTFLAG(LexerFixInterpStringStart) TEST_SUITE_BEGIN("TranspilerTests"); @@ -1175,7 +1175,7 @@ TEST_CASE_FIXTURE(Fixture, "transpile_union_type_nested_3") { std::string code = "local a: nil | (string & number)"; - if (FFlag::LuauAstTypeGroup2) + if (FFlag::LuauAstTypeGroup3) CHECK_EQ("local a: (string & number)?", transpile(code, {}, true).code); else CHECK_EQ("local a: ( string & number)?", transpile(code, {}, true).code); @@ -1736,7 +1736,7 @@ TEST_CASE("transpile_types_preserve_parentheses_style") { ScopedFastFlag flags[] = { {FFlag::LuauStoreCSTData, true}, - {FFlag::LuauAstTypeGroup2, true}, + {FFlag::LuauAstTypeGroup3, true}, }; std::string code = R"( type Foo = number )"; diff --git a/tests/TypeInfer.definitions.test.cpp b/tests/TypeInfer.definitions.test.cpp index 90593891..fb200533 100644 --- a/tests/TypeInfer.definitions.test.cpp +++ b/tests/TypeInfer.definitions.test.cpp @@ -12,6 +12,7 @@ using namespace Luau; LUAU_FASTFLAG(LuauClipNestedAndRecursiveUnion) LUAU_FASTINT(LuauTypeInferRecursionLimit) LUAU_FASTFLAG(LuauPreventReentrantTypeFunctionReduction) +LUAU_FASTFLAG(LuauDontForgetToReduceUnionFunc) TEST_SUITE_BEGIN("DefinitionTests"); @@ -557,6 +558,38 @@ TEST_CASE_FIXTURE(Fixture, "recursive_redefinition_reduces_rightfully") )")); } +TEST_CASE_FIXTURE(BuiltinsFixture, "cli_142285_reduce_minted_union_func") +{ + ScopedFastFlag sffs[] = { + {FFlag::LuauSolverV2, true}, + {FFlag::LuauDontForgetToReduceUnionFunc, true} + }; + + CheckResult result = check(R"( + local function middle(a: number, b: number): number + return math.ceil((a + b) / 2 - 0.5) + end + + local function find(array: {T}, item: T): number? + local l, m, r = 1, middle(1, #array), #array + while l <= r do + if item <= array[m] then + if item == array[m] then return m end + m, r = middle(l, m-1), m-1 + else + l, m = middle(m+1, r), m+1 + end + end + return nil + end + )"); + LUAU_REQUIRE_ERROR_COUNT(3, result); + // There are three errors in the above snippet, but they should all be where + // clause needed errors. + for (const auto& e: result.errors) + CHECK(get(e)); +} + TEST_CASE_FIXTURE(Fixture, "vector3_overflow") { ScopedFastFlag _{FFlag::LuauPreventReentrantTypeFunctionReduction, true}; diff --git a/tests/TypeInfer.tables.test.cpp b/tests/TypeInfer.tables.test.cpp index 6376ff65..3d7fdaa8 100644 --- a/tests/TypeInfer.tables.test.cpp +++ b/tests/TypeInfer.tables.test.cpp @@ -27,6 +27,9 @@ LUAU_FASTFLAG(LuauFollowTableFreeze) LUAU_FASTFLAG(LuauPrecalculateMutatedFreeTypes2) LUAU_FASTFLAG(LuauDeferBidirectionalInferenceForTableAssignment) LUAU_FASTFLAG(LuauBidirectionalInferenceUpcast) +LUAU_FASTFLAG(DebugLuauAssertOnForcedConstraint) +LUAU_FASTFLAG(LuauSearchForRefineableType) +LUAU_FASTFLAG(LuauDoNotGeneralizeInTypeFunctions) TEST_SUITE_BEGIN("TableTests"); @@ -5338,4 +5341,31 @@ TEST_CASE_FIXTURE(Fixture, "missing_fields_bidirectional_inference") } +TEST_CASE_FIXTURE(Fixture, "deeply_nested_classish_inference") +{ + ScopedFastFlag sffs[] = { + {FFlag::LuauSolverV2, true}, + {FFlag::LuauDoNotGeneralizeInTypeFunctions, true}, + {FFlag::LuauSearchForRefineableType, true}, + {FFlag::DebugLuauAssertOnForcedConstraint, true}, + }; + // NOTE: This probably should be revisited after CLI-143852: we end up + // cyclic types with *tons* of overlap. + LUAU_REQUIRE_NO_ERRORS(check(R"( + local function f(part, flag, params) + local humanoid = part.Parent:FindFirstChild("Humanoid") or part.Parent.Parent:FindFirstChild("Humanoid") + if humanoid.Parent:GetAttribute("Blocking") then + if flag then + params.Found = { humanoid } + else + humanoid:Think(1) + end + else + humanoid:Think(2) + end + end + )")); +} + + TEST_SUITE_END(); diff --git a/tests/TypeInfer.test.cpp b/tests/TypeInfer.test.cpp index 77e2aeb1..6e367ce1 100644 --- a/tests/TypeInfer.test.cpp +++ b/tests/TypeInfer.test.cpp @@ -20,13 +20,15 @@ LUAU_FASTFLAG(LuauFixLocationSpanTableIndexExpr) LUAU_FASTFLAG(LuauSolverV2) LUAU_FASTFLAG(LuauInstantiateInSubtyping) LUAU_FASTINT(LuauCheckRecursionLimit) +LUAU_FASTFLAG(LuauGlobalSelfAssignmentCycle) LUAU_FASTINT(LuauNormalizeCacheLimit) LUAU_FASTINT(LuauRecursionLimit) LUAU_FASTINT(LuauTypeInferRecursionLimit) -LUAU_FASTFLAG(LuauAstTypeGroup2) +LUAU_FASTFLAG(LuauAstTypeGroup3) LUAU_FASTFLAG(LuauNewNonStrictWarnOnUnknownGlobals) LUAU_FASTFLAG(LuauInferLocalTypesInMultipleAssignments) LUAU_FASTFLAG(LuauUnifyMetatableWithAny) +LUAU_FASTFLAG(LuauExtraFollows) using namespace Luau; @@ -1202,12 +1204,12 @@ TEST_CASE_FIXTURE(Fixture, "type_infer_recursion_limit_normalizer") if (FFlag::LuauSolverV2) { CHECK(3 == result.errors.size()); - if (FFlag::LuauAstTypeGroup2) + if (FFlag::LuauAstTypeGroup3) CHECK(Location{{2, 22}, {2, 42}} == result.errors[0].location); else CHECK(Location{{2, 22}, {2, 41}} == result.errors[0].location); CHECK(Location{{3, 22}, {3, 42}} == result.errors[1].location); - if (FFlag::LuauAstTypeGroup2) + if (FFlag::LuauAstTypeGroup3) CHECK(Location{{3, 22}, {3, 41}} == result.errors[2].location); else CHECK(Location{{3, 23}, {3, 40}} == result.errors[2].location); @@ -1809,6 +1811,17 @@ TEST_CASE_FIXTURE(Fixture, "multiple_assignment") LUAU_REQUIRE_NO_ERRORS(result); } +TEST_CASE_FIXTURE(Fixture, "fuzz_global_self_assignment") +{ + ScopedFastFlag luauGlobalSelfAssignmentCycle{FFlag::LuauGlobalSelfAssignmentCycle, true}; + + // Shouldn't assert or crash + check(R"( + _ = _ + )"); +} + + TEST_CASE_FIXTURE(BuiltinsFixture, "getmetatable_works_with_any") { ScopedFastFlag _{FFlag::LuauUnifyMetatableWithAny, true}; @@ -1860,4 +1873,42 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "getmetatable_infer_any_param") CHECK_EQ("({ @metatable MT, {+ +} }) -> any", toString(requireType("check"))); } +TEST_CASE_FIXTURE(BuiltinsFixture, "fuzzer_pack_check_missing_follow") +{ + ScopedFastFlag luauExtraFollows{FFlag::LuauExtraFollows, true}; + + // Shouldn't assert or crash + check(R"( +_ = n255 +function _() +setmetatable(_)[_[xpcall(_,setmetatable(_,_()))]] /= xpcall(_,_) +_.n16(_,_)[_[_]] *= _ +end + )"); +} + +TEST_CASE_FIXTURE(Fixture, "fuzzer_unify_with_free_missing_follow") +{ + ScopedFastFlag luauExtraFollows{FFlag::LuauExtraFollows, true}; + + // Shouldn't assert or crash + check(R"( +for _ in ... do +repeat +local function l0(l0) +end +_ = l0["aaaa"] +repeat +_ = true,_("") +_ = _[_] +until _ +until _ +repeat +_ = if _ then _,_() +_ = _[_] +until _ +end + )"); +} + TEST_SUITE_END();