From 7568957f105f6100861be39984bd8961a2e08a55 Mon Sep 17 00:00:00 2001 From: Vyacheslav Egorov Date: Fri, 7 Mar 2025 15:25:00 +0200 Subject: [PATCH] Sync to upstream/release/664 --- Analysis/include/Luau/Clone.h | 5 + Analysis/include/Luau/FragmentAutocomplete.h | 5 +- Analysis/include/Luau/Frontend.h | 9 + Analysis/include/Luau/Module.h | 1 + Analysis/include/Luau/Refinement.h | 2 + Analysis/include/Luau/TableLiteralInference.h | 2 + Analysis/include/Luau/Unifier2.h | 3 + Analysis/src/AutocompleteCore.cpp | 10 + Analysis/src/Clone.cpp | 174 +++++- Analysis/src/ConstraintGenerator.cpp | 18 +- Analysis/src/ConstraintSolver.cpp | 30 +- Analysis/src/DataFlowGraph.cpp | 13 +- Analysis/src/FragmentAutocomplete.cpp | 283 ++++++++-- Analysis/src/Frontend.cpp | 272 +++++++++- Analysis/src/Normalize.cpp | 6 + Analysis/src/Refinement.cpp | 10 +- Analysis/src/Symbol.cpp | 5 +- Analysis/src/TableLiteralInference.cpp | 40 +- Analysis/src/TypeInfer.cpp | 23 +- Analysis/src/Unifier2.cpp | 15 + Ast/src/Lexer.cpp | 10 +- CLI/src/Flags.cpp | 2 +- Common/include/Luau/ExperimentalFlags.h | 5 +- tests/FragmentAutocomplete.test.cpp | 496 +++++++++++------- tests/Frontend.test.cpp | 24 +- tests/Module.test.cpp | 2 - tests/Normalize.test.cpp | 21 + tests/TypeInfer.refinements.test.cpp | 50 +- tests/TypeInfer.tables.test.cpp | 155 ++++++ tests/TypeInfer.test.cpp | 52 ++ 30 files changed, 1435 insertions(+), 308 deletions(-) diff --git a/Analysis/include/Luau/Clone.h b/Analysis/include/Luau/Clone.h index 7d5ce892..5f383dc4 100644 --- a/Analysis/include/Luau/Clone.h +++ b/Analysis/include/Luau/Clone.h @@ -40,4 +40,9 @@ TypeId clone(TypeId tp, TypeArena& dest, CloneState& cloneState); TypeFun clone(const TypeFun& typeFun, TypeArena& dest, CloneState& cloneState); Binding clone(const Binding& binding, TypeArena& dest, CloneState& cloneState); +TypePackId cloneIncremental(TypePackId tp, TypeArena& dest, CloneState& cloneState, Scope* freshScopeForFreeTypes); +TypeId cloneIncremental(TypeId typeId, TypeArena& dest, CloneState& cloneState, Scope* freshScopeForFreeTypes); +TypeFun cloneIncremental(const TypeFun& typeFun, TypeArena& dest, CloneState& cloneState, Scope* freshScopeForFreeTypes); +Binding cloneIncremental(const Binding& binding, TypeArena& dest, CloneState& cloneState, Scope* freshScopeForFreeTypes); + } // namespace Luau diff --git a/Analysis/include/Luau/FragmentAutocomplete.h b/Analysis/include/Luau/FragmentAutocomplete.h index 4c32f90b..292cb4c8 100644 --- a/Analysis/include/Luau/FragmentAutocomplete.h +++ b/Analysis/include/Luau/FragmentAutocomplete.h @@ -57,7 +57,8 @@ struct FragmentAutocompleteResult FragmentAutocompleteAncestryResult findAncestryForFragmentParse(AstStatBlock* root, const Position& cursorPos); std::optional parseFragment( - const SourceModule& srcModule, + AstStatBlock* root, + AstNameTable* names, std::string_view src, const Position& cursorPos, std::optional fragmentEndPosition @@ -98,7 +99,7 @@ struct FragmentAutocompleteStatusResult struct FragmentContext { std::string_view newSrc; - const ParseResult& newAstRoot; + const ParseResult& freshParse; std::optional opts; std::optional DEPRECATED_fragmentEndPosition; }; diff --git a/Analysis/include/Luau/Frontend.h b/Analysis/include/Luau/Frontend.h index dc443777..91e94e4a 100644 --- a/Analysis/include/Luau/Frontend.h +++ b/Analysis/include/Luau/Frontend.h @@ -32,6 +32,7 @@ struct ModuleResolver; struct ParseResult; struct HotComment; struct BuildQueueItem; +struct BuildQueueWorkState; struct FrontendCancellationToken; struct AnyTypeSummary; @@ -216,6 +217,11 @@ struct Frontend std::function task)> executeTask = {}, std::function progress = {} ); + std::vector checkQueuedModules_DEPRECATED( + std::optional optionOverride = {}, + std::function task)> executeTask = {}, + std::function progress = {} + ); std::optional getCheckResult(const ModuleName& name, bool accumulateNested, bool forAutocomplete = false); std::vector getRequiredScripts(const ModuleName& name); @@ -251,6 +257,9 @@ private: void checkBuildQueueItem(BuildQueueItem& item); void checkBuildQueueItems(std::vector& items); void recordItemResult(const BuildQueueItem& item); + void performQueueItemTask(std::shared_ptr state, size_t itemPos); + void sendQueueItemTask(std::shared_ptr state, size_t itemPos); + void sendQueueCycleItemTask(std::shared_ptr state); static LintResult classifyLints(const std::vector& warnings, const Config& config); diff --git a/Analysis/include/Luau/Module.h b/Analysis/include/Luau/Module.h index ebce78cf..521361cb 100644 --- a/Analysis/include/Luau/Module.h +++ b/Analysis/include/Luau/Module.h @@ -93,6 +93,7 @@ struct Module // Scopes and AST types refer to parse data, so we need to keep that alive std::shared_ptr allocator; std::shared_ptr names; + AstStatBlock* root = nullptr; std::vector> scopes; // never empty diff --git a/Analysis/include/Luau/Refinement.h b/Analysis/include/Luau/Refinement.h index 3fea7868..4d373ceb 100644 --- a/Analysis/include/Luau/Refinement.h +++ b/Analysis/include/Luau/Refinement.h @@ -53,6 +53,7 @@ struct Proposition { const RefinementKey* key; TypeId discriminantTy; + bool implicitFromCall; }; template @@ -69,6 +70,7 @@ struct RefinementArena RefinementId disjunction(RefinementId lhs, RefinementId rhs); RefinementId equivalence(RefinementId lhs, RefinementId rhs); RefinementId proposition(const RefinementKey* key, TypeId discriminantTy); + RefinementId implicitProposition(const RefinementKey* key, TypeId discriminantTy); private: TypedAllocator allocator; diff --git a/Analysis/include/Luau/TableLiteralInference.h b/Analysis/include/Luau/TableLiteralInference.h index 6be1e872..cb4e8786 100644 --- a/Analysis/include/Luau/TableLiteralInference.h +++ b/Analysis/include/Luau/TableLiteralInference.h @@ -14,6 +14,7 @@ namespace Luau struct TypeArena; struct BuiltinTypes; struct Unifier2; +struct Subtyping; class AstExpr; TypeId matchLiteralType( @@ -22,6 +23,7 @@ TypeId matchLiteralType( NotNull builtinTypes, NotNull arena, NotNull unifier, + NotNull subtyping, TypeId expectedType, TypeId exprType, const AstExpr* expr, diff --git a/Analysis/include/Luau/Unifier2.h b/Analysis/include/Luau/Unifier2.h index 8734aeec..c014ec9a 100644 --- a/Analysis/include/Luau/Unifier2.h +++ b/Analysis/include/Luau/Unifier2.h @@ -87,6 +87,9 @@ struct Unifier2 bool unify(const AnyType* subAny, const TableType* superTable); bool unify(const TableType* subTable, const AnyType* superAny); + bool unify(const MetatableType* subMetatable, const AnyType*); + bool unify(const AnyType*, const MetatableType* superMetatable); + // TODO think about this one carefully. We don't do unions or intersections of type packs bool unify(TypePackId subTp, TypePackId superTp); diff --git a/Analysis/src/AutocompleteCore.cpp b/Analysis/src/AutocompleteCore.cpp index faabdf47..924e4d3e 100644 --- a/Analysis/src/AutocompleteCore.cpp +++ b/Analysis/src/AutocompleteCore.cpp @@ -23,6 +23,7 @@ LUAU_FASTFLAG(LuauSolverV2) LUAU_FASTINT(LuauTypeInferIterationLimit) LUAU_FASTINT(LuauTypeInferRecursionLimit) +LUAU_FASTFLAGVARIABLE(DebugLuauMagicVariableNames) LUAU_FASTFLAGVARIABLE(LuauAutocompleteRefactorsForIncrementalAutocomplete) @@ -1343,6 +1344,15 @@ static AutocompleteContext autocompleteExpression( AstNode* node = ancestry.rbegin()[0]; + if (FFlag::DebugLuauMagicVariableNames) + { + InternalErrorReporter ice; + if (auto local = node->as(); local && local->local->name == "_luau_autocomplete_ice") + ice.ice("_luau_autocomplete_ice encountered", local->location); + if (auto global = node->as(); global && global->name == "_luau_autocomplete_ice") + ice.ice("_luau_autocomplete_ice encountered", global->location); + } + if (node->is()) { if (auto it = module.astTypes.find(node->asExpr())) diff --git a/Analysis/src/Clone.cpp b/Analysis/src/Clone.cpp index 6309fa7c..d048422d 100644 --- a/Analysis/src/Clone.cpp +++ b/Analysis/src/Clone.cpp @@ -5,6 +5,7 @@ #include "Luau/Type.h" #include "Luau/TypePack.h" #include "Luau/Unifiable.h" +#include "Luau/VisitType.h" LUAU_FASTFLAG(LuauSolverV2) LUAU_FASTFLAG(LuauFreezeIgnorePersistent) @@ -28,6 +29,8 @@ const T* get(const Kind& kind) class TypeCloner { + +protected: NotNull arena; NotNull builtinTypes; @@ -62,6 +65,8 @@ public: { } + virtual ~TypeCloner() = default; + TypeId clone(TypeId ty) { shallowClone(ty); @@ -120,6 +125,7 @@ private: } } +protected: std::optional find(TypeId ty) const { ty = follow(ty, FollowOption::DisableLazyTypeThunks); @@ -154,7 +160,7 @@ private: } public: - TypeId shallowClone(TypeId ty) + virtual TypeId shallowClone(TypeId ty) { // We want to [`Luau::follow`] but without forcing the expansion of [`LazyType`]s. ty = follow(ty, FollowOption::DisableLazyTypeThunks); @@ -181,7 +187,7 @@ public: return target; } - TypePackId shallowClone(TypePackId tp) + virtual TypePackId shallowClone(TypePackId tp) { tp = follow(tp); @@ -469,6 +475,78 @@ private: } }; +class FragmentAutocompleteTypeCloner final : public TypeCloner +{ + Scope* freeTypeReplacementScope = nullptr; + +public: + FragmentAutocompleteTypeCloner( + NotNull arena, + NotNull builtinTypes, + NotNull types, + NotNull packs, + TypeId forceTy, + TypePackId forceTp, + Scope* freeTypeReplacementScope + ) + : TypeCloner(arena, builtinTypes, types, packs, forceTy, forceTp) + , freeTypeReplacementScope(freeTypeReplacementScope) + { + LUAU_ASSERT(freeTypeReplacementScope); + } + + TypeId shallowClone(TypeId ty) override + { + // We want to [`Luau::follow`] but without forcing the expansion of [`LazyType`]s. + ty = follow(ty, FollowOption::DisableLazyTypeThunks); + + if (auto clone = find(ty)) + return *clone; + else if (ty->persistent && (!FFlag::LuauFreezeIgnorePersistent || ty != forceTy)) + return ty; + + TypeId target = arena->addType(ty->ty); + asMutable(target)->documentationSymbol = ty->documentationSymbol; + + if (auto generic = getMutable(target)) + generic->scope = nullptr; + else if (auto free = getMutable(target)) + { + free->scope = freeTypeReplacementScope; + } + else if (auto fn = getMutable(target)) + fn->scope = nullptr; + else if (auto table = getMutable(target)) + table->scope = nullptr; + + (*types)[ty] = target; + queue.emplace_back(target); + return target; + } + + TypePackId shallowClone(TypePackId tp) override + { + tp = follow(tp); + + if (auto clone = find(tp)) + return *clone; + else if (tp->persistent && (!FFlag::LuauFreezeIgnorePersistent || tp != forceTp)) + return tp; + + TypePackId target = arena->addTypePack(tp->ty); + + if (auto generic = getMutable(target)) + generic->scope = nullptr; + else if (auto free = getMutable(target)) + free->scope = freeTypeReplacementScope; + + (*packs)[tp] = target; + queue.emplace_back(target); + return target; + } +}; + + } // namespace TypePackId shallowClone(TypePackId tp, TypeArena& dest, CloneState& cloneState, bool ignorePersistent) @@ -564,4 +642,96 @@ Binding clone(const Binding& binding, TypeArena& dest, CloneState& cloneState) return b; } +TypePackId cloneIncremental(TypePackId tp, TypeArena& dest, CloneState& cloneState, Scope* freshScopeForFreeTypes) +{ + if (tp->persistent) + return tp; + + FragmentAutocompleteTypeCloner cloner{ + NotNull{&dest}, + cloneState.builtinTypes, + NotNull{&cloneState.seenTypes}, + NotNull{&cloneState.seenTypePacks}, + nullptr, + nullptr, + freshScopeForFreeTypes + }; + return cloner.clone(tp); +} + +TypeId cloneIncremental(TypeId typeId, TypeArena& dest, CloneState& cloneState, Scope* freshScopeForFreeTypes) +{ + if (typeId->persistent) + return typeId; + + FragmentAutocompleteTypeCloner cloner{ + NotNull{&dest}, + cloneState.builtinTypes, + NotNull{&cloneState.seenTypes}, + NotNull{&cloneState.seenTypePacks}, + nullptr, + nullptr, + freshScopeForFreeTypes + }; + return cloner.clone(typeId); +} + +TypeFun cloneIncremental(const TypeFun& typeFun, TypeArena& dest, CloneState& cloneState, Scope* freshScopeForFreeTypes) +{ + FragmentAutocompleteTypeCloner cloner{ + NotNull{&dest}, + cloneState.builtinTypes, + NotNull{&cloneState.seenTypes}, + NotNull{&cloneState.seenTypePacks}, + nullptr, + nullptr, + freshScopeForFreeTypes + }; + + TypeFun copy = typeFun; + + for (auto& param : copy.typeParams) + { + param.ty = cloner.clone(param.ty); + + if (param.defaultValue) + param.defaultValue = cloner.clone(*param.defaultValue); + } + + for (auto& param : copy.typePackParams) + { + param.tp = cloner.clone(param.tp); + + if (param.defaultValue) + param.defaultValue = cloner.clone(*param.defaultValue); + } + + copy.type = cloner.clone(copy.type); + + return copy; +} + +Binding cloneIncremental(const Binding& binding, TypeArena& dest, CloneState& cloneState, Scope* freshScopeForFreeTypes) +{ + FragmentAutocompleteTypeCloner cloner{ + NotNull{&dest}, + cloneState.builtinTypes, + NotNull{&cloneState.seenTypes}, + NotNull{&cloneState.seenTypePacks}, + nullptr, + nullptr, + freshScopeForFreeTypes + }; + + Binding b; + b.deprecated = binding.deprecated; + b.deprecatedSuggestion = binding.deprecatedSuggestion; + b.documentationSymbol = binding.documentationSymbol; + b.location = binding.location; + b.typeId = cloner.clone(binding.typeId); + + return b; +} + + } // namespace Luau diff --git a/Analysis/src/ConstraintGenerator.cpp b/Analysis/src/ConstraintGenerator.cpp index 962d11fa..686a78de 100644 --- a/Analysis/src/ConstraintGenerator.cpp +++ b/Analysis/src/ConstraintGenerator.cpp @@ -16,6 +16,7 @@ #include "Luau/Scope.h" #include "Luau/Simplify.h" #include "Luau/StringUtils.h" +#include "Luau/Subtyping.h" #include "Luau/TableLiteralInference.h" #include "Luau/TimeTrace.h" #include "Luau/Type.h" @@ -40,6 +41,7 @@ LUAU_FASTFLAGVARIABLE(LuauUngeneralizedTypesForRecursiveFunctions) LUAU_FASTFLAG(LuauFreeTypesMustHaveBounds) LUAU_FASTFLAGVARIABLE(LuauInferLocalTypesInMultipleAssignments) +LUAU_FASTFLAGVARIABLE(LuauDoNotLeakNilInRefinement) namespace Luau { @@ -527,7 +529,15 @@ void ConstraintGenerator::computeRefinement( // When the top-level expression is `t[x]`, we want to refine it into `nil`, not `never`. LUAU_ASSERT(refis->get(proposition->key->def)); - refis->get(proposition->key->def)->shouldAppendNilType = (sense || !eq) && containsSubscriptedDefinition(proposition->key->def); + if (FFlag::LuauDoNotLeakNilInRefinement) + { + refis->get(proposition->key->def)->shouldAppendNilType = + (sense || !eq) && containsSubscriptedDefinition(proposition->key->def) && !proposition->implicitFromCall; + } + else + { + refis->get(proposition->key->def)->shouldAppendNilType = (sense || !eq) && containsSubscriptedDefinition(proposition->key->def); + } } } @@ -1985,7 +1995,7 @@ InferencePack ConstraintGenerator::checkPack(const ScopePtr& scope, AstExprCall* if (auto key = dfg->getRefinementKey(indexExpr->expr)) { TypeId discriminantTy = arena->addType(BlockedType{}); - returnRefinements.push_back(refinementArena.proposition(key, discriminantTy)); + returnRefinements.push_back(refinementArena.implicitProposition(key, discriminantTy)); discriminantTypes.push_back(discriminantTy); } else @@ -1999,7 +2009,7 @@ InferencePack ConstraintGenerator::checkPack(const ScopePtr& scope, AstExprCall* if (auto key = dfg->getRefinementKey(arg)) { TypeId discriminantTy = arena->addType(BlockedType{}); - returnRefinements.push_back(refinementArena.proposition(key, discriminantTy)); + returnRefinements.push_back(refinementArena.implicitProposition(key, discriminantTy)); discriminantTypes.push_back(discriminantTy); } else @@ -3022,6 +3032,7 @@ Inference ConstraintGenerator::check(const ScopePtr& scope, AstExprTable* expr, else { Unifier2 unifier{arena, builtinTypes, NotNull{scope.get()}, ice}; + Subtyping sp{builtinTypes, arena, simplifier, normalizer, typeFunctionRuntime, ice}; std::vector toBlock; // This logic is incomplete as we want to re-run this // _after_ blocked types have resolved, but this @@ -3035,6 +3046,7 @@ Inference ConstraintGenerator::check(const ScopePtr& scope, AstExprTable* expr, builtinTypes, arena, NotNull{&unifier}, + NotNull{&sp}, *expectedType, ty, expr, diff --git a/Analysis/src/ConstraintSolver.cpp b/Analysis/src/ConstraintSolver.cpp index 73538532..f4032557 100644 --- a/Analysis/src/ConstraintSolver.cpp +++ b/Analysis/src/ConstraintSolver.cpp @@ -635,6 +635,7 @@ struct TypeSearcher : TypeVisitor TypeId needle; Polarity current = Polarity::Positive; + size_t count = 0; Polarity result = Polarity::None; explicit TypeSearcher(TypeId needle) @@ -649,7 +650,10 @@ struct TypeSearcher : TypeVisitor bool visit(TypeId ty) override { if (ty == needle) - result = Polarity(int(result) | int(current)); + { + ++count; + result = Polarity(size_t(result) | size_t(current)); + } return true; } @@ -749,7 +753,7 @@ void ConstraintSolver::generalizeOneType(TypeId ty) case TypeSearcher::Polarity::Negative: case TypeSearcher::Polarity::Mixed: - if (get(upperBound)) + if (get(upperBound) && ts.count > 1) { asMutable(ty)->reassign(Type{GenericType{tyScope}}); function->generics.emplace_back(ty); @@ -759,7 +763,7 @@ void ConstraintSolver::generalizeOneType(TypeId ty) break; case TypeSearcher::Polarity::Positive: - if (get(lowerBound)) + if (get(lowerBound) && ts.count > 1) { asMutable(ty)->reassign(Type{GenericType{tyScope}}); function->generics.emplace_back(ty); @@ -1370,9 +1374,17 @@ bool ConstraintSolver::tryDispatch(const FunctionCallConstraint& c, NotNull(fn)) @@ -1658,8 +1670,11 @@ bool ConstraintSolver::tryDispatch(const FunctionCheckConstraint& c, NotNullis()) { Unifier2 u2{arena, builtinTypes, constraint->scope, NotNull{&iceReporter}}; + Subtyping sp{builtinTypes, arena, simplifier, normalizer, typeFunctionRuntime, NotNull{&iceReporter}}; std::vector toBlock; - (void)matchLiteralType(c.astTypes, c.astExpectedTypes, builtinTypes, arena, NotNull{&u2}, expectedArgTy, actualArgTy, expr, toBlock); + (void)matchLiteralType( + c.astTypes, c.astExpectedTypes, builtinTypes, arena, NotNull{&u2}, NotNull{&sp}, expectedArgTy, actualArgTy, expr, toBlock + ); LUAU_ASSERT(toBlock.empty()); } } @@ -1683,8 +1698,9 @@ bool ConstraintSolver::tryDispatch(const TableCheckConstraint& c, NotNullscope, NotNull{&iceReporter}}; + Subtyping sp{builtinTypes, arena, simplifier, normalizer, typeFunctionRuntime, NotNull{&iceReporter}}; std::vector toBlock; - (void)matchLiteralType(c.astTypes, c.astExpectedTypes, builtinTypes, arena, NotNull{&u2}, c.expectedType, c.exprType, c.table, toBlock); + (void)matchLiteralType(c.astTypes, c.astExpectedTypes, builtinTypes, arena, NotNull{&u2}, NotNull{&sp}, c.expectedType, c.exprType, c.table, toBlock); LUAU_ASSERT(toBlock.empty()); return true; } diff --git a/Analysis/src/DataFlowGraph.cpp b/Analysis/src/DataFlowGraph.cpp index 46c87845..f3fc5fbc 100644 --- a/Analysis/src/DataFlowGraph.cpp +++ b/Analysis/src/DataFlowGraph.cpp @@ -911,8 +911,17 @@ DataFlowResult DataFlowGraphBuilder::visitExpr(AstExprCall* c) for (AstExpr* arg : c->args) visitExpr(arg); - // calls should be treated as subscripted. - return {defArena->freshCell(/* subscripted */ true), nullptr}; + // We treat function calls as "subscripted" as they could potentially + // return a subscripted value, consider: + // + // local function foo(tbl: {[string]: woof) + // return tbl["foobarbaz"] + // end + // + // local v = foo({}) + // + // We want to consider `v` to be subscripted here. + return {defArena->freshCell(/*subscripted=*/true)}; } DataFlowResult DataFlowGraphBuilder::visitExpr(AstExprIndexName* i) diff --git a/Analysis/src/FragmentAutocomplete.cpp b/Analysis/src/FragmentAutocomplete.cpp index 47c0c1a1..4c531954 100644 --- a/Analysis/src/FragmentAutocomplete.cpp +++ b/Analysis/src/FragmentAutocomplete.cpp @@ -33,6 +33,11 @@ LUAU_FASTFLAGVARIABLE(LuauMixedModeDefFinderTraversesTypeOf) LUAU_FASTFLAG(LuauBetterReverseDependencyTracking) LUAU_FASTFLAGVARIABLE(LuauCloneIncrementalModule) LUAU_FASTFLAGVARIABLE(LogFragmentsFromAutocomplete) +LUAU_FASTFLAGVARIABLE(LuauBetterCursorInCommentDetection) +LUAU_FASTFLAGVARIABLE(LuauAllFreeTypesHaveScopes) +LUAU_FASTFLAGVARIABLE(LuauPersistConstraintGenerationScopes) +LUAU_FASTFLAG(LuauModuleHoldsAstRoot) + namespace { template @@ -54,7 +59,7 @@ namespace Luau { template -void cloneModuleMap(TypeArena& destArena, CloneState& cloneState, const Luau::DenseHashMap& source, Luau::DenseHashMap& dest) +void cloneModuleMap_DEPRECATED(TypeArena& destArena, CloneState& cloneState, const Luau::DenseHashMap& source, Luau::DenseHashMap& dest) { for (auto [k, v] : source) { @@ -62,6 +67,21 @@ void cloneModuleMap(TypeArena& destArena, CloneState& cloneState, const Luau::De } } +template +void cloneModuleMap( + TypeArena& destArena, + CloneState& cloneState, + const Luau::DenseHashMap& source, + Luau::DenseHashMap& dest, + Scope* freshScopeForFreeType +) +{ + for (auto [k, v] : source) + { + dest[k] = Luau::cloneIncremental(v, destArena, cloneState, freshScopeForFreeType); + } +} + struct MixedModeIncrementalTCDefFinder : public AstVisitor { bool visit(AstExprLocal* local) override @@ -87,7 +107,7 @@ struct MixedModeIncrementalTCDefFinder : public AstVisitor std::vector> referencedLocalDefs; }; -void cloneAndSquashScopes( +void cloneAndSquashScopes_DEPRECATED( CloneState& cloneState, const Scope* staleScope, const ModulePtr& staleModule, @@ -144,6 +164,63 @@ void cloneAndSquashScopes( return; } +void cloneAndSquashScopes( + CloneState& cloneState, + const Scope* staleScope, + const ModulePtr& staleModule, + NotNull destArena, + NotNull dfg, + AstStatBlock* program, + Scope* destScope +) +{ + LUAU_TIMETRACE_SCOPE("Luau::cloneAndSquashScopes", "FragmentAutocomplete"); + std::vector scopes; + for (const Scope* current = staleScope; current; current = current->parent.get()) + { + scopes.emplace_back(current); + } + + // 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) + { + const Scope* curr = *it; + // Clone the lvalue types + for (const auto& [def, ty] : curr->lvalueTypes) + destScope->lvalueTypes[def] = Luau::cloneIncremental(ty, *destArena, cloneState, destScope); + // Clone the rvalueRefinements + for (const auto& [def, ty] : curr->rvalueRefinements) + destScope->rvalueRefinements[def] = Luau::cloneIncremental(ty, *destArena, cloneState, destScope); + for (const auto& [n, m] : curr->importedTypeBindings) + { + std::unordered_map importedBindingTypes; + for (const auto& [v, tf] : m) + importedBindingTypes[v] = Luau::cloneIncremental(tf, *destArena, cloneState, destScope); + destScope->importedTypeBindings[n] = std::move(importedBindingTypes); + } + + // Finally, clone up the bindings + for (const auto& [s, b] : curr->bindings) + { + destScope->bindings[s] = Luau::cloneIncremental(b, *destArena, cloneState, destScope); + } + } + + // 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) + { + if (std::optional binding = staleScope->linearSearchForBinding(loc->name.value, true)) + { + destScope->lvalueTypes[dfg->getDef(expr)] = Luau::cloneIncremental(binding->typeId, *destArena, cloneState, destScope); + } + } + return; +} + static FrontendModuleResolver& getModuleResolver(Frontend& frontend, std::optional options) { if (FFlag::LuauSolverV2 || !options) @@ -152,6 +229,16 @@ static FrontendModuleResolver& getModuleResolver(Frontend& frontend, std::option return options->forAutocomplete ? frontend.moduleResolverForAutocomplete : frontend.moduleResolver; } +bool statIsBeforePos(const AstNode* stat, const Position& cursorPos) +{ + if (FFlag::LuauIncrementalAutocompleteBugfixes) + { + return (stat->location.begin < cursorPos); + } + + return stat->location.begin < cursorPos && stat->location.begin.line < cursorPos.line; +} + FragmentAutocompleteAncestryResult findAncestryForFragmentParse(AstStatBlock* root, const Position& cursorPos) { std::vector ancestry = findAncestryAtPositionForAutocomplete(root, cursorPos); @@ -168,12 +255,25 @@ FragmentAutocompleteAncestryResult findAncestryForFragmentParse(AstStatBlock* ro { if (stat->location.begin <= cursorPos) nearestStatement = stat; - if (stat->location.begin < cursorPos && stat->location.begin.line < cursorPos.line) + } + } + } + if (!nearestStatement) + nearestStatement = ancestry[0]->asStat(); + LUAU_ASSERT(nearestStatement); + + for (AstNode* node : ancestry) + { + if (auto block = node->as()) + { + for (auto stat : block->body) + { + if (statIsBeforePos(stat, FFlag::LuauIncrementalAutocompleteBugfixes ? nearestStatement->location.begin : cursorPos)) { // This statement precedes the current one - if (auto loc = stat->as()) + if (auto statLoc = stat->as()) { - for (auto v : loc->vars) + for (auto v : statLoc->vars) { localStack.push_back(v); localMap[v->name] = v; @@ -206,11 +306,22 @@ FragmentAutocompleteAncestryResult findAncestryForFragmentParse(AstStatBlock* ro } } } + if (FFlag::LuauIncrementalAutocompleteBugfixes) + { + if (auto exprFunc = node->as()) + { + if (exprFunc->location.contains(cursorPos)) + { + for (auto v : exprFunc->args) + { + localStack.push_back(v); + localMap[v->name] = v; + } + } + } + } } - if (!nearestStatement) - nearestStatement = ancestry[0]->asStat(); - LUAU_ASSERT(nearestStatement); return {std::move(localMap), std::move(localStack), std::move(ancestry), std::move(nearestStatement)}; } @@ -296,16 +407,17 @@ ScopePtr findClosestScope(const ModulePtr& module, const AstStat* nearestStateme } std::optional parseFragment( - const SourceModule& srcModule, + AstStatBlock* root, + AstNameTable* names, std::string_view src, const Position& cursorPos, std::optional fragmentEndPosition ) { - FragmentAutocompleteAncestryResult result = findAncestryForFragmentParse(srcModule.root, cursorPos); + FragmentAutocompleteAncestryResult result = findAncestryForFragmentParse(root, cursorPos); AstStat* nearestStatement = result.nearestStatement; - const Location& rootSpan = srcModule.root->location; + const Location& rootSpan = root->location; // Did we append vs did we insert inline bool appended = cursorPos >= rootSpan.end; // statement spans multiple lines @@ -314,7 +426,7 @@ std::optional parseFragment( const Position endPos = fragmentEndPosition.value_or(cursorPos); // We start by re-parsing everything (we'll refine this as we go) - Position startPos = srcModule.root->location.begin; + Position startPos = root->location.begin; // If we added to the end of the sourceModule, use the end of the nearest location if (appended && multiline) @@ -330,7 +442,6 @@ std::optional parseFragment( auto [offsetStart, parseLength] = getDocumentOffsets(src, startPos, endPos); const char* srcStart = src.data() + offsetStart; std::string_view dbg = src.substr(offsetStart, parseLength); - const std::shared_ptr& nameTbl = srcModule.names; FragmentParseResult fragmentResult; fragmentResult.fragmentToParse = std::string(dbg.data(), parseLength); // For the duration of the incremental parse, we want to allow the name table to re-use duplicate names @@ -341,7 +452,7 @@ std::optional parseFragment( opts.allowDeclarationSyntax = false; opts.captureComments = true; opts.parseFragment = FragmentParseResumeSettings{std::move(result.localMap), std::move(result.localStack), startPos}; - ParseResult p = Luau::Parser::parse(srcStart, parseLength, *nameTbl, *fragmentResult.alloc.get(), opts); + ParseResult p = Luau::Parser::parse(srcStart, parseLength, *names, *fragmentResult.alloc, opts); // This means we threw a ParseError and we should decline to offer autocomplete here. if (p.root == nullptr) return std::nullopt; @@ -362,7 +473,7 @@ std::optional parseFragment( return fragmentResult; } -ModulePtr cloneModule(CloneState& cloneState, const ModulePtr& source, std::unique_ptr alloc) +ModulePtr cloneModule_DEPRECATED(CloneState& cloneState, const ModulePtr& source, std::unique_ptr alloc) { LUAU_TIMETRACE_SCOPE("Luau::cloneModule", "FragmentAutocomplete"); freeze(source->internalTypes); @@ -372,13 +483,38 @@ ModulePtr cloneModule(CloneState& cloneState, const ModulePtr& source, std::uniq incremental->humanReadableName = source->humanReadableName; incremental->allocator = std::move(alloc); // Clone types - cloneModuleMap(incremental->internalTypes, cloneState, source->astTypes, incremental->astTypes); - cloneModuleMap(incremental->internalTypes, cloneState, source->astTypePacks, incremental->astTypePacks); - cloneModuleMap(incremental->internalTypes, cloneState, source->astExpectedTypes, incremental->astExpectedTypes); + cloneModuleMap_DEPRECATED(incremental->internalTypes, cloneState, source->astTypes, incremental->astTypes); + cloneModuleMap_DEPRECATED(incremental->internalTypes, cloneState, source->astTypePacks, incremental->astTypePacks); + cloneModuleMap_DEPRECATED(incremental->internalTypes, cloneState, source->astExpectedTypes, incremental->astExpectedTypes); - cloneModuleMap(incremental->internalTypes, cloneState, source->astOverloadResolvedTypes, incremental->astOverloadResolvedTypes); + cloneModuleMap_DEPRECATED(incremental->internalTypes, cloneState, source->astOverloadResolvedTypes, incremental->astOverloadResolvedTypes); - cloneModuleMap(incremental->internalTypes, cloneState, source->astForInNextTypes, incremental->astForInNextTypes); + cloneModuleMap_DEPRECATED(incremental->internalTypes, cloneState, source->astForInNextTypes, incremental->astForInNextTypes); + + copyModuleMap(incremental->astScopes, source->astScopes); + + return incremental; +} + +ModulePtr cloneModule(CloneState& cloneState, const ModulePtr& source, std::unique_ptr alloc, Scope* freeTypeFreshScope) +{ + LUAU_TIMETRACE_SCOPE("Luau::cloneModule", "FragmentAutocomplete"); + freeze(source->internalTypes); + freeze(source->interfaceTypes); + ModulePtr incremental = std::make_shared(); + incremental->name = source->name; + incremental->humanReadableName = source->humanReadableName; + incremental->allocator = std::move(alloc); + // Clone types + cloneModuleMap(incremental->internalTypes, cloneState, source->astTypes, incremental->astTypes, freeTypeFreshScope); + cloneModuleMap(incremental->internalTypes, cloneState, source->astTypePacks, incremental->astTypePacks, freeTypeFreshScope); + cloneModuleMap(incremental->internalTypes, cloneState, source->astExpectedTypes, incremental->astExpectedTypes, freeTypeFreshScope); + + cloneModuleMap( + incremental->internalTypes, cloneState, source->astOverloadResolvedTypes, incremental->astOverloadResolvedTypes, freeTypeFreshScope + ); + + cloneModuleMap(incremental->internalTypes, cloneState, source->astForInNextTypes, incremental->astForInNextTypes, freeTypeFreshScope); copyModuleMap(incremental->astScopes, source->astScopes); @@ -451,8 +587,15 @@ FragmentTypeCheckResult typecheckFragment_( freeze(stale->internalTypes); freeze(stale->interfaceTypes); CloneState cloneState{frontend.builtinTypes}; - ModulePtr incrementalModule = - FFlag::LuauCloneIncrementalModule ? cloneModule(cloneState, stale, std::move(astAllocator)) : copyModule(stale, std::move(astAllocator)); + std::shared_ptr freshChildOfNearestScope = std::make_shared(closestScope); + ModulePtr incrementalModule = nullptr; + if (FFlag::LuauAllFreeTypesHaveScopes) + incrementalModule = cloneModule(cloneState, stale, std::move(astAllocator), freshChildOfNearestScope.get()); + else if (FFlag::LuauCloneIncrementalModule) + incrementalModule = cloneModule_DEPRECATED(cloneState, stale, std::move(astAllocator)); + else + incrementalModule = copyModule(stale, std::move(astAllocator)); + incrementalModule->checkedInNewSolver = true; unfreeze(incrementalModule->internalTypes); unfreeze(incrementalModule->interfaceTypes); @@ -500,23 +643,32 @@ FragmentTypeCheckResult typecheckFragment_( NotNull{&dfg}, {} }; - std::shared_ptr freshChildOfNearestScope = nullptr; + if (FFlag::LuauCloneIncrementalModule) { - freshChildOfNearestScope = std::make_shared(closestScope); incrementalModule->scopes.emplace_back(root->location, freshChildOfNearestScope); cg.rootScope = freshChildOfNearestScope.get(); - cloneAndSquashScopes( - cloneState, closestScope.get(), stale, NotNull{&incrementalModule->internalTypes}, NotNull{&dfg}, root, freshChildOfNearestScope.get() - ); + if (FFlag::LuauAllFreeTypesHaveScopes) + cloneAndSquashScopes( + cloneState, closestScope.get(), stale, NotNull{&incrementalModule->internalTypes}, NotNull{&dfg}, root, freshChildOfNearestScope.get() + ); + else + cloneAndSquashScopes_DEPRECATED( + cloneState, closestScope.get(), stale, NotNull{&incrementalModule->internalTypes}, NotNull{&dfg}, root, freshChildOfNearestScope.get() + ); cg.visitFragmentRoot(freshChildOfNearestScope, root); + + if (FFlag::LuauPersistConstraintGenerationScopes) + { + for (auto p : cg.scopes) + incrementalModule->scopes.emplace_back(std::move(p)); + } } else { // Any additions to the scope must occur in a fresh scope cg.rootScope = stale->getModuleScope().get(); - freshChildOfNearestScope = std::make_shared(closestScope); incrementalModule->scopes.emplace_back(root->location, freshChildOfNearestScope); mixedModeCompatibility(closestScope, freshChildOfNearestScope, stale, NotNull{&dfg}, root); // closest Scope -> children = { ...., freshChildOfNearestScope} @@ -529,6 +681,15 @@ FragmentTypeCheckResult typecheckFragment_( closestScope->children.pop_back(); } + if (FFlag::LuauAllFreeTypesHaveScopes) + { + if (Scope* sc = freshChildOfNearestScope.get()) + { + if (!sc->interiorFreeTypes.has_value()) + sc->interiorFreeTypes.emplace(); + } + } + /// Initialize the constraint solver and run it ConstraintSolver cs{ NotNull{&normalizer}, @@ -586,13 +747,6 @@ std::pair typecheckFragment( return {FragmentTypeCheckStatus::SkipAutocomplete, {}}; } - const SourceModule* sourceModule = frontend.getSourceModule(moduleName); - if (!sourceModule) - { - LUAU_ASSERT(!"Expected Source Module for fragment typecheck"); - return {}; - } - FrontendModuleResolver& resolver = getModuleResolver(frontend, opts); ModulePtr module = resolver.getModule(moduleName); if (!module) @@ -601,15 +755,30 @@ std::pair typecheckFragment( return {}; } - if (FFlag::LuauIncrementalAutocompleteBugfixes) + std::optional tryParse; + if (FFlag::LuauModuleHoldsAstRoot) { - if (sourceModule->allocator.get() != module->allocator.get()) - { - return {FragmentTypeCheckStatus::SkipAutocomplete, {}}; - } + tryParse = parseFragment(module->root, module->names.get(), src, cursorPos, fragmentEndPosition); } + else + { + const SourceModule* sourceModule = frontend.getSourceModule(moduleName); + if (!sourceModule) + { + LUAU_ASSERT(!"Expected Source Module for fragment typecheck"); + return {}; + } - auto tryParse = parseFragment(*sourceModule, src, cursorPos, fragmentEndPosition); + if (FFlag::LuauIncrementalAutocompleteBugfixes) + { + if (sourceModule->allocator.get() != module->allocator.get()) + { + return {FragmentTypeCheckStatus::SkipAutocomplete, {}}; + } + } + + tryParse = parseFragment(sourceModule->root, sourceModule->names.get(), src, cursorPos, fragmentEndPosition); + } if (!tryParse) return {FragmentTypeCheckStatus::SkipAutocomplete, {}}; @@ -635,17 +804,16 @@ FragmentAutocompleteStatusResult tryFragmentAutocomplete( StringCompletionCallback stringCompletionCB ) { + if (FFlag::LuauBetterCursorInCommentDetection) + { + if (isWithinComment(context.freshParse.commentLocations, cursorPosition)) + return {FragmentAutocompleteStatus::Success, std::nullopt}; + } // TODO: we should calculate fragmentEnd position here, by using context.newAstRoot and cursorPosition try { Luau::FragmentAutocompleteResult fragmentAutocomplete = Luau::fragmentAutocomplete( - frontend, - context.newSrc, - moduleName, - cursorPosition, - context.opts, - std::move(stringCompletionCB), - context.DEPRECATED_fragmentEndPosition + frontend, context.newSrc, moduleName, cursorPosition, context.opts, std::move(stringCompletionCB), context.DEPRECATED_fragmentEndPosition ); return {FragmentAutocompleteStatus::Success, std::move(fragmentAutocomplete)}; } @@ -671,16 +839,19 @@ FragmentAutocompleteResult fragmentAutocomplete( LUAU_TIMETRACE_SCOPE("Luau::fragmentAutocomplete", "FragmentAutocomplete"); LUAU_TIMETRACE_ARGUMENT("name", moduleName.c_str()); - const SourceModule* sourceModule = frontend.getSourceModule(moduleName); - if (!sourceModule) + if (!FFlag::LuauModuleHoldsAstRoot) { - LUAU_ASSERT(!"Expected Source Module for fragment typecheck"); - return {}; - } + const SourceModule* sourceModule = frontend.getSourceModule(moduleName); + if (!sourceModule) + { + LUAU_ASSERT(!"Expected Source Module for fragment typecheck"); + return {}; + } - // If the cursor is within a comment in the stale source module we should avoid providing a recommendation - if (isWithinComment(*sourceModule, fragmentEndPosition.value_or(cursorPosition))) - return {}; + // If the cursor is within a comment in the stale source module we should avoid providing a recommendation + if (isWithinComment(*sourceModule, fragmentEndPosition.value_or(cursorPosition))) + return {}; + } auto [tcStatus, tcResult] = typecheckFragment(frontend, moduleName, cursorPosition, opts, src, fragmentEndPosition); if (tcStatus == FragmentTypeCheckStatus::SkipAutocomplete) diff --git a/Analysis/src/Frontend.cpp b/Analysis/src/Frontend.cpp index 4bb801ae..98d38099 100644 --- a/Analysis/src/Frontend.cpp +++ b/Analysis/src/Frontend.cpp @@ -47,10 +47,12 @@ LUAU_FASTFLAGVARIABLE(DebugLuauForceStrictMode) LUAU_FASTFLAGVARIABLE(DebugLuauForceNonStrictMode) LUAU_DYNAMIC_FASTFLAGVARIABLE(LuauRunCustomModuleChecks, false) +LUAU_FASTFLAGVARIABLE(LuauModuleHoldsAstRoot) + LUAU_FASTFLAGVARIABLE(LuauBetterReverseDependencyTracking) +LUAU_FASTFLAGVARIABLE(LuauFixMultithreadTypecheck) LUAU_FASTFLAG(StudioReportLuauAny2) -LUAU_FASTFLAGVARIABLE(LuauStoreSolverTypeOnModule) LUAU_FASTFLAGVARIABLE(LuauSelectivelyRetainDFGArena) @@ -82,6 +84,20 @@ struct BuildQueueItem Frontend::Stats stats; }; +struct BuildQueueWorkState +{ + std::function task)> executeTask; + + std::vector buildQueueItems; + + std::mutex mtx; + std::condition_variable cv; + std::vector readyQueueItems; + + size_t processing = 0; + size_t remaining = 0; +}; + std::optional parseMode(const std::vector& hotcomments) { for (const HotComment& hc : hotcomments) @@ -481,6 +497,203 @@ std::vector Frontend::checkQueuedModules( std::function progress ) { + if (!FFlag::LuauFixMultithreadTypecheck) + { + return checkQueuedModules_DEPRECATED(optionOverride, executeTask, progress); + } + + FrontendOptions frontendOptions = optionOverride.value_or(options); + if (FFlag::LuauSolverV2) + frontendOptions.forAutocomplete = false; + + // By taking data into locals, we make sure queue is cleared at the end, even if an ICE or a different exception is thrown + std::vector currModuleQueue; + std::swap(currModuleQueue, moduleQueue); + + DenseHashSet seen{{}}; + + std::shared_ptr state = std::make_shared(); + + for (const ModuleName& name : currModuleQueue) + { + if (seen.contains(name)) + continue; + + if (!isDirty(name, frontendOptions.forAutocomplete)) + { + seen.insert(name); + continue; + } + + std::vector queue; + bool cycleDetected = parseGraph( + queue, + name, + frontendOptions.forAutocomplete, + [&seen](const ModuleName& name) + { + return seen.contains(name); + } + ); + + addBuildQueueItems(state->buildQueueItems, queue, cycleDetected, seen, frontendOptions); + } + + if (state->buildQueueItems.empty()) + return {}; + + // We need a mapping from modules to build queue slots + std::unordered_map moduleNameToQueue; + + for (size_t i = 0; i < state->buildQueueItems.size(); i++) + { + BuildQueueItem& item = state->buildQueueItems[i]; + moduleNameToQueue[item.name] = i; + } + + // Default task execution is single-threaded and immediate + if (!executeTask) + { + executeTask = [](std::function task) + { + task(); + }; + } + + state->executeTask = executeTask; + state->remaining = state->buildQueueItems.size(); + + // Record dependencies between modules + for (size_t i = 0; i < state->buildQueueItems.size(); i++) + { + BuildQueueItem& item = state->buildQueueItems[i]; + + for (const ModuleName& dep : item.sourceNode->requireSet) + { + if (auto it = sourceNodes.find(dep); it != sourceNodes.end()) + { + if (it->second->hasDirtyModule(frontendOptions.forAutocomplete)) + { + item.dirtyDependencies++; + + state->buildQueueItems[moduleNameToQueue[dep]].reverseDeps.push_back(i); + } + } + } + } + + // In the first pass, check all modules with no pending dependencies + for (size_t i = 0; i < state->buildQueueItems.size(); i++) + { + if (state->buildQueueItems[i].dirtyDependencies == 0) + sendQueueItemTask(state, i); + } + + // If not a single item was found, a cycle in the graph was hit + if (state->processing == 0) + sendQueueCycleItemTask(state); + + std::vector nextItems; + std::optional itemWithException; + bool cancelled = false; + + while (state->remaining != 0) + { + { + std::unique_lock guard(state->mtx); + + // If nothing is ready yet, wait + state->cv.wait( + guard, + [state] + { + return !state->readyQueueItems.empty(); + } + ); + + // Handle checked items + for (size_t i : state->readyQueueItems) + { + const BuildQueueItem& item = state->buildQueueItems[i]; + + // If exception was thrown, stop adding new items and wait for processing items to complete + if (item.exception) + itemWithException = i; + + if (item.module && item.module->cancelled) + cancelled = true; + + if (itemWithException || cancelled) + break; + + recordItemResult(item); + + // Notify items that were waiting for this dependency + for (size_t reverseDep : item.reverseDeps) + { + BuildQueueItem& reverseDepItem = state->buildQueueItems[reverseDep]; + + LUAU_ASSERT(reverseDepItem.dirtyDependencies != 0); + reverseDepItem.dirtyDependencies--; + + // In case of a module cycle earlier, check if unlocked an item that was already processed + if (!reverseDepItem.processing && reverseDepItem.dirtyDependencies == 0) + nextItems.push_back(reverseDep); + } + } + + LUAU_ASSERT(state->processing >= state->readyQueueItems.size()); + state->processing -= state->readyQueueItems.size(); + + LUAU_ASSERT(state->remaining >= state->readyQueueItems.size()); + state->remaining -= state->readyQueueItems.size(); + state->readyQueueItems.clear(); + } + + if (progress) + { + if (!progress(state->buildQueueItems.size() - state->remaining, state->buildQueueItems.size())) + cancelled = true; + } + + // Items cannot be submitted while holding the lock + for (size_t i : nextItems) + sendQueueItemTask(state, i); + nextItems.clear(); + + if (state->processing == 0) + { + // Typechecking might have been cancelled by user, don't return partial results + if (cancelled) + return {}; + + // We might have stopped because of a pending exception + if (itemWithException) + recordItemResult(state->buildQueueItems[*itemWithException]); + } + + // If we aren't done, but don't have anything processing, we hit a cycle + if (state->remaining != 0 && state->processing == 0) + sendQueueCycleItemTask(state); + } + + std::vector checkedModules; + checkedModules.reserve(state->buildQueueItems.size()); + + for (size_t i = 0; i < state->buildQueueItems.size(); i++) + checkedModules.push_back(std::move(state->buildQueueItems[i].name)); + + return checkedModules; +} + +std::vector Frontend::checkQueuedModules_DEPRECATED( + std::optional optionOverride, + std::function task)> executeTask, + std::function progress +) +{ + LUAU_ASSERT(!FFlag::LuauFixMultithreadTypecheck); + FrontendOptions frontendOptions = optionOverride.value_or(options); if (FFlag::LuauSolverV2) frontendOptions.forAutocomplete = false; @@ -1170,6 +1383,58 @@ void Frontend::recordItemResult(const BuildQueueItem& item) stats.filesNonstrict += item.stats.filesNonstrict; } +void Frontend::performQueueItemTask(std::shared_ptr state, size_t itemPos) +{ + BuildQueueItem& item = state->buildQueueItems[itemPos]; + + try + { + checkBuildQueueItem(item); + } + catch (...) + { + item.exception = std::current_exception(); + } + + { + std::unique_lock guard(state->mtx); + state->readyQueueItems.push_back(itemPos); + } + + state->cv.notify_one(); +} + +void Frontend::sendQueueItemTask(std::shared_ptr state, size_t itemPos) +{ + BuildQueueItem& item = state->buildQueueItems[itemPos]; + + LUAU_ASSERT(!item.processing); + item.processing = true; + + state->processing++; + + state->executeTask( + [this, state, itemPos]() + { + performQueueItemTask(state, itemPos); + } + ); +} + +void Frontend::sendQueueCycleItemTask(std::shared_ptr state) +{ + for (size_t i = 0; i < state->buildQueueItems.size(); i++) + { + BuildQueueItem& item = state->buildQueueItems[i]; + + if (!item.processing) + { + sendQueueItemTask(state, i); + break; + } + } +} + ScopePtr Frontend::getModuleEnvironment(const SourceModule& module, const Config& config, bool forAutocomplete) const { ScopePtr result; @@ -1422,8 +1687,7 @@ ModulePtr check( LUAU_TIMETRACE_ARGUMENT("name", sourceModule.humanReadableName.c_str()); ModulePtr result = std::make_shared(); - if (FFlag::LuauStoreSolverTypeOnModule) - result->checkedInNewSolver = true; + result->checkedInNewSolver = true; result->name = sourceModule.name; result->humanReadableName = sourceModule.humanReadableName; result->mode = mode; @@ -1431,6 +1695,8 @@ ModulePtr check( result->interfaceTypes.owningModule = result.get(); result->allocator = sourceModule.allocator; result->names = sourceModule.names; + if (FFlag::LuauModuleHoldsAstRoot) + result->root = sourceModule.root; iceHandler->moduleName = sourceModule.name; diff --git a/Analysis/src/Normalize.cpp b/Analysis/src/Normalize.cpp index 864c12a8..dc35a5a5 100644 --- a/Analysis/src/Normalize.cpp +++ b/Analysis/src/Normalize.cpp @@ -20,6 +20,7 @@ LUAU_FASTFLAGVARIABLE(DebugLuauCheckNormalizeInvariant) LUAU_FASTINTVARIABLE(LuauNormalizeCacheLimit, 100000) LUAU_FASTINTVARIABLE(LuauNormalizeIntersectionLimit, 200) LUAU_FASTFLAG(LuauSolverV2) +LUAU_FASTFLAGVARIABLE(LuauNormalizeNegationFix) LUAU_FASTFLAGVARIABLE(LuauFixInfiniteRecursionInNormalization) LUAU_FASTFLAGVARIABLE(LuauFixNormalizedIntersectionOfNegatedClass) @@ -3305,7 +3306,12 @@ NormalizationResult Normalizer::intersectNormalWithTy( return NormalizationResult::True; } else if (auto nt = get(t)) + { + if (FFlag::LuauNormalizeNegationFix) + here.tyvars = std::move(tyvars); + return intersectNormalWithTy(here, nt->ty, seenTablePropPairs, seenSetTypes); + } else { // TODO negated unions, intersections, table, and function. diff --git a/Analysis/src/Refinement.cpp b/Analysis/src/Refinement.cpp index e98b6e5a..3dc74a33 100644 --- a/Analysis/src/Refinement.cpp +++ b/Analysis/src/Refinement.cpp @@ -54,7 +54,15 @@ RefinementId RefinementArena::proposition(const RefinementKey* key, TypeId discr if (!key) return nullptr; - return NotNull{allocator.allocate(Proposition{key, discriminantTy})}; + return NotNull{allocator.allocate(Proposition{key, discriminantTy, false})}; +} + +RefinementId RefinementArena::implicitProposition(const RefinementKey* key, TypeId discriminantTy) +{ + if (!key) + return nullptr; + + return NotNull{allocator.allocate(Proposition{key, discriminantTy, true})}; } } // namespace Luau diff --git a/Analysis/src/Symbol.cpp b/Analysis/src/Symbol.cpp index a5117608..44a9f864 100644 --- a/Analysis/src/Symbol.cpp +++ b/Analysis/src/Symbol.cpp @@ -4,7 +4,6 @@ #include "Luau/Common.h" LUAU_FASTFLAG(LuauSolverV2) -LUAU_FASTFLAGVARIABLE(LuauSymbolEquality) namespace Luau { @@ -15,10 +14,8 @@ bool Symbol::operator==(const Symbol& rhs) const return local == rhs.local; else if (global.value) return rhs.global.value && global == rhs.global.value; // Subtlety: AstName::operator==(const char*) uses strcmp, not pointer identity. - else if (FFlag::LuauSolverV2 || FFlag::LuauSymbolEquality) - return !rhs.local && !rhs.global.value; // Reflexivity: we already know `this` Symbol is empty, so check that rhs is. else - return false; + return !rhs.local && !rhs.global.value; // Reflexivity: we already know `this` Symbol is empty, so check that rhs is. } std::string toString(const Symbol& name) diff --git a/Analysis/src/TableLiteralInference.cpp b/Analysis/src/TableLiteralInference.cpp index e5d8be04..61418b78 100644 --- a/Analysis/src/TableLiteralInference.cpp +++ b/Analysis/src/TableLiteralInference.cpp @@ -1,8 +1,12 @@ // This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details +#include "Luau/TableLiteralInference.h" + #include "Luau/Ast.h" +#include "Luau/Common.h" #include "Luau/Normalize.h" #include "Luau/Simplify.h" +#include "Luau/Subtyping.h" #include "Luau/Type.h" #include "Luau/ToString.h" #include "Luau/TypeArena.h" @@ -11,6 +15,7 @@ LUAU_FASTFLAGVARIABLE(LuauDontInPlaceMutateTableType) LUAU_FASTFLAGVARIABLE(LuauAllowNonSharedTableTypesInLiteral) +LUAU_FASTFLAGVARIABLE(LuauBidirectionalInferenceUpcast) namespace Luau { @@ -112,6 +117,7 @@ TypeId matchLiteralType( NotNull builtinTypes, NotNull arena, NotNull unifier, + NotNull subtyping, TypeId expectedType, TypeId exprType, const AstExpr* expr, @@ -133,7 +139,17 @@ TypeId matchLiteralType( * by the expected type. */ if (!isLiteral(expr)) - return exprType; + { + if (FFlag::LuauBidirectionalInferenceUpcast) + { + auto result = subtyping->isSubtype(/*subTy=*/exprType, /*superTy=*/expectedType, unifier->scope); + return result.isSubtype + ? expectedType + : exprType; + } + else + return exprType; + } expectedType = follow(expectedType); exprType = follow(exprType); @@ -210,7 +226,16 @@ TypeId matchLiteralType( return exprType; } - // TODO: lambdas + + if (FFlag::LuauBidirectionalInferenceUpcast && expr->is()) + { + // TODO: Push argument / return types into the lambda. For now, just do + // the non-literal thing: check for a subtype and upcast if valid. + auto result = subtyping->isSubtype(/*subTy=*/exprType, /*superTy=*/expectedType, unifier->scope); + return result.isSubtype + ? expectedType + : exprType; + } if (auto exprTable = expr->as()) { @@ -229,7 +254,7 @@ TypeId matchLiteralType( if (tt) { - TypeId res = matchLiteralType(astTypes, astExpectedTypes, builtinTypes, arena, unifier, *tt, exprType, expr, toBlock); + TypeId res = matchLiteralType(astTypes, astExpectedTypes, builtinTypes, arena, unifier, subtyping, *tt, exprType, expr, toBlock); parts.push_back(res); return arena->addType(UnionType{std::move(parts)}); @@ -285,6 +310,7 @@ TypeId matchLiteralType( builtinTypes, arena, unifier, + subtyping, expectedTableTy->indexer->indexResultType, propTy, item.value, @@ -300,6 +326,7 @@ TypeId matchLiteralType( keysToDelete.insert(item.key->as()); else tableTy->props.erase(keyStr); + } // If it's just an extra property and the expected type @@ -323,21 +350,21 @@ TypeId matchLiteralType( if (expectedProp.isShared()) { matchedType = - matchLiteralType(astTypes, astExpectedTypes, builtinTypes, arena, unifier, *expectedReadTy, propTy, item.value, toBlock); + matchLiteralType(astTypes, astExpectedTypes, builtinTypes, arena, unifier, subtyping, *expectedReadTy, propTy, item.value, toBlock); prop.readTy = matchedType; prop.writeTy = matchedType; } else if (expectedReadTy) { matchedType = - matchLiteralType(astTypes, astExpectedTypes, builtinTypes, arena, unifier, *expectedReadTy, propTy, item.value, toBlock); + matchLiteralType(astTypes, astExpectedTypes, builtinTypes, arena, unifier, subtyping, *expectedReadTy, propTy, item.value, toBlock); prop.readTy = matchedType; prop.writeTy.reset(); } else if (expectedWriteTy) { matchedType = - matchLiteralType(astTypes, astExpectedTypes, builtinTypes, arena, unifier, *expectedWriteTy, propTy, item.value, toBlock); + matchLiteralType(astTypes, astExpectedTypes, builtinTypes, arena, unifier, subtyping, *expectedWriteTy, propTy, item.value, toBlock); prop.readTy.reset(); prop.writeTy = matchedType; } @@ -371,6 +398,7 @@ TypeId matchLiteralType( builtinTypes, arena, unifier, + subtyping, expectedTableTy->indexer->indexResultType, *propTy, item.value, diff --git a/Analysis/src/TypeInfer.cpp b/Analysis/src/TypeInfer.cpp index 73f8b1be..832e433e 100644 --- a/Analysis/src/TypeInfer.cpp +++ b/Analysis/src/TypeInfer.cpp @@ -32,10 +32,11 @@ LUAU_FASTINTVARIABLE(LuauVisitRecursionLimit, 500) LUAU_FASTFLAG(LuauKnowsTheDataModel3) LUAU_FASTFLAGVARIABLE(DebugLuauFreezeDuringUnification) LUAU_FASTFLAG(LuauInstantiateInSubtyping) -LUAU_FASTFLAGVARIABLE(LuauOldSolverCreatesChildScopePointers) LUAU_FASTFLAG(LuauPreserveUnionIntersectionNodeForLeadingTokenSingleType) LUAU_FASTFLAG(LuauFreeTypesMustHaveBounds) +LUAU_FASTFLAG(LuauModuleHoldsAstRoot) + namespace Luau { @@ -255,6 +256,8 @@ ModulePtr TypeChecker::checkWithoutRecursionCheck(const SourceModule& module, Mo currentModule->type = module.type; currentModule->allocator = module.allocator; currentModule->names = module.names; + if (FFlag::LuauModuleHoldsAstRoot) + currentModule->root = module.root; iceHandler->moduleName = module.name; normalizer.arena = ¤tModule->internalTypes; @@ -5212,12 +5215,9 @@ LUAU_NOINLINE void TypeChecker::reportErrorCodeTooComplex(const Location& locati ScopePtr TypeChecker::childFunctionScope(const ScopePtr& parent, const Location& location, int subLevel) { ScopePtr scope = std::make_shared(parent, subLevel); - if (FFlag::LuauOldSolverCreatesChildScopePointers) - { - scope->location = location; - scope->returnType = parent->returnType; - parent->children.emplace_back(scope.get()); - } + scope->location = location; + scope->returnType = parent->returnType; + parent->children.emplace_back(scope.get()); currentModule->scopes.push_back(std::make_pair(location, scope)); return scope; @@ -5229,12 +5229,9 @@ ScopePtr TypeChecker::childScope(const ScopePtr& parent, const Location& locatio ScopePtr scope = std::make_shared(parent); scope->level = parent->level; scope->varargPack = parent->varargPack; - if (FFlag::LuauOldSolverCreatesChildScopePointers) - { - scope->location = location; - scope->returnType = parent->returnType; - parent->children.emplace_back(scope.get()); - } + scope->location = location; + scope->returnType = parent->returnType; + parent->children.emplace_back(scope.get()); currentModule->scopes.push_back(std::make_pair(location, scope)); return scope; diff --git a/Analysis/src/Unifier2.cpp b/Analysis/src/Unifier2.cpp index e63856d3..7bfa7cb4 100644 --- a/Analysis/src/Unifier2.cpp +++ b/Analysis/src/Unifier2.cpp @@ -18,6 +18,7 @@ #include LUAU_FASTINT(LuauTypeInferRecursionLimit) +LUAU_FASTFLAGVARIABLE(LuauUnifyMetatableWithAny) namespace Luau { @@ -235,6 +236,10 @@ bool Unifier2::unify(TypeId subTy, TypeId superTy) auto superMetatable = get(superTy); if (subMetatable && superMetatable) return unify(subMetatable, superMetatable); + else if (FFlag::LuauUnifyMetatableWithAny && subMetatable && superAny) + return unify(subMetatable, superAny); + else if (FFlag::LuauUnifyMetatableWithAny && subAny && superMetatable) + return unify(subAny, superMetatable); else if (subMetatable) // if we only have one metatable, unify with the inner table return unify(subMetatable->table, superTy); else if (superMetatable) // if we only have one metatable, unify with the inner table @@ -524,6 +529,16 @@ bool Unifier2::unify(const TableType* subTable, const AnyType* superAny) return true; } +bool Unifier2::unify(const MetatableType* subMetatable, const AnyType*) +{ + return unify(subMetatable->metatable, builtinTypes->anyType) && unify(subMetatable->table, builtinTypes->anyType); +} + +bool Unifier2::unify(const AnyType*, const MetatableType* superMetatable) +{ + return unify(builtinTypes->anyType, superMetatable->metatable) && unify(builtinTypes->anyType, superMetatable->table); +} + // FIXME? This should probably return an ErrorVec or an optional // rather than a boolean to signal an occurs check failure. bool Unifier2::unify(TypePackId subTp, TypePackId superTp) diff --git a/Ast/src/Lexer.cpp b/Ast/src/Lexer.cpp index 557295e0..e2760807 100644 --- a/Ast/src/Lexer.cpp +++ b/Ast/src/Lexer.cpp @@ -8,7 +8,6 @@ #include -LUAU_FASTFLAGVARIABLE(LexerResumesFromPosition2) LUAU_FASTFLAGVARIABLE(LexerFixInterpStringStart) namespace Luau @@ -342,12 +341,9 @@ Lexer::Lexer(const char* buffer, size_t bufferSize, AstNameTable& names, Positio : buffer(buffer) , bufferSize(bufferSize) , offset(0) - , line(FFlag::LexerResumesFromPosition2 ? startPosition.line : 0) - , lineOffset(FFlag::LexerResumesFromPosition2 ? 0u - startPosition.column : 0) - , lexeme( - (FFlag::LexerResumesFromPosition2 ? Location(Position(startPosition.line, startPosition.column), 0) : Location(Position(0, 0), 0)), - Lexeme::Eof - ) + , line(startPosition.line) + , lineOffset(0u - startPosition.column) + , lexeme((Location(Position(startPosition.line, startPosition.column), 0)), Lexeme::Eof) , names(names) , skipComments(false) , readNames(true) diff --git a/CLI/src/Flags.cpp b/CLI/src/Flags.cpp index ee5918c9..4bdad341 100644 --- a/CLI/src/Flags.cpp +++ b/CLI/src/Flags.cpp @@ -31,7 +31,7 @@ static void setLuauFlags(bool state) void setLuauFlagsDefault() { for (Luau::FValue* flag = Luau::FValue::list; flag; flag = flag->next) - if (strncmp(flag->name, "Luau", 4) == 0 && !Luau::isFlagExperimental(flag->name)) + if (strncmp(flag->name, "Luau", 4) == 0 && !Luau::isAnalysisFlagExperimental(flag->name)) flag->value = true; } diff --git a/Common/include/Luau/ExperimentalFlags.h b/Common/include/Luau/ExperimentalFlags.h index 68ae1e8c..ade0bf40 100644 --- a/Common/include/Luau/ExperimentalFlags.h +++ b/Common/include/Luau/ExperimentalFlags.h @@ -6,10 +6,11 @@ namespace Luau { -inline bool isFlagExperimental(const char* flag) +inline bool isAnalysisFlagExperimental(const char* flag) { // Flags in this list are disabled by default in various command-line tools. They may have behavior that is not fully final, - // or critical bugs that are found after the code has been submitted. + // or critical bugs that are found after the code has been submitted. This list is intended _only_ for flags that affect + // Luau's type checking. Flags that may change runtime behavior (e.g.: parser or VM flags) are not appropriate for this list. static const char* const kList[] = { "LuauInstantiateInSubtyping", // requires some fixes to lua-apps code "LuauFixIndexerSubtypingOrdering", // requires some small fixes to lua-apps code since this fixes a false negative diff --git a/tests/FragmentAutocomplete.test.cpp b/tests/FragmentAutocomplete.test.cpp index 9f7a5261..078ae806 100644 --- a/tests/FragmentAutocomplete.test.cpp +++ b/tests/FragmentAutocomplete.test.cpp @@ -23,9 +23,6 @@ using namespace Luau; LUAU_FASTFLAG(LuauAutocompleteRefactorsForIncrementalAutocomplete) -LUAU_FASTFLAG(LuauSymbolEquality); -LUAU_FASTFLAG(LuauStoreSolverTypeOnModule); -LUAU_FASTFLAG(LexerResumesFromPosition2) LUAU_FASTFLAG(LuauIncrementalAutocompleteCommentDetection) LUAU_FASTINT(LuauParseErrorLimit) LUAU_FASTFLAG(LuauCloneIncrementalModule) @@ -36,6 +33,9 @@ LUAU_FASTFLAG(LuauFreeTypesMustHaveBounds) LUAU_FASTFLAG(LuauBetterReverseDependencyTracking) LUAU_FASTFLAG(LuauAutocompleteUsesModuleForTypeCompatibility) +LUAU_FASTFLAG(LuauBetterCursorInCommentDetection) +LUAU_FASTFLAG(LuauAllFreeTypesHaveScopes) +LUAU_FASTFLAG(LuauModuleHoldsAstRoot) static std::optional nullCallback(std::string tag, std::optional ptr, std::optional contents) { @@ -65,13 +65,15 @@ struct FragmentAutocompleteFixtureImpl : BaseType { static_assert(std::is_base_of_v, "BaseType must be a descendant of Fixture"); - ScopedFastFlag sffs[6] = { + + ScopedFastFlag sffs[7] = { {FFlag::LuauAutocompleteRefactorsForIncrementalAutocomplete, true}, - {FFlag::LuauStoreSolverTypeOnModule, true}, - {FFlag::LuauSymbolEquality, true}, - {FFlag::LexerResumesFromPosition2, true}, {FFlag::LuauIncrementalAutocompleteBugfixes, true}, {FFlag::LuauBetterReverseDependencyTracking, true}, + {FFlag::LuauFreeTypesMustHaveBounds, true}, + {FFlag::LuauCloneIncrementalModule, true}, + {FFlag::LuauAllFreeTypesHaveScopes, true}, + {FFlag::LuauModuleHoldsAstRoot, true} }; FragmentAutocompleteFixtureImpl() @@ -79,6 +81,11 @@ struct FragmentAutocompleteFixtureImpl : BaseType { } + CheckResult checkWithOptions(const std::string& source) + { + return this->check(source, getOptions()); + } + FragmentAutocompleteAncestryResult runAutocompleteVisitor(const std::string& source, const Position& cursorPos) { ParseResult p = this->tryParse(source); // We don't care about parsing incomplete asts @@ -93,9 +100,9 @@ struct FragmentAutocompleteFixtureImpl : BaseType std::optional fragmentEndPosition = std::nullopt ) { - SourceModule* srcModule = this->getMainSourceModule(); + ModulePtr module = this->getMainModule(getOptions().forAutocomplete); std::string_view srcString = document; - return Luau::parseFragment(*srcModule, srcString, cursorPos, fragmentEndPosition); + return Luau::parseFragment(module->root, module->names.get(), srcString, cursorPos, fragmentEndPosition); } CheckResult checkOldSolver(const std::string& source) @@ -114,14 +121,19 @@ struct FragmentAutocompleteFixtureImpl : BaseType return result; } - FragmentAutocompleteResult autocompleteFragment( + FragmentAutocompleteStatusResult autocompleteFragment( const std::string& document, Position cursorPos, std::optional fragmentEndPosition = std::nullopt ) { - FrontendOptions options; - return Luau::fragmentAutocomplete(this->frontend, document, "MainModule", cursorPos, getOptions(), nullCallback, fragmentEndPosition); + ParseOptions parseOptions; + parseOptions.captureComments = true; + SourceModule source; + ParseResult parseResult = Parser::parse(document.c_str(), document.length(), *source.names, *source.allocator, parseOptions); + FrontendOptions options = getOptions(); + FragmentContext context{document, parseResult, options, fragmentEndPosition}; + return Luau::tryFragmentAutocomplete(this->frontend, "MainModule", cursorPos, context, nullCallback); } @@ -129,14 +141,14 @@ struct FragmentAutocompleteFixtureImpl : BaseType const std::string& document, const std::string& updated, Position cursorPos, - std::function assertions, + std::function assertions, std::optional fragmentEndPosition = std::nullopt ) { ScopedFastFlag sff{FFlag::LuauSolverV2, true}; - this->check(document); + this->check(document, getOptions()); - FragmentAutocompleteResult result = autocompleteFragment(updated, cursorPos, fragmentEndPosition); + FragmentAutocompleteStatusResult result = autocompleteFragment(updated, cursorPos, fragmentEndPosition); assertions(result); ScopedFastFlag _{FFlag::LuauSolverV2, false}; @@ -156,14 +168,20 @@ struct FragmentAutocompleteFixtureImpl : BaseType return Luau::typecheckFragment(this->frontend, module, cursorPos, getOptions(), document, fragmentEndPosition); } - FragmentAutocompleteResult autocompleteFragmentForModule( + FragmentAutocompleteStatusResult autocompleteFragmentForModule( const ModuleName& module, const std::string& document, Position cursorPos, std::optional fragmentEndPosition = std::nullopt ) { - return Luau::fragmentAutocomplete(this->frontend, document, module, cursorPos, getOptions(), nullCallback, fragmentEndPosition); + ParseOptions parseOptions; + parseOptions.captureComments = true; + SourceModule source; + ParseResult parseResult = Parser::parse(document.c_str(), document.length(), *source.names, *source.allocator, parseOptions); + FrontendOptions options; + FragmentContext context{document, parseResult, options, fragmentEndPosition}; + return Luau::tryFragmentAutocomplete(this->frontend, module, cursorPos, context, nullCallback); } }; @@ -316,9 +334,9 @@ local function bar() return x + foo() end ); CHECK_EQ(8, result.ancestry.size()); - CHECK_EQ(2, result.localStack.size()); + CHECK_EQ(3, result.localStack.size()); CHECK_EQ(result.localMap.size(), result.localStack.size()); - CHECK_EQ("x", std::string(result.localStack.back()->name.value)); + CHECK_EQ("bar", std::string(result.localStack.back()->name.value)); auto returnSt = result.nearestStatement->as(); CHECK(returnSt != nullptr); } @@ -330,7 +348,7 @@ TEST_SUITE_BEGIN("FragmentAutocompleteParserTests"); TEST_CASE_FIXTURE(FragmentAutocompleteFixture, "thrown_parse_error_leads_to_null_root") { - check("type A = "); + checkWithOptions("type A = "); ScopedFastInt sfi{FInt::LuauParseErrorLimit, 1}; auto fragment = parseFragment("type A = <>function<> more garbage here", Position(0, 39)); CHECK(fragment == std::nullopt); @@ -339,7 +357,7 @@ TEST_CASE_FIXTURE(FragmentAutocompleteFixture, "thrown_parse_error_leads_to_null TEST_CASE_FIXTURE(FragmentAutocompleteFixture, "local_initializer") { ScopedFastFlag sff{FFlag::LuauSolverV2, true}; - check("local a ="); + checkWithOptions("local a ="); auto fragment = parseFragment("local a =", Position(0, 10)); REQUIRE(fragment.has_value()); @@ -350,7 +368,7 @@ TEST_CASE_FIXTURE(FragmentAutocompleteFixture, "local_initializer") TEST_CASE_FIXTURE(FragmentAutocompleteFixture, "statement_in_empty_fragment_is_non_null") { ScopedFastFlag sff{FFlag::LuauSolverV2, true}; - auto res = check(R"( + auto res = checkWithOptions(R"( )"); @@ -374,7 +392,7 @@ TEST_CASE_FIXTURE(FragmentAutocompleteFixture, "statement_in_empty_fragment_is_n TEST_CASE_FIXTURE(FragmentAutocompleteFixture, "can_parse_complete_fragments") { ScopedFastFlag sff{FFlag::LuauSolverV2, true}; - auto res = check( + auto res = checkWithOptions( R"( local x = 4 local y = 5 @@ -421,7 +439,7 @@ local z = x + y TEST_CASE_FIXTURE(FragmentAutocompleteFixture, "can_parse_fragments_in_line") { ScopedFastFlag sff{FFlag::LuauSolverV2, true}; - auto res = check( + auto res = checkWithOptions( R"( local x = 4 local y = 5 @@ -467,7 +485,7 @@ local y = 5 TEST_CASE_FIXTURE(FragmentAutocompleteFixture, "can_parse_in_correct_scope") { ScopedFastFlag sff{FFlag::LuauSolverV2, true}; - check(R"( + checkWithOptions(R"( local myLocal = 4 function abc() local myInnerLocal = 1 @@ -494,7 +512,7 @@ TEST_CASE_FIXTURE(FragmentAutocompleteFixture, "can_parse_in_correct_scope") TEST_CASE_FIXTURE(FragmentAutocompleteFixture, "can_parse_single_line_fragment_override") { ScopedFastFlag sff{FFlag::LuauSolverV2, true}; - auto res = check("function abc(foo: string) end"); + auto res = checkWithOptions("function abc(foo: string) end"); LUAU_REQUIRE_NO_ERRORS(res); @@ -556,7 +574,7 @@ TEST_CASE_FIXTURE(FragmentAutocompleteFixture, "can_parse_multi_line_fragment_ov { ScopedFastFlag sff{FFlag::LuauSolverV2, true}; - auto res = check("function abc(foo: string) end"); + auto res = checkWithOptions("function abc(foo: string) end"); LUAU_REQUIRE_NO_ERRORS(res); @@ -605,10 +623,15 @@ t frontend.check("game/A", opts); CHECK_NE(frontend.moduleResolverForAutocomplete.getModule("game/A"), nullptr); CHECK_EQ(frontend.moduleResolver.getModule("game/A"), nullptr); + ParseOptions parseOptions; + parseOptions.captureComments = true; + SourceModule sourceMod; + ParseResult parseResult = Parser::parse(source.c_str(), source.length(), *sourceMod.names, *sourceMod.allocator, parseOptions); + FragmentContext context{source, parseResult, opts, std::nullopt}; - - FragmentAutocompleteResult result = Luau::fragmentAutocomplete(frontend, source, "game/A", Position{2, 1}, opts, nullCallback); - CHECK_EQ("game/A", result.incrementalModule->name); + FragmentAutocompleteStatusResult frag = Luau::tryFragmentAutocomplete(frontend, "game/A", Position{2, 1}, context, nullCallback); + REQUIRE(frag.result); + CHECK_EQ("game/A", frag.result->incrementalModule->name); CHECK_NE(frontend.moduleResolverForAutocomplete.getModule("game/A"), nullptr); CHECK_EQ(frontend.moduleResolver.getModule("game/A"), nullptr); } @@ -621,7 +644,7 @@ TEST_SUITE_BEGIN("FragmentAutocompleteTypeCheckerTests"); TEST_CASE_FIXTURE(FragmentAutocompleteFixture, "can_typecheck_simple_fragment") { ScopedFastFlag sff{FFlag::LuauSolverV2, true}; - auto res = check( + auto res = checkWithOptions( R"( local x = 4 local y = 5 @@ -647,7 +670,7 @@ local z = x + y TEST_CASE_FIXTURE(FragmentAutocompleteFixture, "can_typecheck_fragment_inserted_inline") { ScopedFastFlag sff{FFlag::LuauSolverV2, true}; - auto res = check( + auto res = checkWithOptions( R"( local x = 4 local y = 5 @@ -742,16 +765,17 @@ tbl. )", Position{2, 5} ); + REQUIRE(fragment.result); + LUAU_ASSERT(fragment.result->freshScope); - LUAU_ASSERT(fragment.freshScope); - - CHECK_EQ(1, fragment.acResults.entryMap.size()); - CHECK(fragment.acResults.entryMap.count("abc")); - CHECK_EQ(AutocompleteContext::Property, fragment.acResults.context); + CHECK_EQ(1, fragment.result->acResults.entryMap.size()); + CHECK(fragment.result->acResults.entryMap.count("abc")); + CHECK_EQ(AutocompleteContext::Property, fragment.result->acResults.context); } TEST_CASE_FIXTURE(FragmentAutocompleteFixture, "typecheck_fragment_handles_stale_module") { + ScopedFastFlag sff(FFlag::LuauModuleHoldsAstRoot, false); const std::string sourceName = "MainModule"; fileResolver.source[sourceName] = "local x = 5"; @@ -816,7 +840,7 @@ TEST_CASE_FIXTURE(FragmentAutocompleteFixture, "multiple_fragment_autocomplete") auto checkAndExamine = [&](const std::string& src, const std::string& idName, const std::string& idString) { - check(src, getOptions()); + checkWithOptions(src); auto id = getType(idName, true); LUAU_ASSERT(id); CHECK_EQ(Luau::toString(*id, opt), idString); @@ -835,8 +859,9 @@ TEST_CASE_FIXTURE(FragmentAutocompleteFixture, "multiple_fragment_autocomplete") const std::string& srcIdString, const std::string& fragIdString) { - FragmentAutocompleteResult result = autocompleteFragment(updated, pos, std::nullopt); - auto fragId = getTypeFromModule(result.incrementalModule, idName); + FragmentAutocompleteStatusResult frag = autocompleteFragment(updated, pos, std::nullopt); + REQUIRE(frag.result); + auto fragId = getTypeFromModule(frag.result->incrementalModule, idName); LUAU_ASSERT(fragId); CHECK_EQ(Luau::toString(*fragId, opt), fragIdString); @@ -890,13 +915,14 @@ tbl. source, updated, Position{2, 5}, - [](FragmentAutocompleteResult& fragment) + [](FragmentAutocompleteStatusResult& fragment) { - LUAU_ASSERT(fragment.freshScope); + REQUIRE(fragment.result); + auto acResults = fragment.result->acResults; - CHECK_EQ(1, fragment.acResults.entryMap.size()); - CHECK(fragment.acResults.entryMap.count("abc")); - CHECK_EQ(AutocompleteContext::Property, fragment.acResults.context); + CHECK_EQ(1, acResults.entryMap.size()); + CHECK(acResults.entryMap.count("abc")); + CHECK_EQ(AutocompleteContext::Property, acResults.context); } ); } @@ -914,14 +940,15 @@ tbl.abc. source, updated, Position{2, 8}, - [](FragmentAutocompleteResult& fragment) + [](FragmentAutocompleteStatusResult& fragment) { - LUAU_ASSERT(fragment.freshScope); + REQUIRE(fragment.result); + LUAU_ASSERT(fragment.result->freshScope); - CHECK_EQ(2, fragment.acResults.entryMap.size()); - CHECK(fragment.acResults.entryMap.count("def")); - CHECK(fragment.acResults.entryMap.count("egh")); - CHECK_EQ(fragment.acResults.context, AutocompleteContext::Property); + CHECK_EQ(2, fragment.result->acResults.entryMap.size()); + CHECK(fragment.result->acResults.entryMap.count("def")); + CHECK(fragment.result->acResults.entryMap.count("egh")); + CHECK_EQ(fragment.result->acResults.context, AutocompleteContext::Property); } ); } @@ -943,9 +970,10 @@ end text, text, Position{0, 0}, - [](FragmentAutocompleteResult& fragment) + [](FragmentAutocompleteStatusResult& fragment) { - auto strings = fragment.acResults.entryMap; + REQUIRE(fragment.result); + auto strings = fragment.result->acResults.entryMap; CHECK(strings.count("f1") == 0); CHECK(strings.count("a1") == 0); CHECK(strings.count("l1") == 0); @@ -961,9 +989,10 @@ end text, text, Position{0, 22}, - [](FragmentAutocompleteResult& fragment) + [](FragmentAutocompleteStatusResult& fragment) { - auto strings = fragment.acResults.entryMap; + REQUIRE(fragment.result); + auto strings = fragment.result->acResults.entryMap; CHECK(strings.count("f1") != 0); CHECK(strings.count("a1") != 0); CHECK(strings.count("l1") == 0); @@ -979,9 +1008,10 @@ end text, text, Position{1, 17}, - [](FragmentAutocompleteResult& fragment) + [](FragmentAutocompleteStatusResult& fragment) { - auto strings = fragment.acResults.entryMap; + REQUIRE(fragment.result); + auto strings = fragment.result->acResults.entryMap; CHECK(strings.count("f1") != 0); CHECK(strings.count("a1") != 0); CHECK(strings.count("l1") != 0); @@ -997,9 +1027,10 @@ end text, text, Position{2, 11}, - [](FragmentAutocompleteResult& fragment) + [](FragmentAutocompleteStatusResult& fragment) { - auto strings = fragment.acResults.entryMap; + REQUIRE(fragment.result); + auto strings = fragment.result->acResults.entryMap; CHECK(strings.count("f1") != 0); CHECK(strings.count("a1") != 0); CHECK(strings.count("l1") != 0); @@ -1015,9 +1046,10 @@ end text, text, Position{4, 0}, - [](FragmentAutocompleteResult& fragment) + [](FragmentAutocompleteStatusResult& fragment) { - auto strings = fragment.acResults.entryMap; + REQUIRE(fragment.result); + auto strings = fragment.result->acResults.entryMap; CHECK(strings.count("f1") != 0); // FIXME: RIDE-11123: This should be zero counts of `a1`. CHECK(strings.count("a1") != 0); @@ -1034,9 +1066,10 @@ end text, text, Position{6, 17}, - [](FragmentAutocompleteResult& fragment) + [](FragmentAutocompleteStatusResult& fragment) { - auto strings = fragment.acResults.entryMap; + REQUIRE(fragment.result); + auto strings = fragment.result->acResults.entryMap; CHECK(strings.count("f1") != 0); CHECK(strings.count("a1") == 0); CHECK(strings.count("l1") == 0); @@ -1052,9 +1085,10 @@ end text, text, Position{8, 4}, - [](FragmentAutocompleteResult& fragment) + [](FragmentAutocompleteStatusResult& fragment) { - auto strings = fragment.acResults.entryMap; + REQUIRE(fragment.result); + auto strings = fragment.result->acResults.entryMap; CHECK(strings.count("f1") != 0); CHECK(strings.count("a1") == 0); CHECK(strings.count("l1") == 0); @@ -1089,13 +1123,13 @@ end source, updated, Position{4, 15}, - [](FragmentAutocompleteResult& fragment) + [](FragmentAutocompleteStatusResult& fragment) { - LUAU_ASSERT(fragment.freshScope); - - REQUIRE(fragment.acResults.entryMap.count("Table")); - REQUIRE(fragment.acResults.entryMap["Table"].type); - const TableType* tv = get(follow(*fragment.acResults.entryMap["Table"].type)); + REQUIRE(fragment.result); + LUAU_ASSERT(fragment.result->freshScope); + REQUIRE(fragment.result->acResults.entryMap.count("Table")); + REQUIRE(fragment.result->acResults.entryMap["Table"].type); + const TableType* tv = get(follow(*fragment.result->acResults.entryMap["Table"].type)); REQUIRE(tv); CHECK(tv->props.count("x")); } @@ -1112,10 +1146,11 @@ end source, source, Position{2, 0}, - [](FragmentAutocompleteResult& fragment) + [](FragmentAutocompleteStatusResult& fragment) { - CHECK(fragment.acResults.entryMap.count("foo")); - CHECK_EQ(AutocompleteContext::Statement, fragment.acResults.context); + REQUIRE(fragment.result); + CHECK(fragment.result->acResults.entryMap.count("foo")); + CHECK_EQ(AutocompleteContext::Statement, fragment.result->acResults.context); } ); } @@ -1131,26 +1166,26 @@ foo("abc") source, source, Position{2, 6}, - [](FragmentAutocompleteResult& fragment) + [](FragmentAutocompleteStatusResult& fragment) { - CHECK(fragment.acResults.entryMap.empty()); - CHECK_EQ(AutocompleteContext::String, fragment.acResults.context); + REQUIRE(fragment.result); + CHECK(fragment.result->acResults.entryMap.empty()); + CHECK_EQ(AutocompleteContext::String, fragment.result->acResults.context); }, Position{2, 9} ); } -// Start compatibility tests! - TEST_CASE_FIXTURE(FragmentAutocompleteFixture, "empty_program") { autocompleteFragmentInBothSolvers( "", "", Position{0, 1}, - [](FragmentAutocompleteResult& frag) + [](FragmentAutocompleteStatusResult& frag) { - auto ac = frag.acResults; + REQUIRE(frag.result); + auto ac = frag.result->acResults; CHECK(ac.entryMap.count("table")); CHECK(ac.entryMap.count("math")); CHECK_EQ(ac.context, AutocompleteContext::Statement); @@ -1165,9 +1200,10 @@ TEST_CASE_FIXTURE(FragmentAutocompleteFixture, "local_initializer") source, source, Position{0, 9}, - [](FragmentAutocompleteResult& frag) + [](FragmentAutocompleteStatusResult& frag) { - auto ac = frag.acResults; + REQUIRE(frag.result); + auto ac = frag.result->acResults; CHECK(ac.entryMap.count("table")); CHECK(ac.entryMap.count("math")); @@ -1184,9 +1220,10 @@ TEST_CASE_FIXTURE(FragmentAutocompleteFixture, "leave_numbers_alone") source, source, Position{0, 12}, - [](FragmentAutocompleteResult& frag) + [](FragmentAutocompleteStatusResult& frag) { - auto ac = frag.acResults; + REQUIRE(frag.result); + auto ac = frag.result->acResults; CHECK(ac.entryMap.empty()); CHECK_EQ(ac.context, AutocompleteContext::Unknown); } @@ -1201,9 +1238,10 @@ TEST_CASE_FIXTURE(FragmentAutocompleteFixture, "user_defined_globals") source, source, Position{0, 18}, - [](FragmentAutocompleteResult& frag) + [](FragmentAutocompleteStatusResult& frag) { - auto ac = frag.acResults; + REQUIRE(frag.result); + auto ac = frag.result->acResults; CHECK(ac.entryMap.count("myLocal")); CHECK(ac.entryMap.count("table")); @@ -1228,9 +1266,10 @@ TEST_CASE_FIXTURE(FragmentAutocompleteFixture, "dont_suggest_local_before_its_de source, source, Position{3, 0}, - [](FragmentAutocompleteResult& fragment) + [](FragmentAutocompleteStatusResult& fragment) { - auto ac = fragment.acResults; + REQUIRE(fragment.result); + auto ac = fragment.result->acResults; CHECK(ac.entryMap.count("myLocal")); LUAU_CHECK_HAS_NO_KEY(ac.entryMap, "myInnerLocal"); } @@ -1240,9 +1279,10 @@ TEST_CASE_FIXTURE(FragmentAutocompleteFixture, "dont_suggest_local_before_its_de source, source, Position{4, 0}, - [](FragmentAutocompleteResult& fragment) + [](FragmentAutocompleteStatusResult& fragment) { - auto ac = fragment.acResults; + REQUIRE(fragment.result); + auto ac = fragment.result->acResults; CHECK(ac.entryMap.count("myLocal")); CHECK(ac.entryMap.count("myInnerLocal")); } @@ -1253,9 +1293,10 @@ TEST_CASE_FIXTURE(FragmentAutocompleteFixture, "dont_suggest_local_before_its_de source, source, Position{6, 0}, - [](FragmentAutocompleteResult& fragment) + [](FragmentAutocompleteStatusResult& fragment) { - auto ac = fragment.acResults; + REQUIRE(fragment.result); + auto ac = fragment.result->acResults; CHECK(ac.entryMap.count("myLocal")); LUAU_CHECK_HAS_NO_KEY(ac.entryMap, "myInnerLocal"); } @@ -1275,9 +1316,10 @@ TEST_CASE_FIXTURE(FragmentAutocompleteFixture, "nested_recursive_function") source, source, Position{3, 0}, - [](FragmentAutocompleteResult& result) + [](FragmentAutocompleteStatusResult& frag) { - auto ac = result.acResults; + REQUIRE(frag.result); + auto ac = frag.result->acResults; CHECK(ac.entryMap.count("inner")); CHECK(ac.entryMap.count("outer")); } @@ -1296,9 +1338,10 @@ TEST_CASE_FIXTURE(FragmentAutocompleteFixture, "user_defined_local_functions_in_ source, source, Position{2, 0}, - [](FragmentAutocompleteResult& result) + [](FragmentAutocompleteStatusResult& frag) { - auto ac = result.acResults; + REQUIRE(frag.result); + auto ac = frag.result->acResults; CHECK(ac.entryMap.count("abc")); CHECK(ac.entryMap.count("table")); CHECK(ac.entryMap.count("math")); @@ -1319,9 +1362,10 @@ TEST_CASE_FIXTURE(FragmentAutocompleteFixture, "global_functions_are_not_scoped_ source, source, Position{6, 0}, - [](FragmentAutocompleteResult& result) + [](FragmentAutocompleteStatusResult& frag) { - auto ac = result.acResults; + REQUIRE(frag.result); + auto ac = frag.result->acResults; CHECK(!ac.entryMap.empty()); CHECK(ac.entryMap.count("abc")); CHECK(ac.entryMap.count("table")); @@ -1344,9 +1388,10 @@ TEST_CASE_FIXTURE(FragmentAutocompleteFixture, "local_functions_fall_out_of_scop source, source, Position{6, 0}, - [](FragmentAutocompleteResult& result) + [](FragmentAutocompleteStatusResult& frag) { - auto ac = result.acResults; + REQUIRE(frag.result); + auto ac = frag.result->acResults; CHECK_NE(0, ac.entryMap.size()); LUAU_CHECK_HAS_NO_KEY(ac.entryMap, "abc"); } @@ -1365,9 +1410,10 @@ TEST_CASE_FIXTURE(FragmentAutocompleteFixture, "function_parameters") source, source, Position{3, 0}, - [](FragmentAutocompleteResult& result) + [](FragmentAutocompleteStatusResult& frag) { - auto ac = result.acResults; + REQUIRE(frag.result); + auto ac = frag.result->acResults; CHECK(ac.entryMap.count("test")); } ); @@ -1385,9 +1431,10 @@ TEST_CASE_FIXTURE(FragmentAutocompleteFixture, "unsealed_table") source, source, Position{3, 12}, - [](FragmentAutocompleteResult& result) + [](FragmentAutocompleteStatusResult& frag) { - auto ac = result.acResults; + REQUIRE(frag.result); + auto ac = frag.result->acResults; CHECK_EQ(1, ac.entryMap.size()); CHECK(ac.entryMap.count("prop")); CHECK_EQ(ac.context, AutocompleteContext::Property); @@ -1408,9 +1455,10 @@ TEST_CASE_FIXTURE(FragmentAutocompleteFixture, "unsealed_table_2") source, source, Position{4, 18}, - [](FragmentAutocompleteResult& result) + [](FragmentAutocompleteStatusResult& frag) { - auto ac = result.acResults; + REQUIRE(frag.result); + auto ac = frag.result->acResults; CHECK_EQ(1, ac.entryMap.size()); CHECK(ac.entryMap.count("prop")); CHECK_EQ(ac.context, AutocompleteContext::Property); @@ -1431,9 +1479,10 @@ TEST_CASE_FIXTURE(FragmentAutocompleteFixture, "cyclic_table") source, source, Position{4, 16}, - [](FragmentAutocompleteResult& result) + [](FragmentAutocompleteStatusResult& frag) { - auto ac = result.acResults; + REQUIRE(frag.result); + auto ac = frag.result->acResults; CHECK(ac.entryMap.count("abc")); CHECK_EQ(ac.context, AutocompleteContext::Property); } @@ -1461,9 +1510,10 @@ TEST_CASE_FIXTURE(FragmentAutocompleteFixture, "table_union") source, updated, Position{4, 16}, - [](FragmentAutocompleteResult& result) + [](FragmentAutocompleteStatusResult& frag) { - auto ac = result.acResults; + REQUIRE(frag.result); + auto ac = frag.result->acResults; CHECK_EQ(1, ac.entryMap.size()); CHECK(ac.entryMap.count("b2")); CHECK_EQ(ac.context, AutocompleteContext::Property); @@ -1492,9 +1542,10 @@ TEST_CASE_FIXTURE(FragmentAutocompleteFixture, "table_intersection") source, updated, Position{4, 16}, - [](FragmentAutocompleteResult& result) + [](FragmentAutocompleteStatusResult& frag) { - auto ac = result.acResults; + REQUIRE(frag.result); + auto ac = frag.result->acResults; CHECK_EQ(3, ac.entryMap.size()); CHECK(ac.entryMap.count("a1")); CHECK(ac.entryMap.count("b2")); @@ -1515,9 +1566,10 @@ TEST_CASE_FIXTURE(FragmentAutocompleteFixture, "get_suggestions_for_the_very_sta source, source, Position{0, 0}, - [](FragmentAutocompleteResult& result) + [](FragmentAutocompleteStatusResult& frag) { - auto ac = result.acResults; + REQUIRE(frag.result); + auto ac = frag.result->acResults; CHECK(ac.entryMap.count("table")); CHECK_EQ(ac.context, AutocompleteContext::Statement); } @@ -1542,7 +1594,7 @@ local function test() end function a )"; - autocompleteFragmentInBothSolvers(source, updated, Position{6, 10}, [](FragmentAutocompleteResult& result) {}); + autocompleteFragmentInBothSolvers(source, updated, Position{6, 10}, [](FragmentAutocompleteStatusResult& result) {}); } TEST_CASE_FIXTURE(FragmentAutocompleteFixture, "method_call_inside_function_body") @@ -1567,9 +1619,10 @@ TEST_CASE_FIXTURE(FragmentAutocompleteFixture, "method_call_inside_function_body source, updated, Position{4, 17}, - [](FragmentAutocompleteResult& result) + [](FragmentAutocompleteStatusResult& frag) { - auto ac = result.acResults; + REQUIRE(frag.result); + auto ac = frag.result->acResults; CHECK_NE(0, ac.entryMap.size()); LUAU_CHECK_HAS_NO_KEY(ac.entryMap, "math"); @@ -1592,11 +1645,12 @@ end source, source, Position{4, 7}, - [](FragmentAutocompleteResult& result) + [](FragmentAutocompleteStatusResult& frag) { - CHECK_EQ(2, result.acResults.entryMap.size()); - CHECK(result.acResults.entryMap.count("x")); - CHECK(result.acResults.entryMap.count("y")); + REQUIRE(frag.result); + CHECK_EQ(2, frag.result->acResults.entryMap.size()); + CHECK(frag.result->acResults.entryMap.count("x")); + CHECK(frag.result->acResults.entryMap.count("y")); } ); } @@ -1615,11 +1669,12 @@ end source, source, Position{4, 7}, - [](FragmentAutocompleteResult& result) + [](FragmentAutocompleteStatusResult& frag) { - CHECK_EQ(2, result.acResults.entryMap.size()); - CHECK(result.acResults.entryMap.count("x")); - CHECK(result.acResults.entryMap.count("y")); + REQUIRE(frag.result); + CHECK_EQ(2, frag.result->acResults.entryMap.size()); + CHECK(frag.result->acResults.entryMap.count("x")); + CHECK(frag.result->acResults.entryMap.count("y")); } ); } @@ -1637,11 +1692,12 @@ end source, source, Position{3, 7}, - [](FragmentAutocompleteResult& result) + [](FragmentAutocompleteStatusResult& frag) { - CHECK_EQ(2, result.acResults.entryMap.size()); - CHECK(result.acResults.entryMap.count("zero")); - CHECK(result.acResults.entryMap.count("dot")); + REQUIRE(frag.result); + CHECK_EQ(2, frag.result->acResults.entryMap.size()); + CHECK(frag.result->acResults.entryMap.count("zero")); + CHECK(frag.result->acResults.entryMap.count("dot")); } ); } @@ -1659,11 +1715,12 @@ end source, source, Position{3, 7}, - [](FragmentAutocompleteResult& result) + [](FragmentAutocompleteStatusResult& frag) { - CHECK_EQ(2, result.acResults.entryMap.size()); - CHECK(result.acResults.entryMap.count("zero")); - CHECK(result.acResults.entryMap.count("dot")); + REQUIRE(frag.result); + CHECK_EQ(2, frag.result->acResults.entryMap.size()); + CHECK(frag.result->acResults.entryMap.count("zero")); + CHECK(frag.result->acResults.entryMap.count("dot")); } ); } @@ -1683,10 +1740,11 @@ end source, source, Position{5, 5}, - [](FragmentAutocompleteResult& result) + [](FragmentAutocompleteStatusResult& frag) { - CHECK(result.acResults.entryMap.count("abc")); - CHECK(!result.acResults.entryMap.count("abd")); + REQUIRE(frag.result); + CHECK(frag.result->acResults.entryMap.count("abc")); + CHECK(!frag.result->acResults.entryMap.count("abd")); } ); } @@ -1705,17 +1763,40 @@ t source, updated, Position{2, 1}, - [](FragmentAutocompleteResult& result) + [](FragmentAutocompleteStatusResult& frag) { - auto opt = linearSearchForBinding(result.freshScope, "t"); + REQUIRE(frag.result); + auto opt = linearSearchForBinding(frag.result->freshScope, "t"); REQUIRE(opt); CHECK_EQ("number", toString(*opt)); } ); } +TEST_CASE_FIXTURE(FragmentAutocompleteFixture, "do_not_recommend_results_in_multiline_comment") +{ + ScopedFastFlag sff[] = {{FFlag::LuauIncrementalAutocompleteCommentDetection, true}, {FFlag::LuauBetterCursorInCommentDetection, true}}; + std::string source = R"(--[[ +)"; + std::string dest = R"(--[[ +a +)"; + + + autocompleteFragmentInBothSolvers( + source, + dest, + Position{1, 1}, + [](FragmentAutocompleteStatusResult& frag) + { + CHECK(frag.result == std::nullopt); + } + ); +} + TEST_CASE_FIXTURE(FragmentAutocompleteFixture, "no_recs_for_comments_simple") { + ScopedFastFlag sff[] = {{FFlag::LuauIncrementalAutocompleteCommentDetection, true}, {FFlag::LuauBetterCursorInCommentDetection, true}}; const std::string source = R"( -- sel -- retur @@ -1724,14 +1805,13 @@ TEST_CASE_FIXTURE(FragmentAutocompleteFixture, "no_recs_for_comments_simple") -- end -- the )"; - ScopedFastFlag sff{FFlag::LuauIncrementalAutocompleteCommentDetection, true}; autocompleteFragmentInBothSolvers( source, source, Position{4, 6}, - [](FragmentAutocompleteResult& result) + [](FragmentAutocompleteStatusResult& frag) { - CHECK(result.acResults.entryMap.empty()); + CHECK(frag.result == std::nullopt); } ); } @@ -1752,14 +1832,14 @@ bar baz ]] )"; - ScopedFastFlag sff{FFlag::LuauIncrementalAutocompleteCommentDetection, true}; + ScopedFastFlag sff{FFlag::LuauBetterCursorInCommentDetection, true}; autocompleteFragmentInBothSolvers( source, source, Position{3, 0}, - [](FragmentAutocompleteResult& result) + [](FragmentAutocompleteStatusResult& frag) { - CHECK(result.acResults.entryMap.empty()); + CHECK(frag.result == std::nullopt); } ); @@ -1767,9 +1847,10 @@ baz source, source, Position{3, 2}, - [](FragmentAutocompleteResult& result) + [](FragmentAutocompleteStatusResult& frag) { - CHECK(!result.acResults.entryMap.empty()); + REQUIRE(frag.result); + CHECK(!frag.result->acResults.entryMap.empty()); } ); @@ -1777,9 +1858,9 @@ baz source, source, Position{8, 6}, - [](FragmentAutocompleteResult& result) + [](FragmentAutocompleteStatusResult& frag) { - CHECK(result.acResults.entryMap.empty()); + CHECK(frag.result == std::nullopt); } ); @@ -1787,15 +1868,16 @@ baz source, source, Position{10, 0}, - [](FragmentAutocompleteResult& result) + [](FragmentAutocompleteStatusResult& frag) { - CHECK(result.acResults.entryMap.empty()); + CHECK(frag.result == std::nullopt); } ); } TEST_CASE_FIXTURE(FragmentAutocompleteFixture, "no_recs_for_comments") { + ScopedFastFlag sff[] = {{FFlag::LuauIncrementalAutocompleteCommentDetection, true}, {FFlag::LuauBetterCursorInCommentDetection, true}}; const std::string source = R"( -- sel -- retur @@ -1803,14 +1885,13 @@ TEST_CASE_FIXTURE(FragmentAutocompleteFixture, "no_recs_for_comments") --[[ sel ]] local -- hello )"; - ScopedFastFlag sff{FFlag::LuauIncrementalAutocompleteCommentDetection, true}; autocompleteFragmentInBothSolvers( source, source, Position{1, 7}, - [](FragmentAutocompleteResult& result) + [](FragmentAutocompleteStatusResult& frag) { - CHECK(result.acResults.entryMap.empty()); + CHECK(frag.result == std::nullopt); } ); @@ -1818,9 +1899,9 @@ local -- hello source, source, Position{2, 9}, - [](FragmentAutocompleteResult& result) + [](FragmentAutocompleteStatusResult& frag) { - CHECK(result.acResults.entryMap.empty()); + CHECK(frag.result == std::nullopt); } ); @@ -1828,9 +1909,9 @@ local -- hello source, source, Position{3, 6}, - [](FragmentAutocompleteResult& result) + [](FragmentAutocompleteStatusResult& frag) { - CHECK(result.acResults.entryMap.empty()); + CHECK(frag.result == std::nullopt); } ); @@ -1838,9 +1919,9 @@ local -- hello source, source, Position{4, 9}, - [](FragmentAutocompleteResult& result) + [](FragmentAutocompleteStatusResult& frag) { - CHECK(result.acResults.entryMap.empty()); + CHECK(frag.result == std::nullopt); } ); @@ -1848,9 +1929,10 @@ local -- hello source, source, Position{5, 6}, - [](FragmentAutocompleteResult& result) + [](FragmentAutocompleteStatusResult& frag) { - CHECK(!result.acResults.entryMap.empty()); + REQUIRE(frag.result); + CHECK(!frag.result->acResults.entryMap.empty()); } ); @@ -1858,9 +1940,9 @@ local -- hello source, source, Position{5, 14}, - [](FragmentAutocompleteResult& result) + [](FragmentAutocompleteStatusResult& frag) { - CHECK(result.acResults.entryMap.empty()); + CHECK(frag.result == std::nullopt); } ); } @@ -1875,14 +1957,14 @@ if x == 5 local x = 5 if x == 5 then -- a comment )"; - ScopedFastFlag sff{FFlag::LuauIncrementalAutocompleteCommentDetection, true}; + ScopedFastFlag sff[] = {{FFlag::LuauIncrementalAutocompleteCommentDetection, true}, {FFlag::LuauBetterCursorInCommentDetection, true}}; autocompleteFragmentInBothSolvers( source, updated, Position{2, 28}, - [](FragmentAutocompleteResult& result) + [](FragmentAutocompleteStatusResult& frag) { - CHECK(result.acResults.entryMap.empty()); + CHECK(frag.result == std::nullopt); } ); } @@ -1902,15 +1984,17 @@ type A = <>random non code text here source, updated, Position{1, 38}, - [](FragmentAutocompleteResult& result) + [](FragmentAutocompleteStatusResult& frag) { - CHECK(result.acResults.entryMap.empty()); + REQUIRE(frag.result); + CHECK(frag.result->acResults.entryMap.empty()); } ); } TEST_CASE_FIXTURE(FragmentAutocompleteFixture, "fragment_autocomplete_handles_stale_module") { + ScopedFastFlag sff{FFlag::LuauModuleHoldsAstRoot, false}; const std::string sourceName = "MainModule"; fileResolver.source[sourceName] = "local x = 5"; @@ -1918,9 +2002,10 @@ TEST_CASE_FIXTURE(FragmentAutocompleteFixture, "fragment_autocomplete_handles_st frontend.markDirty(sourceName); frontend.parse(sourceName); - FragmentAutocompleteResult result = autocompleteFragmentForModule(sourceName, fileResolver.source[sourceName], Luau::Position(0, 0)); - CHECK(result.acResults.entryMap.empty()); - CHECK_EQ(result.incrementalModule, nullptr); + FragmentAutocompleteStatusResult frag = autocompleteFragmentForModule(sourceName, fileResolver.source[sourceName], Luau::Position(0, 0)); + REQUIRE(frag.result); + CHECK(frag.result->acResults.entryMap.empty()); + CHECK_EQ(frag.result->incrementalModule, nullptr); } TEST_CASE_FIXTURE(FragmentAutocompleteBuiltinsFixture, "require_tracing") @@ -1938,10 +2023,11 @@ local x = 1 + result. fileResolver.source["MainModule"], fileResolver.source["MainModule"], Position{2, 21}, - [](FragmentAutocompleteResult& result) + [](FragmentAutocompleteStatusResult& frag) { - CHECK(result.acResults.entryMap.size() == 1); - CHECK(result.acResults.entryMap.count("x")); + REQUIRE(frag.result); + CHECK(frag.result->acResults.entryMap.size() == 1); + CHECK(frag.result->acResults.entryMap.count("x")); } ); } @@ -1971,7 +2057,7 @@ l return m )"; - autocompleteFragmentInBothSolvers(source, updated, Position{6, 2}, [](auto& _) {}); + autocompleteFragmentInBothSolvers(source, updated, Position{6, 2}, [](FragmentAutocompleteStatusResult& _) {}); } TEST_CASE_FIXTURE(FragmentAutocompleteBuiltinsFixture, "generalization_crash_when_old_solver_freetypes_have_no_bounds_set") @@ -2003,7 +2089,7 @@ UserInputService.InputBegan:Connect(function(Input) end) )"; - autocompleteFragmentInBothSolvers(source, dest, Position{8, 36}, [](auto& _) {}); + autocompleteFragmentInBothSolvers(source, dest, Position{8, 36}, [](FragmentAutocompleteStatusResult& _) {}); } TEST_CASE_FIXTURE(FragmentAutocompleteFixture, "fragment_autocomplete_ensures_memory_isolation") @@ -2018,7 +2104,7 @@ TEST_CASE_FIXTURE(FragmentAutocompleteFixture, "fragment_autocomplete_ensures_me auto checkAndExamine = [&](const std::string& src, const std::string& idName, const std::string& idString) { - check(src, getOptions()); + checkWithOptions(src); auto id = getType(idName, true); LUAU_ASSERT(id); CHECK_EQ(Luau::toString(*id, opt), idString); @@ -2033,15 +2119,16 @@ TEST_CASE_FIXTURE(FragmentAutocompleteFixture, "fragment_autocomplete_ensures_me auto fragmentACAndCheck = [&](const std::string& updated, const Position& pos, const std::string& idName) { - FragmentAutocompleteResult result = autocompleteFragment(updated, pos, std::nullopt); - auto fragId = getTypeFromModule(result.incrementalModule, idName); + FragmentAutocompleteStatusResult frag = autocompleteFragment(updated, pos, std::nullopt); + REQUIRE(frag.result); + auto fragId = getTypeFromModule(frag.result->incrementalModule, idName); LUAU_ASSERT(fragId); auto srcId = getType(idName, true); LUAU_ASSERT(srcId); CHECK((*fragId)->owningArena != (*srcId)->owningArena); - CHECK(&(result.incrementalModule->internalTypes) == (*fragId)->owningArena); + CHECK(&(frag.result->incrementalModule->internalTypes) == (*fragId)->owningArena); }; const std::string source = R"(local module = {} @@ -2088,18 +2175,53 @@ function module.f return module )"; - autocompleteFragmentInBothSolvers(source, updated, Position{1, 18}, [](FragmentAutocompleteResult& result) {}); + autocompleteFragmentInBothSolvers(source, updated, Position{1, 18}, [](FragmentAutocompleteStatusResult& result) {}); } TEST_CASE_FIXTURE(FragmentAutocompleteBuiltinsFixture, "ice_caused_by_mixed_mode_use") { ScopedFastFlag sff{FFlag::LuauAutocompleteUsesModuleForTypeCompatibility, true}; - const std::string source = "--[[\n\tPackage link auto-generated by Rotriever\n]]\nlocal PackageIndex = script.Parent._Index\n\nlocal Package = " - "require(PackageIndex[\"ReactOtter\"][\"ReactOtter\"])\n\nexport type Goal = Package.Goal\nexport type SpringOptions " - "= Package.SpringOptions\n\n\nreturn Pa"; - autocompleteFragmentInBothSolvers(source, source, Position{11,9}, [](auto& _){ + const std::string source = + std::string("--[[\n\tPackage link auto-generated by Rotriever\n]]\nlocal PackageIndex = script.Parent._Index\n\nlocal Package = ") + + "require(PackageIndex[\"ReactOtter\"][\"ReactOtter\"])\n\nexport type Goal = Package.Goal\nexport type SpringOptions " + + "= Package.SpringOptions\n\n\nreturn Pa"; + autocompleteFragmentInBothSolvers( + source, + source, + Position{11, 9}, + [](FragmentAutocompleteStatusResult& _) { - }); + } + ); + autocompleteFragmentInBothSolvers(source, source, Position{11, 9}, [](auto& _) {}); +} + +TEST_CASE_FIXTURE(FragmentAutocompleteBuiltinsFixture, "free_type_in_old_solver_shouldnt_trigger_not_null_assertion") +{ + + const std::string source = R"(--!strict +local foo +local a, z = foo() + +local e = foo().x + +local f = foo().y + +z +)"; + + const std::string dest = R"(--!strict +local foo +local a, z = foo() + +local e = foo().x + +local f = foo().y + +z:a +)"; + + autocompleteFragmentInBothSolvers(source, dest, Position{8, 3}, [](FragmentAutocompleteStatusResult& _) {}); } TEST_SUITE_END(); diff --git a/tests/Frontend.test.cpp b/tests/Frontend.test.cpp index 9d6cfa74..da8a2230 100644 --- a/tests/Frontend.test.cpp +++ b/tests/Frontend.test.cpp @@ -14,10 +14,11 @@ using namespace Luau; LUAU_FASTFLAG(LuauSolverV2); -LUAU_FASTFLAG(DebugLuauFreezeArena); -LUAU_FASTFLAG(DebugLuauMagicTypes); +LUAU_FASTFLAG(DebugLuauFreezeArena) +LUAU_FASTFLAG(DebugLuauMagicTypes) LUAU_FASTFLAG(LuauSelectivelyRetainDFGArena) -LUAU_FASTFLAG(LuauBetterReverseDependencyTracking); +LUAU_FASTFLAG(LuauBetterReverseDependencyTracking) +LUAU_FASTFLAG(LuauModuleHoldsAstRoot) namespace { @@ -1542,6 +1543,23 @@ TEST_CASE_FIXTURE(FrontendFixture, "check_module_references_allocator") CHECK_EQ(module->names.get(), source->names.get()); } +TEST_CASE_FIXTURE(FrontendFixture, "check_module_references_correct_ast_root") +{ + ScopedFastFlag sff{FFlag::LuauModuleHoldsAstRoot, true}; + fileResolver.source["game/workspace/MyScript"] = R"( + print("Hello World") + )"; + + frontend.check("game/workspace/MyScript"); + + ModulePtr module = frontend.moduleResolver.getModule("game/workspace/MyScript"); + SourceModule* source = frontend.getSourceModule("game/workspace/MyScript"); + CHECK(module); + CHECK(source); + + CHECK_EQ(module->root, source->root); +} + TEST_CASE_FIXTURE(FrontendFixture, "dfg_data_cleared_on_retain_type_graphs_unset") { ScopedFastFlag sffs[] = {{FFlag::LuauSolverV2, true}, {FFlag::LuauSelectivelyRetainDFGArena, true}}; diff --git a/tests/Module.test.cpp b/tests/Module.test.cpp index 08b0bb0d..21b6bcf7 100644 --- a/tests/Module.test.cpp +++ b/tests/Module.test.cpp @@ -14,7 +14,6 @@ using namespace Luau; LUAU_FASTFLAG(LuauSolverV2); LUAU_FASTFLAG(DebugLuauFreezeArena); LUAU_FASTINT(LuauTypeCloneIterationLimit); -LUAU_FASTFLAG(LuauOldSolverCreatesChildScopePointers) TEST_SUITE_BEGIN("ModuleTests"); TEST_CASE_FIXTURE(Fixture, "is_within_comment") @@ -542,7 +541,6 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "clone_a_bound_typepack_to_a_persistent_typep TEST_CASE_FIXTURE(Fixture, "old_solver_correctly_populates_child_scopes") { - ScopedFastFlag sff{FFlag::LuauOldSolverCreatesChildScopePointers, true}; check(R"( --!strict if true then diff --git a/tests/Normalize.test.cpp b/tests/Normalize.test.cpp index 0e026edf..dba2924b 100644 --- a/tests/Normalize.test.cpp +++ b/tests/Normalize.test.cpp @@ -13,6 +13,7 @@ LUAU_FASTFLAG(LuauSolverV2) LUAU_FASTINT(LuauTypeInferRecursionLimit) LUAU_FASTFLAG(LuauFixNormalizedIntersectionOfNegatedClass) +LUAU_FASTFLAG(LuauNormalizeNegationFix) using namespace Luau; namespace @@ -1029,6 +1030,26 @@ TEST_CASE_FIXTURE(NormalizeFixture, "truthy_table_property_and_optional_table_wi CHECK("{ x: number }" == toString(ty)); } +TEST_CASE_FIXTURE(NormalizeFixture, "free_type_and_not_truthy") +{ + ScopedFastFlag sff[] = { + {FFlag::LuauSolverV2, true}, // Only because it affects the stringification of free types + {FFlag::LuauNormalizeNegationFix, true}, + }; + + TypeId freeTy = arena.freshType(builtinTypes, &globalScope); + TypeId notTruthy = arena.addType(NegationType{builtinTypes->truthyType}); // ~~(false?) + + TypeId intersectionTy = arena.addType(IntersectionType{{freeTy, notTruthy}}); // 'a & ~~(false?) + + auto norm = normalizer.normalize(intersectionTy); + REQUIRE(norm); + + TypeId result = normalizer.typeFromNormal(*norm); + + CHECK("'a & (false?)" == toString(result)); +} + TEST_CASE_FIXTURE(BuiltinsFixture, "normalizer_should_be_able_to_detect_cyclic_tables_and_not_stack_overflow") { if (!FFlag::LuauSolverV2) diff --git a/tests/TypeInfer.refinements.test.cpp b/tests/TypeInfer.refinements.test.cpp index fa62fbea..270707a5 100644 --- a/tests/TypeInfer.refinements.test.cpp +++ b/tests/TypeInfer.refinements.test.cpp @@ -12,6 +12,8 @@ LUAU_FASTFLAG(DebugLuauEqSatSimplification) LUAU_FASTFLAG(LuauGeneralizationRemoveRecursiveUpperBound2) LUAU_FASTFLAG(LuauIntersectNotNil) LUAU_FASTFLAG(LuauSkipNoRefineDuringRefinement) +LUAU_FASTFLAG(LuauFunctionCallsAreNotNilable) +LUAU_FASTFLAG(LuauDoNotLeakNilInRefinement) using namespace Luau; @@ -2021,14 +2023,10 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "type_annotations_arent_relevant_when_doing_d LUAU_REQUIRE_NO_ERRORS(result); + // Function calls are treated as (potentially) `nil`, the same as table + // access, for UX. CHECK_EQ("nil", toString(requireTypeAtPosition({8, 28}))); - if (FFlag::LuauSolverV2) - { - // CLI-115478 - This should be never - CHECK_EQ("nil", toString(requireTypeAtPosition({9, 28}))); - } - else - CHECK_EQ("nil", toString(requireTypeAtPosition({9, 28}))); + CHECK_EQ("nil", toString(requireTypeAtPosition({9, 28}))); } TEST_CASE_FIXTURE(BuiltinsFixture, "function_call_with_colon_after_refining_not_to_be_nil") @@ -2526,4 +2524,42 @@ TEST_CASE_FIXTURE(Fixture, "truthy_call_of_function_with_table_value_as_argument CHECK_EQ("Item", toString(requireTypeAtPosition({9, 28}))); } +TEST_CASE_FIXTURE(BuiltinsFixture, "function_calls_are_not_nillable") +{ + ScopedFastFlag _{FFlag::LuauDoNotLeakNilInRefinement, true}; + + LUAU_CHECK_NO_ERRORS(check(R"( + local BEFORE_SLASH_PATTERN = "^(.*)[\\/]" + function operateOnPath(path: string): string? + local fileName = string.gsub(path, BEFORE_SLASH_PATTERN, "") + if string.match(fileName, "^init%.") then + return "path=" .. fileName + end + return nil + end + )")); + +} + +TEST_CASE_FIXTURE(BuiltinsFixture, "oss_1528_method_calls_are_not_nillable") +{ + ScopedFastFlag _{FFlag::LuauDoNotLeakNilInRefinement, true}; + + LUAU_CHECK_NO_ERRORS(check(R"( + type RunService = { + IsRunning: (RunService) -> boolean + } + type Game = { + GetRunService: (Game) -> RunService + } + local function getServices(g: Game): RunService + local service = g:GetRunService() + if service:IsRunning() then + return service + end + error("Oh no! The service isn't running!") + end + )")); +} + TEST_SUITE_END(); diff --git a/tests/TypeInfer.tables.test.cpp b/tests/TypeInfer.tables.test.cpp index 4b7ce57c..6376ff65 100644 --- a/tests/TypeInfer.tables.test.cpp +++ b/tests/TypeInfer.tables.test.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/BuiltinDefinitions.h" #include "Luau/Common.h" +#include "Luau/Error.h" #include "Luau/Frontend.h" #include "Luau/ToString.h" #include "Luau/TypeInfer.h" @@ -25,6 +26,7 @@ LUAU_FASTFLAG(LuauAllowNonSharedTableTypesInLiteral) LUAU_FASTFLAG(LuauFollowTableFreeze) LUAU_FASTFLAG(LuauPrecalculateMutatedFreeTypes2) LUAU_FASTFLAG(LuauDeferBidirectionalInferenceForTableAssignment) +LUAU_FASTFLAG(LuauBidirectionalInferenceUpcast) TEST_SUITE_BEGIN("TableTests"); @@ -5183,4 +5185,157 @@ TEST_CASE_FIXTURE(Fixture, "empty_union_container_overflow") )")); } +TEST_CASE_FIXTURE(Fixture, "inference_in_constructor") +{ + LUAU_CHECK_NO_ERRORS(check(R"( + local function new(y) + local t: { x: number } = { x = y } + return t + end + )")); + if (FFlag::LuauSolverV2) + CHECK_EQ("(number) -> { x: number }", toString(requireType("new"))); + else + CHECK_EQ("(number) -> {| x: number |}", toString(requireType("new"))); +} + +TEST_CASE_FIXTURE(Fixture, "returning_optional_in_table") +{ + ScopedFastFlag sffs[] = { + {FFlag::LuauDeferBidirectionalInferenceForTableAssignment, true}, + {FFlag::LuauBidirectionalInferenceUpcast, true}, + }; + + LUAU_CHECK_NO_ERRORS(check(R"( + local Numbers = { zero = 0 } + local function FuncA(): { Value: number? } + return { Value = Numbers.zero } + end + )")); +} + +TEST_CASE_FIXTURE(Fixture, "returning_mismatched_optional_in_table") +{ + ScopedFastFlag sffs[] = { + {FFlag::LuauSolverV2, true}, + {FFlag::LuauDeferBidirectionalInferenceForTableAssignment, true}, + }; + + auto result = check(R"( + local Numbers = { str = ( "" :: string ) } + local function FuncB(): { Value: number? } + return { + Value = Numbers.str + } + end + )"); + LUAU_CHECK_ERROR_COUNT(1, result); + auto err = get(result.errors[0]); + REQUIRE(err); + CHECK_EQ(toString(err->givenTp), "{ Value: string }"); + CHECK_EQ(toString(err->wantedTp), "{ Value: number? }"); +} + +TEST_CASE_FIXTURE(Fixture, "optional_function_in_table") +{ + ScopedFastFlag sffs[] = { + {FFlag::LuauSolverV2, true}, + {FFlag::LuauDeferBidirectionalInferenceForTableAssignment, true}, + {FFlag::LuauBidirectionalInferenceUpcast, true}, + }; + + LUAU_CHECK_NO_ERRORS(check(R"( + local t: { (() -> ())? } = { + function() end, + } + )")); + + auto result = check(R"( + local t: { ((number) -> ())? } = { + function(_: string) end, + } + )"); + + LUAU_CHECK_ERROR_COUNT(1, result); + auto err = get(result.errors[0]); + REQUIRE(err); + CHECK_EQ(toString(err->givenType), "{(string) -> ()}"); + CHECK_EQ(toString(err->wantedType), "{((number) -> ())?}"); +} + +TEST_CASE_FIXTURE(Fixture, "oss_1596_expression_in_table") +{ + ScopedFastFlag sffs[] = { + {FFlag::LuauDeferBidirectionalInferenceForTableAssignment, true}, + {FFlag::LuauBidirectionalInferenceUpcast, true}, + }; + + LUAU_CHECK_NO_ERRORS(check(R"( + type foo = {abc: number?} + local x: foo = {abc = 100} + local y: foo = {abc = 10 * 10} + )")); +} + +TEST_CASE_FIXTURE(Fixture, "oss_1615_parametrized_type_alias") +{ + ScopedFastFlag sffs[] = { + {FFlag::LuauDeferBidirectionalInferenceForTableAssignment, true}, + }; + + LUAU_CHECK_NO_ERRORS(check(R"( + type Pair = { sep: {}? } + local a: Pair<{}> = { + sep = nil, + } + )")); +} + +TEST_CASE_FIXTURE(Fixture, "oss_1543_optional_generic_param") +{ + ScopedFastFlag sffs[] = { + {FFlag::LuauPrecalculateMutatedFreeTypes2, true}, + {FFlag::LuauDeferBidirectionalInferenceForTableAssignment, true}, + }; + + LUAU_CHECK_NO_ERRORS(check(R"( + type foo = { bar: T? } + + local foo: foo = { bar = "foobar" } + local foo: foo = { } + local foo: foo = { } + )")); +} + +TEST_CASE_FIXTURE(Fixture, "missing_fields_bidirectional_inference") +{ + ScopedFastFlag sffs[] = { + {FFlag::LuauSolverV2, true}, + {FFlag::LuauPrecalculateMutatedFreeTypes2, true}, + {FFlag::LuauDeferBidirectionalInferenceForTableAssignment, true}, + }; + + auto result = check(R"( + type Book = { title: string, author: string } + local b: Book = { title = "The Odyssey" } + local t: { Book } = { + { title = "The Illiad", author = "Homer" }, + { author = "Virgil" } + } + )"); + + LUAU_CHECK_ERROR_COUNT(2, result); + auto err = get(result.errors[0]); + REQUIRE(err); + CHECK_EQ(toString(err->givenType), "{ title: string }"); + CHECK_EQ(toString(err->wantedType), "Book"); + CHECK_EQ(result.errors[0].location, Location{{2, 24}, {2, 49}}); + err = get(result.errors[1]); + REQUIRE(err); + CHECK_EQ(toString(err->givenType), "{{ author: string } | { author: string, title: string }}"); + CHECK_EQ(toString(err->wantedType), "{Book}"); + CHECK_EQ(result.errors[1].location, Location{{3, 28}, {6, 9}}); + +} + TEST_SUITE_END(); diff --git a/tests/TypeInfer.test.cpp b/tests/TypeInfer.test.cpp index 217391b8..77e2aeb1 100644 --- a/tests/TypeInfer.test.cpp +++ b/tests/TypeInfer.test.cpp @@ -26,6 +26,7 @@ LUAU_FASTINT(LuauTypeInferRecursionLimit) LUAU_FASTFLAG(LuauAstTypeGroup2) LUAU_FASTFLAG(LuauNewNonStrictWarnOnUnknownGlobals) LUAU_FASTFLAG(LuauInferLocalTypesInMultipleAssignments) +LUAU_FASTFLAG(LuauUnifyMetatableWithAny) using namespace Luau; @@ -1808,4 +1809,55 @@ TEST_CASE_FIXTURE(Fixture, "multiple_assignment") LUAU_REQUIRE_NO_ERRORS(result); } +TEST_CASE_FIXTURE(BuiltinsFixture, "getmetatable_works_with_any") +{ + ScopedFastFlag _{FFlag::LuauUnifyMetatableWithAny, true}; + + LUAU_REQUIRE_NO_ERRORS(check(R"( + return { + new = function(name: string) + local self = newproxy(true) :: any + + getmetatable(self).__tostring = function() + return "Hello, I am " .. name + end + + return self + end, + } + )")); + +} + +TEST_CASE_FIXTURE(BuiltinsFixture, "getmetatable_infer_any_ret") +{ + ScopedFastFlag _{FFlag::LuauUnifyMetatableWithAny, true}; + + LUAU_REQUIRE_NO_ERRORS(check(R"( + local function spooky(x: any) + return getmetatable(x) + end + )")); + + CHECK_EQ("(any) -> any", toString(requireType("spooky"))); +} + +TEST_CASE_FIXTURE(BuiltinsFixture, "getmetatable_infer_any_param") +{ + ScopedFastFlag sffs[] = { + {FFlag::LuauSolverV2, true}, + {FFlag::LuauUnifyMetatableWithAny, true}, + }; + + auto result = check(R"( + local function check(x): any + return getmetatable(x) + end + )"); + + // CLI-144695: We're leaking the `MT` generic here, this happens regardless + // of if `LuauUnifyMetatableWithAny` is set. + CHECK_EQ("({ @metatable MT, {+ +} }) -> any", toString(requireType("check"))); +} + TEST_SUITE_END();