Merge branch 'upstream' into merge

This commit is contained in:
Vyacheslav Egorov 2025-03-07 15:38:36 +02:00
commit 23bc546e97
30 changed files with 1435 additions and 308 deletions

View file

@ -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

View file

@ -57,7 +57,8 @@ struct FragmentAutocompleteResult
FragmentAutocompleteAncestryResult findAncestryForFragmentParse(AstStatBlock* root, const Position& cursorPos);
std::optional<FragmentParseResult> parseFragment(
const SourceModule& srcModule,
AstStatBlock* root,
AstNameTable* names,
std::string_view src,
const Position& cursorPos,
std::optional<Position> fragmentEndPosition
@ -98,7 +99,7 @@ struct FragmentAutocompleteStatusResult
struct FragmentContext
{
std::string_view newSrc;
const ParseResult& newAstRoot;
const ParseResult& freshParse;
std::optional<FrontendOptions> opts;
std::optional<Position> DEPRECATED_fragmentEndPosition;
};

View file

@ -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<void(std::function<void()> task)> executeTask = {},
std::function<bool(size_t done, size_t total)> progress = {}
);
std::vector<ModuleName> checkQueuedModules_DEPRECATED(
std::optional<FrontendOptions> optionOverride = {},
std::function<void(std::function<void()> task)> executeTask = {},
std::function<bool(size_t done, size_t total)> progress = {}
);
std::optional<CheckResult> getCheckResult(const ModuleName& name, bool accumulateNested, bool forAutocomplete = false);
std::vector<ModuleName> getRequiredScripts(const ModuleName& name);
@ -251,6 +257,9 @@ private:
void checkBuildQueueItem(BuildQueueItem& item);
void checkBuildQueueItems(std::vector<BuildQueueItem>& items);
void recordItemResult(const BuildQueueItem& item);
void performQueueItemTask(std::shared_ptr<BuildQueueWorkState> state, size_t itemPos);
void sendQueueItemTask(std::shared_ptr<BuildQueueWorkState> state, size_t itemPos);
void sendQueueCycleItemTask(std::shared_ptr<BuildQueueWorkState> state);
static LintResult classifyLints(const std::vector<LintWarning>& warnings, const Config& config);

View file

@ -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> allocator;
std::shared_ptr<AstNameTable> names;
AstStatBlock* root = nullptr;
std::vector<std::pair<Location, ScopePtr>> scopes; // never empty

View file

@ -53,6 +53,7 @@ struct Proposition
{
const RefinementKey* key;
TypeId discriminantTy;
bool implicitFromCall;
};
template<typename T>
@ -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<Refinement> allocator;

View file

@ -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> builtinTypes,
NotNull<TypeArena> arena,
NotNull<Unifier2> unifier,
NotNull<Subtyping> subtyping,
TypeId expectedType,
TypeId exprType,
const AstExpr* expr,

View file

@ -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);

View file

@ -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<AstExprLocal>(); local && local->local->name == "_luau_autocomplete_ice")
ice.ice("_luau_autocomplete_ice encountered", local->location);
if (auto global = node->as<AstExprGlobal>(); global && global->name == "_luau_autocomplete_ice")
ice.ice("_luau_autocomplete_ice encountered", global->location);
}
if (node->is<AstExprIndexName>())
{
if (auto it = module.astTypes.find(node->asExpr()))

View file

@ -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<TypeArena> arena;
NotNull<BuiltinTypes> builtinTypes;
@ -62,6 +65,8 @@ public:
{
}
virtual ~TypeCloner() = default;
TypeId clone(TypeId ty)
{
shallowClone(ty);
@ -120,6 +125,7 @@ private:
}
}
protected:
std::optional<TypeId> 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<TypeArena> arena,
NotNull<BuiltinTypes> builtinTypes,
NotNull<SeenTypes> types,
NotNull<SeenTypePacks> 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<GenericType>(target))
generic->scope = nullptr;
else if (auto free = getMutable<FreeType>(target))
{
free->scope = freeTypeReplacementScope;
}
else if (auto fn = getMutable<FunctionType>(target))
fn->scope = nullptr;
else if (auto table = getMutable<TableType>(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<GenericTypePack>(target))
generic->scope = nullptr;
else if (auto free = getMutable<FreeTypePack>(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

View file

@ -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<TypeId> 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,

View file

@ -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<UnknownType>(upperBound))
if (get<UnknownType>(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<UnknownType>(lowerBound))
if (get<UnknownType>(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<cons
TypePackId argsPack = follow(c.argsPack);
TypePackId result = follow(c.result);
if (isBlocked(fn) || hasUnresolvedConstraints(fn))
if (FFlag::DebugLuauGreedyGeneralization)
{
return block(c.fn, constraint);
if (isBlocked(fn))
return block(c.fn, constraint);
}
else
{
if (isBlocked(fn) || hasUnresolvedConstraints(fn))
{
return block(c.fn, constraint);
}
}
if (get<AnyType>(fn))
@ -1658,8 +1670,11 @@ bool ConstraintSolver::tryDispatch(const FunctionCheckConstraint& c, NotNull<con
else if (expr->is<AstExprTable>())
{
Unifier2 u2{arena, builtinTypes, constraint->scope, NotNull{&iceReporter}};
Subtyping sp{builtinTypes, arena, simplifier, normalizer, typeFunctionRuntime, NotNull{&iceReporter}};
std::vector<TypeId> 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, NotNull<const
return false;
Unifier2 u2{arena, builtinTypes, constraint->scope, NotNull{&iceReporter}};
Subtyping sp{builtinTypes, arena, simplifier, normalizer, typeFunctionRuntime, NotNull{&iceReporter}};
std::vector<TypeId> 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;
}

View file

@ -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)

View file

@ -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<typename T>
@ -54,7 +59,7 @@ namespace Luau
{
template<typename K, typename V>
void cloneModuleMap(TypeArena& destArena, CloneState& cloneState, const Luau::DenseHashMap<K, V>& source, Luau::DenseHashMap<K, V>& dest)
void cloneModuleMap_DEPRECATED(TypeArena& destArena, CloneState& cloneState, const Luau::DenseHashMap<K, V>& source, Luau::DenseHashMap<K, V>& dest)
{
for (auto [k, v] : source)
{
@ -62,6 +67,21 @@ void cloneModuleMap(TypeArena& destArena, CloneState& cloneState, const Luau::De
}
}
template<typename K, typename V>
void cloneModuleMap(
TypeArena& destArena,
CloneState& cloneState,
const Luau::DenseHashMap<K, V>& source,
Luau::DenseHashMap<K, V>& 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<std::pair<AstLocal*, AstExpr*>> 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<TypeArena> destArena,
NotNull<DataFlowGraph> dfg,
AstStatBlock* program,
Scope* destScope
)
{
LUAU_TIMETRACE_SCOPE("Luau::cloneAndSquashScopes", "FragmentAutocomplete");
std::vector<const Scope*> 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<Name, TypeFun> 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<std::pair<AstLocal*, AstExpr*>> locals = std::move(finder.referencedLocalDefs);
for (auto [loc, expr] : locals)
{
if (std::optional<Binding> 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<FrontendOptions> 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<AstNode*> 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<AstStatBlock>())
{
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<AstStatLocal>())
if (auto statLoc = stat->as<AstStatLocal>())
{
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<AstExprFunction>())
{
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<FragmentParseResult> parseFragment(
const SourceModule& srcModule,
AstStatBlock* root,
AstNameTable* names,
std::string_view src,
const Position& cursorPos,
std::optional<Position> 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<FragmentParseResult> 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<FragmentParseResult> 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<AstNameTable>& 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<FragmentParseResult> 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<FragmentParseResult> parseFragment(
return fragmentResult;
}
ModulePtr cloneModule(CloneState& cloneState, const ModulePtr& source, std::unique_ptr<Allocator> alloc)
ModulePtr cloneModule_DEPRECATED(CloneState& cloneState, const ModulePtr& source, std::unique_ptr<Allocator> 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<Allocator> alloc, Scope* freeTypeFreshScope)
{
LUAU_TIMETRACE_SCOPE("Luau::cloneModule", "FragmentAutocomplete");
freeze(source->internalTypes);
freeze(source->interfaceTypes);
ModulePtr incremental = std::make_shared<Module>();
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<Scope> freshChildOfNearestScope = std::make_shared<Scope>(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<Scope> freshChildOfNearestScope = nullptr;
if (FFlag::LuauCloneIncrementalModule)
{
freshChildOfNearestScope = std::make_shared<Scope>(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<Scope>(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<FragmentTypeCheckStatus, FragmentTypeCheckResult> 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<FragmentTypeCheckStatus, FragmentTypeCheckResult> typecheckFragment(
return {};
}
if (FFlag::LuauIncrementalAutocompleteBugfixes)
std::optional<FragmentParseResult> 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)

View file

@ -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<void(std::function<void()> task)> executeTask;
std::vector<BuildQueueItem> buildQueueItems;
std::mutex mtx;
std::condition_variable cv;
std::vector<size_t> readyQueueItems;
size_t processing = 0;
size_t remaining = 0;
};
std::optional<Mode> parseMode(const std::vector<HotComment>& hotcomments)
{
for (const HotComment& hc : hotcomments)
@ -481,6 +497,203 @@ std::vector<ModuleName> Frontend::checkQueuedModules(
std::function<bool(size_t done, size_t total)> 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<ModuleName> currModuleQueue;
std::swap(currModuleQueue, moduleQueue);
DenseHashSet<Luau::ModuleName> seen{{}};
std::shared_ptr<BuildQueueWorkState> state = std::make_shared<BuildQueueWorkState>();
for (const ModuleName& name : currModuleQueue)
{
if (seen.contains(name))
continue;
if (!isDirty(name, frontendOptions.forAutocomplete))
{
seen.insert(name);
continue;
}
std::vector<ModuleName> 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<ModuleName, size_t> 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<void()> 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<size_t> nextItems;
std::optional<size_t> 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<ModuleName> 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<ModuleName> Frontend::checkQueuedModules_DEPRECATED(
std::optional<FrontendOptions> optionOverride,
std::function<void(std::function<void()> task)> executeTask,
std::function<bool(size_t done, size_t total)> 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<BuildQueueWorkState> 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<BuildQueueWorkState> 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<BuildQueueWorkState> 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<Module>();
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;

View file

@ -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<NegationType>(t))
{
if (FFlag::LuauNormalizeNegationFix)
here.tyvars = std::move(tyvars);
return intersectNormalWithTy(here, nt->ty, seenTablePropPairs, seenSetTypes);
}
else
{
// TODO negated unions, intersections, table, and function.

View file

@ -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

View file

@ -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)

View file

@ -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> builtinTypes,
NotNull<TypeArena> arena,
NotNull<Unifier2> unifier,
NotNull<Subtyping> 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<AstExprFunction>())
{
// 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<AstExprTable>())
{
@ -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<AstExprConstantString>());
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,

View file

@ -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 = &currentModule->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<Scope>(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<Scope>(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;

View file

@ -18,6 +18,7 @@
#include <optional>
LUAU_FASTINT(LuauTypeInferRecursionLimit)
LUAU_FASTFLAGVARIABLE(LuauUnifyMetatableWithAny)
namespace Luau
{
@ -235,6 +236,10 @@ bool Unifier2::unify(TypeId subTy, TypeId superTy)
auto superMetatable = get<MetatableType>(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<TypeError>
// rather than a boolean to signal an occurs check failure.
bool Unifier2::unify(TypePackId subTp, TypePackId superTp)

View file

@ -8,7 +8,6 @@
#include <limits.h>
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)

View file

@ -31,7 +31,7 @@ static void setLuauFlags(bool state)
void setLuauFlagsDefault()
{
for (Luau::FValue<bool>* flag = Luau::FValue<bool>::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;
}

View file

@ -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

File diff suppressed because it is too large Load diff

View file

@ -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}};

View file

@ -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

View file

@ -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)

View file

@ -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();

View file

@ -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<TypePackMismatch>(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<TypeMismatch>(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<Node> = { 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<T> = { bar: T? }
local foo: foo<any> = { bar = "foobar" }
local foo: foo<any> = { }
local foo: foo<nil> = { }
)"));
}
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<TypeMismatch>(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<TypeMismatch>(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();

View file

@ -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();