Merge branch 'master' into overloaded-functions-singleton-parameters

This commit is contained in:
ariel 2025-03-24 09:13:55 -07:00 committed by GitHub
commit aeb27e1a2c
Signed by: DevComp
GPG key ID: B5690EEEBB952194
107 changed files with 6333 additions and 1555 deletions

View file

@ -70,6 +70,7 @@ Property makeProperty(TypeId ty, std::optional<std::string> documentationSymbol
void assignPropDocumentationSymbols(TableType::Props& props, const std::string& baseName);
std::string getBuiltinDefinitionSource();
std::string getTypeFunctionDefinitionSource();
void addGlobalBinding(GlobalTypes& globals, const std::string& name, TypeId ty, const std::string& packageName);
void addGlobalBinding(GlobalTypes& globals, const std::string& name, Binding binding);

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

@ -117,12 +117,15 @@ struct ConstraintGenerator
// Needed to register all available type functions for execution at later stages.
NotNull<TypeFunctionRuntime> typeFunctionRuntime;
DenseHashMap<const AstStatTypeFunction*, ScopePtr> astTypeFunctionEnvironmentScopes{nullptr};
// Needed to resolve modules to make 'require' import types properly.
NotNull<ModuleResolver> moduleResolver;
// Occasionally constraint generation needs to produce an ICE.
const NotNull<InternalErrorReporter> ice;
ScopePtr globalScope;
ScopePtr typeFunctionScope;
std::function<void(const ModuleName&, const ScopePtr&)> prepareModuleScope;
std::vector<RequireCycle> requireCycles;
@ -140,6 +143,7 @@ struct ConstraintGenerator
NotNull<BuiltinTypes> builtinTypes,
NotNull<InternalErrorReporter> ice,
const ScopePtr& globalScope,
const ScopePtr& typeFunctionScope,
std::function<void(const ModuleName&, const ScopePtr&)> prepareModuleScope,
DcrLogger* logger,
NotNull<DataFlowGraph> dfg,

View file

@ -1,8 +1,9 @@
// This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details
#pragma once
#include <string>
#include <memory>
#include <optional>
#include <string>
#include <vector>
namespace Luau
@ -32,15 +33,71 @@ struct ModuleInfo
bool optional = false;
};
struct RequireAlias
{
std::string alias; // Unprefixed alias name (no leading `@`).
std::vector<std::string> tags = {};
};
struct RequireNode
{
virtual ~RequireNode() {}
// Get the path component representing this node.
virtual std::string getPathComponent() const = 0;
// Get the displayed user-facing label for this node, defaults to getPathComponent()
virtual std::string getLabel() const
{
return getPathComponent();
}
// Get tags to attach to this node's RequireSuggestion (defaults to none).
virtual std::vector<std::string> getTags() const
{
return {};
}
// TODO: resolvePathToNode() can ultimately be replaced with a call into
// require-by-string's path resolution algorithm. This will first require
// generalizing that algorithm to work with a virtual file system.
virtual std::unique_ptr<RequireNode> resolvePathToNode(const std::string& path) const = 0;
// Get children of this node, if any (if this node represents a directory).
virtual std::vector<std::unique_ptr<RequireNode>> getChildren() const = 0;
// A list of the aliases available to this node.
virtual std::vector<RequireAlias> getAvailableAliases() const = 0;
};
struct RequireSuggestion
{
std::string label;
std::string fullPath;
std::vector<std::string> tags;
};
using RequireSuggestions = std::vector<RequireSuggestion>;
struct RequireSuggester
{
virtual ~RequireSuggester() {}
std::optional<RequireSuggestions> getRequireSuggestions(const ModuleName& requirer, const std::optional<std::string>& pathString) const;
protected:
virtual std::unique_ptr<RequireNode> getNode(const ModuleName& name) const = 0;
private:
std::optional<RequireSuggestions> getRequireSuggestionsImpl(const ModuleName& requirer, const std::optional<std::string>& path) const;
};
struct FileResolver
{
FileResolver() = default;
FileResolver(std::shared_ptr<RequireSuggester> requireSuggester)
: requireSuggester(std::move(requireSuggester))
{
}
virtual ~FileResolver() {}
virtual std::optional<SourceCode> readSource(const ModuleName& name) = 0;
@ -60,10 +117,10 @@ struct FileResolver
return std::nullopt;
}
virtual std::optional<RequireSuggestions> getRequireSuggestions(const ModuleName& requirer, const std::optional<std::string>& pathString) const
{
return std::nullopt;
}
// Make non-virtual when removing FFlagLuauImproveRequireByStringAutocomplete.
virtual std::optional<RequireSuggestions> getRequireSuggestions(const ModuleName& requirer, const std::optional<std::string>& pathString) const;
std::shared_ptr<RequireSuggester> requireSuggester;
};
struct NullFileResolver : FileResolver

View file

@ -15,6 +15,28 @@ namespace Luau
{
struct FrontendOptions;
enum class FragmentAutocompleteWaypoint
{
ParseFragmentEnd,
CloneModuleStart,
CloneModuleEnd,
DfgBuildEnd,
CloneAndSquashScopeStart,
CloneAndSquashScopeEnd,
ConstraintSolverStart,
ConstraintSolverEnd,
TypecheckFragmentEnd,
AutocompleteEnd,
COUNT,
};
class IFragmentAutocompleteReporter
{
public:
virtual void reportWaypoint(FragmentAutocompleteWaypoint) = 0;
virtual void reportFragmentString(std::string_view) = 0;
};
enum class FragmentTypeCheckStatus
{
SkipAutocomplete,
@ -57,7 +79,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
@ -69,7 +92,8 @@ std::pair<FragmentTypeCheckStatus, FragmentTypeCheckResult> typecheckFragment(
const Position& cursorPos,
std::optional<FrontendOptions> opts,
std::string_view src,
std::optional<Position> fragmentEndPosition
std::optional<Position> fragmentEndPosition,
IFragmentAutocompleteReporter* reporter = nullptr
);
FragmentAutocompleteResult fragmentAutocomplete(
@ -79,7 +103,8 @@ FragmentAutocompleteResult fragmentAutocomplete(
Position cursorPosition,
std::optional<FrontendOptions> opts,
StringCompletionCallback callback,
std::optional<Position> fragmentEndPosition = std::nullopt
std::optional<Position> fragmentEndPosition = std::nullopt,
IFragmentAutocompleteReporter* reporter = nullptr
);
enum class FragmentAutocompleteStatus
@ -98,9 +123,10 @@ struct FragmentAutocompleteStatusResult
struct FragmentContext
{
std::string_view newSrc;
const ParseResult& newAstRoot;
const ParseResult& freshParse;
std::optional<FrontendOptions> opts;
std::optional<Position> DEPRECATED_fragmentEndPosition;
IFragmentAutocompleteReporter* reporter = nullptr;
};
/**

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);
@ -296,6 +305,7 @@ ModulePtr check(
NotNull<ModuleResolver> moduleResolver,
NotNull<FileResolver> fileResolver,
const ScopePtr& globalScope,
const ScopePtr& typeFunctionScope,
std::function<void(const ModuleName&, const ScopePtr&)> prepareModuleScope,
FrontendOptions options,
TypeCheckLimits limits
@ -310,6 +320,7 @@ ModulePtr check(
NotNull<ModuleResolver> moduleResolver,
NotNull<FileResolver> fileResolver,
const ScopePtr& globalScope,
const ScopePtr& typeFunctionScope,
std::function<void(const ModuleName&, const ScopePtr&)> prepareModuleScope,
FrontendOptions options,
TypeCheckLimits limits,

View file

@ -12,8 +12,9 @@ std::optional<TypeId> generalize(
NotNull<TypeArena> arena,
NotNull<BuiltinTypes> builtinTypes,
NotNull<Scope> scope,
NotNull<DenseHashSet<TypeId>> bakedTypes,
NotNull<DenseHashSet<TypeId>> cachedTypes,
TypeId ty,
/* avoid sealing tables*/ bool avoidSealingTables = false
);
}

View file

@ -19,7 +19,9 @@ struct GlobalTypes
TypeArena globalTypes;
SourceModule globalNames; // names for symbols entered into globalScope
ScopePtr globalScope; // shared by all modules
ScopePtr globalTypeFunctionScope; // shared by all modules
};
} // namespace Luau

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

@ -31,13 +31,4 @@ struct OrderedMap
}
};
struct QuantifierResult
{
TypeId result;
OrderedMap<TypeId, TypeId> insertedGenerics;
OrderedMap<TypePackId, TypePackId> insertedGenericPacks;
};
std::optional<QuantifierResult> quantify(TypeArena* arena, TypeId ty, Scope* scope);
} // namespace Luau

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

@ -35,7 +35,7 @@ struct Scope
explicit Scope(TypePackId returnType); // root scope
explicit Scope(const ScopePtr& parent, int subLevel = 0); // child scope. Parent must not be nullptr.
const ScopePtr parent; // null for the root
ScopePtr parent; // null for the root
// All the children of this scope.
std::vector<NotNull<Scope>> children;
@ -59,6 +59,8 @@ struct Scope
std::optional<TypeId> lookup(Symbol sym) const;
std::optional<TypeId> lookupUnrefinedType(DefId def) const;
std::optional<TypeId> lookupRValueRefinementType(DefId def) const;
std::optional<TypeId> lookup(DefId def) const;
std::optional<std::pair<TypeId, Scope*>> lookupEx(DefId def);
std::optional<std::pair<Binding*, Scope*>> lookupEx(Symbol sym);
@ -71,6 +73,7 @@ struct Scope
// WARNING: This function linearly scans for a string key of equal value! It is thus O(n**2)
std::optional<Binding> linearSearchForBinding(const std::string& name, bool traverseScopeChain = true) const;
std::optional<std::pair<Symbol, Binding>> linearSearchForBindingPair(const std::string& name, bool traverseScopeChain) const;
RefinementMap refinements;

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

@ -622,7 +622,6 @@ struct UserDefinedFunctionData
AstStatTypeFunction* definition = nullptr;
DenseHashMap<Name, std::pair<AstStatTypeFunction*, size_t>> environment{""};
DenseHashMap<Name, AstStatTypeFunction*> environment_DEPRECATED{""};
};
/**

View file

@ -13,6 +13,8 @@
#include "Luau/TypeOrPack.h"
#include "Luau/TypeUtils.h"
LUAU_FASTFLAG(LuauImproveTypePathsInErrors)
namespace Luau
{
@ -38,18 +40,29 @@ struct Reasonings
std::string toString()
{
if (FFlag::LuauImproveTypePathsInErrors && reasons.empty())
return "";
// DenseHashSet ordering is entirely undefined, so we want to
// sort the reasons here to achieve a stable error
// stringification.
std::sort(reasons.begin(), reasons.end());
std::string allReasons;
std::string allReasons = FFlag::LuauImproveTypePathsInErrors ? "\nthis is because " : "";
bool first = true;
for (const std::string& reason : reasons)
{
if (first)
first = false;
if (FFlag::LuauImproveTypePathsInErrors)
{
if (reasons.size() > 1)
allReasons += "\n\t * ";
}
else
allReasons += "\n\t";
{
if (first)
first = false;
else
allReasons += "\n\t";
}
allReasons += reason;
}

View file

@ -48,6 +48,9 @@ struct TypeFunctionRuntime
// Evaluation of type functions should only be performed in the absence of parse errors in the source module
bool allowEvaluation = true;
// Root scope in which the type function operates in, set up by ConstraintGenerator
ScopePtr rootScope;
// Output created by 'print' function
std::vector<std::string> messages;

View file

@ -223,8 +223,6 @@ struct TypeFunctionClassType
std::optional<TypeFunctionTypeId> writeParent;
TypeId classTy;
std::string name_DEPRECATED;
};
struct TypeFunctionGenericType

View file

@ -28,14 +28,8 @@ struct TypeFunctionRuntimeBuilderState
{
NotNull<TypeFunctionContext> ctx;
// Mapping of class name to ClassType
// Invariant: users can not create a new class types -> any class types that get deserialized must have been an argument to the type function
// Using this invariant, whenever a ClassType is serialized, we can put it into this map
// whenever a ClassType is deserialized, we can use this map to return the corresponding value
DenseHashMap<std::string, TypeId> classesSerialized_DEPRECATED{{}};
// List of errors that occur during serialization/deserialization
// At every iteration of serialization/deserialzation, if this list.size() != 0, we halt the process
// At every iteration of serialization/deserialization, if this list.size() != 0, we halt the process
std::vector<std::string> errors{};
TypeFunctionRuntimeBuilderState(NotNull<TypeFunctionContext> ctx)

View file

@ -42,9 +42,19 @@ struct Property
/// element.
struct Index
{
enum class Variant
{
Pack,
Union,
Intersection
};
/// The 0-based index to use for the lookup.
size_t index;
/// The sort of thing we're indexing from, this is used in stringifying the type path for errors.
Variant variant;
bool operator==(const Index& other) const;
};
@ -205,6 +215,9 @@ using Path = TypePath::Path;
/// terribly clear to end users of the Luau type system.
std::string toString(const TypePath::Path& path, bool prefixDot = false);
/// Converts a Path to a human readable string for error reporting.
std::string toStringHuman(const TypePath::Path& path);
std::optional<TypeOrPack> traverse(TypeId root, const Path& path, NotNull<BuiltinTypes> builtinTypes);
std::optional<TypeOrPack> traverse(TypePackId root, const Path& path, NotNull<BuiltinTypes> builtinTypes);

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

@ -1065,6 +1065,11 @@ struct AstJsonEncoder : public AstVisitor
);
}
void write(class AstTypeOptional* node)
{
writeNode(node, "AstTypeOptional", [&]() {});
}
void write(class AstTypeUnion* node)
{
writeNode(

View file

@ -23,10 +23,14 @@
LUAU_FASTFLAG(LuauSolverV2)
LUAU_FASTINT(LuauTypeInferIterationLimit)
LUAU_FASTINT(LuauTypeInferRecursionLimit)
LUAU_FASTFLAGVARIABLE(DebugLuauMagicVariableNames)
LUAU_FASTFLAG(LuauExposeRequireByStringAutocomplete)
LUAU_FASTFLAGVARIABLE(LuauAutocompleteRefactorsForIncrementalAutocomplete)
LUAU_FASTFLAGVARIABLE(LuauAutocompleteUsesModuleForTypeCompatibility)
LUAU_FASTFLAGVARIABLE(LuauAutocompleteUnionCopyPreviousSeen)
static const std::unordered_set<std::string> kStatementStartingKeywords =
{"while", "if", "local", "repeat", "function", "do", "for", "return", "break", "continue", "type", "export"};
@ -481,6 +485,21 @@ static void autocompleteProps(
AutocompleteEntryMap inner;
std::unordered_set<TypeId> innerSeen;
// If we don't do this, and we have the misfortune of receiving a
// recursive union like:
//
// t1 where t1 = t1 | Class
//
// Then we are on a one way journey to a stack overflow.
if (FFlag::LuauAutocompleteUnionCopyPreviousSeen)
{
for (auto ty: seen)
{
if (is<UnionType, IntersectionType>(ty))
innerSeen.insert(ty);
}
}
if (isNil(*iter))
{
++iter;
@ -1343,6 +1362,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()))
@ -1509,10 +1537,14 @@ static std::optional<AutocompleteEntryMap> convertRequireSuggestionsToAutocomple
return std::nullopt;
AutocompleteEntryMap result;
for (const RequireSuggestion& suggestion : *suggestions)
for (RequireSuggestion& suggestion : *suggestions)
{
AutocompleteEntry entry = {AutocompleteEntryKind::RequirePath};
entry.insertText = std::move(suggestion.fullPath);
if (FFlag::LuauExposeRequireByStringAutocomplete)
{
entry.tags = std::move(suggestion.tags);
}
result[std::move(suggestion.label)] = std::move(entry);
}
return result;

View file

@ -32,8 +32,8 @@ LUAU_FASTFLAG(LuauSolverV2)
LUAU_FASTFLAGVARIABLE(LuauStringFormatErrorSuppression)
LUAU_FASTFLAGVARIABLE(LuauTableCloneClonesType3)
LUAU_FASTFLAG(LuauTrackInteriorFreeTypesOnScope)
LUAU_FASTFLAGVARIABLE(LuauFreezeIgnorePersistent)
LUAU_FASTFLAGVARIABLE(LuauFollowTableFreeze)
LUAU_FASTFLAGVARIABLE(LuauUserTypeFunTypecheck)
namespace Luau
{
@ -288,6 +288,22 @@ void assignPropDocumentationSymbols(TableType::Props& props, const std::string&
}
}
static void finalizeGlobalBindings(ScopePtr scope)
{
LUAU_ASSERT(FFlag::LuauUserTypeFunTypecheck);
for (const auto& pair : scope->bindings)
{
persist(pair.second.typeId);
if (TableType* ttv = getMutable<TableType>(pair.second.typeId))
{
if (!ttv->name)
ttv->name = "typeof(" + toString(pair.first) + ")";
}
}
}
void registerBuiltinGlobals(Frontend& frontend, GlobalTypes& globals, bool typeCheckForAutocomplete)
{
LUAU_ASSERT(!globals.globalTypes.types.isFrozen());
@ -399,14 +415,21 @@ void registerBuiltinGlobals(Frontend& frontend, GlobalTypes& globals, bool typeC
// clang-format on
}
for (const auto& pair : globals.globalScope->bindings)
if (FFlag::LuauUserTypeFunTypecheck)
{
persist(pair.second.typeId);
if (TableType* ttv = getMutable<TableType>(pair.second.typeId))
finalizeGlobalBindings(globals.globalScope);
}
else
{
for (const auto& pair : globals.globalScope->bindings)
{
if (!ttv->name)
ttv->name = "typeof(" + toString(pair.first) + ")";
persist(pair.second.typeId);
if (TableType* ttv = getMutable<TableType>(pair.second.typeId))
{
if (!ttv->name)
ttv->name = "typeof(" + toString(pair.first) + ")";
}
}
}
@ -467,6 +490,59 @@ void registerBuiltinGlobals(Frontend& frontend, GlobalTypes& globals, bool typeC
TypeId requireTy = getGlobalBinding(globals, "require");
attachTag(requireTy, kRequireTagName);
attachMagicFunction(requireTy, std::make_shared<MagicRequire>());
if (FFlag::LuauUserTypeFunTypecheck)
{
// Global scope cannot be the parent of the type checking environment because it can be changed by the embedder
globals.globalTypeFunctionScope->exportedTypeBindings = globals.globalScope->exportedTypeBindings;
globals.globalTypeFunctionScope->builtinTypeNames = globals.globalScope->builtinTypeNames;
// Type function runtime also removes a few standard libraries and globals, so we will take only the ones that are defined
static const char* typeFunctionRuntimeBindings[] = {
// Libraries
"math",
"table",
"string",
"bit32",
"utf8",
"buffer",
// Globals
"assert",
"error",
"print",
"next",
"ipairs",
"pairs",
"select",
"unpack",
"getmetatable",
"setmetatable",
"rawget",
"rawset",
"rawlen",
"rawequal",
"tonumber",
"tostring",
"type",
"typeof",
};
for (auto& name : typeFunctionRuntimeBindings)
{
AstName astName = globals.globalNames.names->get(name);
LUAU_ASSERT(astName.value);
globals.globalTypeFunctionScope->bindings[astName] = globals.globalScope->bindings[astName];
}
LoadDefinitionFileResult typeFunctionLoadResult = frontend.loadDefinitionFile(
globals, globals.globalTypeFunctionScope, getTypeFunctionDefinitionSource(), "@luau", /* captureComments */ false, false
);
LUAU_ASSERT(typeFunctionLoadResult.success);
finalizeGlobalBindings(globals.globalTypeFunctionScope);
}
}
static std::vector<TypeId> parseFormatString(NotNull<BuiltinTypes> builtinTypes, const char* data, size_t size)
@ -1444,7 +1520,7 @@ bool MagicClone::infer(const MagicFunctionCallContext& context)
return false;
CloneState cloneState{context.solver->builtinTypes};
TypeId resultType = shallowClone(inputType, *arena, cloneState, /* ignorePersistent */ FFlag::LuauFreezeIgnorePersistent);
TypeId resultType = shallowClone(inputType, *arena, cloneState, /* ignorePersistent */ true);
if (auto tableType = getMutable<TableType>(resultType))
{
@ -1481,7 +1557,7 @@ static std::optional<TypeId> freezeTable(TypeId inputType, const MagicFunctionCa
{
// Clone the input type, this will become our final result type after we mutate it.
CloneState cloneState{context.solver->builtinTypes};
TypeId resultType = shallowClone(inputType, *arena, cloneState, /* ignorePersistent */ FFlag::LuauFreezeIgnorePersistent);
TypeId resultType = shallowClone(inputType, *arena, cloneState, /* ignorePersistent */ true);
auto tableTy = getMutable<TableType>(resultType);
// `clone` should not break this.
LUAU_ASSERT(tableTy);

View file

@ -1,17 +1,20 @@
// This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details
#include "Luau/Clone.h"
#include "Luau/Common.h"
#include "Luau/NotNull.h"
#include "Luau/Type.h"
#include "Luau/TypePack.h"
#include "Luau/Unifiable.h"
#include "Luau/VisitType.h"
LUAU_FASTFLAG(LuauSolverV2)
LUAU_FASTFLAG(LuauFreezeIgnorePersistent)
// For each `Luau::clone` call, we will clone only up to N amount of types _and_ packs, as controlled by this limit.
LUAU_FASTINTVARIABLE(LuauTypeCloneIterationLimit, 100'000)
LUAU_FASTFLAGVARIABLE(LuauClonedTableAndFunctionTypesMustHaveScopes)
LUAU_FASTFLAGVARIABLE(LuauDoNotClonePersistentBindings)
LUAU_FASTFLAG(LuauIncrementalAutocompleteDemandBasedCloning)
namespace Luau
{
@ -28,6 +31,8 @@ const T* get(const Kind& kind)
class TypeCloner
{
protected:
NotNull<TypeArena> arena;
NotNull<BuiltinTypes> builtinTypes;
@ -62,6 +67,8 @@ public:
{
}
virtual ~TypeCloner() = default;
TypeId clone(TypeId ty)
{
shallowClone(ty);
@ -120,12 +127,13 @@ private:
}
}
protected:
std::optional<TypeId> find(TypeId ty) const
{
ty = follow(ty, FollowOption::DisableLazyTypeThunks);
if (auto it = types->find(ty); it != types->end())
return it->second;
else if (ty->persistent && (!FFlag::LuauFreezeIgnorePersistent || ty != forceTy))
else if (ty->persistent && ty != forceTy)
return ty;
return std::nullopt;
}
@ -135,7 +143,7 @@ private:
tp = follow(tp);
if (auto it = packs->find(tp); it != packs->end())
return it->second;
else if (tp->persistent && (!FFlag::LuauFreezeIgnorePersistent || tp != forceTp))
else if (tp->persistent && tp != forceTp)
return tp;
return std::nullopt;
}
@ -154,14 +162,14 @@ 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);
if (auto clone = find(ty))
return *clone;
else if (ty->persistent && (!FFlag::LuauFreezeIgnorePersistent || ty != forceTy))
else if (ty->persistent && ty != forceTy)
return ty;
TypeId target = arena->addType(ty->ty);
@ -181,13 +189,13 @@ public:
return target;
}
TypePackId shallowClone(TypePackId tp)
virtual TypePackId shallowClone(TypePackId tp)
{
tp = follow(tp);
if (auto clone = find(tp))
return *clone;
else if (tp->persistent && (!FFlag::LuauFreezeIgnorePersistent || tp != forceTp))
else if (tp->persistent && tp != forceTp)
return tp;
TypePackId target = arena->addTypePack(tp->ty);
@ -389,7 +397,7 @@ private:
ty = shallowClone(ty);
}
void cloneChildren(LazyType* t)
virtual void cloneChildren(LazyType* t)
{
if (auto unwrapped = t->unwrapped.load())
t->unwrapped.store(shallowClone(unwrapped));
@ -469,11 +477,99 @@ private:
}
};
class FragmentAutocompleteTypeCloner final : public TypeCloner
{
Scope* replacementForNullScope = nullptr;
public:
FragmentAutocompleteTypeCloner(
NotNull<TypeArena> arena,
NotNull<BuiltinTypes> builtinTypes,
NotNull<SeenTypes> types,
NotNull<SeenTypePacks> packs,
TypeId forceTy,
TypePackId forceTp,
Scope* replacementForNullScope
)
: TypeCloner(arena, builtinTypes, types, packs, forceTy, forceTp)
, replacementForNullScope(replacementForNullScope)
{
LUAU_ASSERT(replacementForNullScope);
}
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 && 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 = replacementForNullScope;
}
else if (auto tt = getMutable<TableType>(target))
{
if (FFlag::LuauClonedTableAndFunctionTypesMustHaveScopes)
tt->scope = replacementForNullScope;
}
else if (auto fn = getMutable<FunctionType>(target))
{
if (FFlag::LuauClonedTableAndFunctionTypesMustHaveScopes)
fn->scope = replacementForNullScope;
}
(*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 && 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 = replacementForNullScope;
(*packs)[tp] = target;
queue.emplace_back(target);
return target;
}
void cloneChildren(LazyType* t) override
{
// Do not clone lazy types
if (!FFlag::LuauIncrementalAutocompleteDemandBasedCloning)
{
if (auto unwrapped = t->unwrapped.load())
t->unwrapped.store(shallowClone(unwrapped));
}
}
};
} // namespace
TypePackId shallowClone(TypePackId tp, TypeArena& dest, CloneState& cloneState, bool ignorePersistent)
{
if (tp->persistent && (!FFlag::LuauFreezeIgnorePersistent || !ignorePersistent))
if (tp->persistent && !ignorePersistent)
return tp;
TypeCloner cloner{
@ -482,7 +578,7 @@ TypePackId shallowClone(TypePackId tp, TypeArena& dest, CloneState& cloneState,
NotNull{&cloneState.seenTypes},
NotNull{&cloneState.seenTypePacks},
nullptr,
FFlag::LuauFreezeIgnorePersistent && ignorePersistent ? tp : nullptr
ignorePersistent ? tp : nullptr
};
return cloner.shallowClone(tp);
@ -490,7 +586,7 @@ TypePackId shallowClone(TypePackId tp, TypeArena& dest, CloneState& cloneState,
TypeId shallowClone(TypeId typeId, TypeArena& dest, CloneState& cloneState, bool ignorePersistent)
{
if (typeId->persistent && (!FFlag::LuauFreezeIgnorePersistent || !ignorePersistent))
if (typeId->persistent && !ignorePersistent)
return typeId;
TypeCloner cloner{
@ -498,7 +594,7 @@ TypeId shallowClone(TypeId typeId, TypeArena& dest, CloneState& cloneState, bool
cloneState.builtinTypes,
NotNull{&cloneState.seenTypes},
NotNull{&cloneState.seenTypePacks},
FFlag::LuauFreezeIgnorePersistent && ignorePersistent ? typeId : nullptr,
ignorePersistent ? typeId : nullptr,
nullptr
};
@ -564,4 +660,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 = FFlag::LuauDoNotClonePersistentBindings && binding.typeId->persistent ? binding.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"
@ -38,9 +39,13 @@ LUAU_FASTFLAG(DebugLuauGreedyGeneralization)
LUAU_FASTFLAGVARIABLE(LuauTrackInteriorFreeTypesOnScope)
LUAU_FASTFLAGVARIABLE(LuauDeferBidirectionalInferenceForTableAssignment)
LUAU_FASTFLAGVARIABLE(LuauUngeneralizedTypesForRecursiveFunctions)
LUAU_FASTFLAGVARIABLE(LuauGlobalSelfAssignmentCycle)
LUAU_FASTFLAG(LuauFreeTypesMustHaveBounds)
LUAU_FASTFLAGVARIABLE(LuauInferLocalTypesInMultipleAssignments)
LUAU_FASTFLAGVARIABLE(LuauDoNotLeakNilInRefinement)
LUAU_FASTFLAGVARIABLE(LuauExtraFollows)
LUAU_FASTFLAG(LuauUserTypeFunTypecheck)
namespace Luau
{
@ -181,6 +186,7 @@ ConstraintGenerator::ConstraintGenerator(
NotNull<BuiltinTypes> builtinTypes,
NotNull<InternalErrorReporter> ice,
const ScopePtr& globalScope,
const ScopePtr& typeFunctionScope,
std::function<void(const ModuleName&, const ScopePtr&)> prepareModuleScope,
DcrLogger* logger,
NotNull<DataFlowGraph> dfg,
@ -197,6 +203,7 @@ ConstraintGenerator::ConstraintGenerator(
, moduleResolver(moduleResolver)
, ice(ice)
, globalScope(globalScope)
, typeFunctionScope(typeFunctionScope)
, prepareModuleScope(std::move(prepareModuleScope))
, requireCycles(std::move(requireCycles))
, logger(logger)
@ -218,6 +225,14 @@ void ConstraintGenerator::visitModuleRoot(AstStatBlock* block)
rootScope->returnType = freshTypePack(scope);
if (FFlag::LuauUserTypeFunTypecheck)
{
// Create module-local scope for the type function environment
ScopePtr localTypeFunctionScope = std::make_shared<Scope>(typeFunctionScope);
localTypeFunctionScope->location = block->location;
typeFunctionRuntime->rootScope = localTypeFunctionScope;
}
TypeId moduleFnTy = arena->addType(FunctionType{TypeLevel{}, rootScope, builtinTypes->anyTypePack, rootScope->returnType});
interiorTypes.emplace_back();
@ -528,7 +543,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);
}
}
}
@ -688,6 +711,9 @@ void ConstraintGenerator::checkAliases(const ScopePtr& scope, AstStatBlock* bloc
std::unordered_map<Name, Location> aliasDefinitionLocations;
std::unordered_map<Name, Location> classDefinitionLocations;
bool hasTypeFunction = false;
ScopePtr typeFunctionEnvScope;
// In order to enable mutually-recursive type aliases, we need to
// populate the type bindings before we actually check any of the
// alias statements.
@ -733,6 +759,9 @@ void ConstraintGenerator::checkAliases(const ScopePtr& scope, AstStatBlock* bloc
}
else if (auto function = stat->as<AstStatTypeFunction>())
{
if (FFlag::LuauUserTypeFunTypecheck)
hasTypeFunction = true;
// If a type function w/ same name has already been defined, error for having duplicates
if (scope->exportedTypeBindings.count(function->name.value) || scope->privateTypeBindings.count(function->name.value))
{
@ -742,7 +771,8 @@ void ConstraintGenerator::checkAliases(const ScopePtr& scope, AstStatBlock* bloc
continue;
}
ScopePtr defnScope = childScope(function, scope);
// Variable becomes unused with the removal of FFlag::LuauUserTypeFunTypecheck
ScopePtr defnScope = FFlag::LuauUserTypeFunTypecheck ? nullptr : childScope(function, scope);
// Create TypeFunctionInstanceType
@ -809,11 +839,22 @@ void ConstraintGenerator::checkAliases(const ScopePtr& scope, AstStatBlock* bloc
}
}
if (FFlag::LuauUserTypeFunTypecheck && hasTypeFunction)
typeFunctionEnvScope = std::make_shared<Scope>(typeFunctionRuntime->rootScope);
// Additional pass for user-defined type functions to fill in their environments completely
for (AstStat* stat : block->body)
{
if (auto function = stat->as<AstStatTypeFunction>())
{
if (FFlag::LuauUserTypeFunTypecheck)
{
// Similar to global pre-population, create a binding for each type function in the scope upfront
TypeId bt = arena->addType(BlockedType{});
typeFunctionEnvScope->bindings[function->name] = Binding{bt, function->location};
astTypeFunctionEnvironmentScopes[function] = typeFunctionEnvScope;
}
// Find the type function we have already created
TypeFunctionInstanceType* mainTypeFun = nullptr;
@ -832,51 +873,60 @@ void ConstraintGenerator::checkAliases(const ScopePtr& scope, AstStatBlock* bloc
UserDefinedFunctionData& userFuncData = mainTypeFun->userFuncData;
size_t level = 0;
for (Scope* curr = scope.get(); curr; curr = curr->parent.get())
if (FFlag::LuauUserTypeFunTypecheck)
{
for (auto& [name, tf] : curr->privateTypeBindings)
auto addToEnvironment = [this](UserDefinedFunctionData& userFuncData, ScopePtr scope, const Name& name, TypeId type, size_t level)
{
if (userFuncData.environment.find(name))
continue;
return;
if (auto ty = get<TypeFunctionInstanceType>(tf.type); ty && ty->userFuncData.definition)
if (auto ty = get<TypeFunctionInstanceType>(type); ty && ty->userFuncData.definition)
{
userFuncData.environment[name] = std::make_pair(ty->userFuncData.definition, level);
}
for (auto& [name, tf] : curr->exportedTypeBindings)
if (auto it = astTypeFunctionEnvironmentScopes.find(ty->userFuncData.definition))
{
if (auto existing = (*it)->linearSearchForBinding(name, /* traverseScopeChain */ false))
scope->bindings[ty->userFuncData.definition->name] =
Binding{existing->typeId, ty->userFuncData.definition->location};
}
}
};
for (Scope* curr = scope.get(); curr; curr = curr->parent.get())
{
if (userFuncData.environment.find(name))
continue;
for (auto& [name, tf] : curr->privateTypeBindings)
addToEnvironment(userFuncData, typeFunctionEnvScope, name, tf.type, level);
if (auto ty = get<TypeFunctionInstanceType>(tf.type); ty && ty->userFuncData.definition)
userFuncData.environment[name] = std::make_pair(ty->userFuncData.definition, level);
for (auto& [name, tf] : curr->exportedTypeBindings)
addToEnvironment(userFuncData, typeFunctionEnvScope, name, tf.type, level);
level++;
}
level++;
}
}
else if (mainTypeFun)
{
UserDefinedFunctionData& userFuncData = mainTypeFun->userFuncData;
for (Scope* curr = scope.get(); curr; curr = curr->parent.get())
else
{
for (auto& [name, tf] : curr->privateTypeBindings)
for (Scope* curr = scope.get(); curr; curr = curr->parent.get())
{
if (userFuncData.environment_DEPRECATED.find(name))
continue;
for (auto& [name, tf] : curr->privateTypeBindings)
{
if (userFuncData.environment.find(name))
continue;
if (auto ty = get<TypeFunctionInstanceType>(tf.type); ty && ty->userFuncData.definition)
userFuncData.environment_DEPRECATED[name] = ty->userFuncData.definition;
}
if (auto ty = get<TypeFunctionInstanceType>(tf.type); ty && ty->userFuncData.definition)
userFuncData.environment[name] = std::make_pair(ty->userFuncData.definition, level);
}
for (auto& [name, tf] : curr->exportedTypeBindings)
{
if (userFuncData.environment_DEPRECATED.find(name))
continue;
for (auto& [name, tf] : curr->exportedTypeBindings)
{
if (userFuncData.environment.find(name))
continue;
if (auto ty = get<TypeFunctionInstanceType>(tf.type); ty && ty->userFuncData.definition)
userFuncData.environment_DEPRECATED[name] = ty->userFuncData.definition;
if (auto ty = get<TypeFunctionInstanceType>(tf.type); ty && ty->userFuncData.definition)
userFuncData.environment[name] = std::make_pair(ty->userFuncData.definition, level);
}
level++;
}
}
}
@ -1678,6 +1728,64 @@ ControlFlow ConstraintGenerator::visit(const ScopePtr& scope, AstStatTypeAlias*
ControlFlow ConstraintGenerator::visit(const ScopePtr& scope, AstStatTypeFunction* function)
{
if (!FFlag::LuauUserTypeFunTypecheck)
return ControlFlow::None;
auto scopePtr = astTypeFunctionEnvironmentScopes.find(function);
LUAU_ASSERT(scopePtr);
Checkpoint startCheckpoint = checkpoint(this);
FunctionSignature sig = checkFunctionSignature(*scopePtr, function->body, /* expectedType */ std::nullopt);
// Place this function as a child of the non-type function scope
scope->children.push_back(NotNull{sig.signatureScope.get()});
interiorTypes.push_back(std::vector<TypeId>{});
checkFunctionBody(sig.bodyScope, function->body);
Checkpoint endCheckpoint = checkpoint(this);
TypeId generalizedTy = arena->addType(BlockedType{});
NotNull<Constraint> gc = addConstraint(
sig.signatureScope,
function->location,
GeneralizationConstraint{
generalizedTy, sig.signature, FFlag::LuauTrackInteriorFreeTypesOnScope ? std::vector<TypeId>{} : std::move(interiorTypes.back())
}
);
if (FFlag::LuauTrackInteriorFreeTypesOnScope)
sig.signatureScope->interiorFreeTypes = std::move(interiorTypes.back());
getMutable<BlockedType>(generalizedTy)->setOwner(gc);
interiorTypes.pop_back();
Constraint* previous = nullptr;
forEachConstraint(
startCheckpoint,
endCheckpoint,
this,
[gc, &previous](const ConstraintPtr& constraint)
{
gc->dependencies.emplace_back(constraint.get());
if (auto psc = get<PackSubtypeConstraint>(*constraint); psc && psc->returns)
{
if (previous)
constraint->dependencies.push_back(NotNull{previous});
previous = constraint.get();
}
}
);
std::optional<TypeId> existingFunctionTy = (*scopePtr)->lookup(function->name);
if (!existingFunctionTy)
ice->ice("checkAliases did not populate type function name", function->nameLocation);
if (auto bt = get<BlockedType>(*existingFunctionTy); bt && nullptr == bt->getOwner())
emplaceType<BoundType>(asMutable(*existingFunctionTy), generalizedTy);
return ControlFlow::None;
}
@ -1986,7 +2094,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
@ -2000,7 +2108,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
@ -2083,7 +2191,7 @@ InferencePack ConstraintGenerator::checkPack(const ScopePtr& scope, AstExprCall*
{
std::vector<TypeId> unpackedTypes;
if (args.size() > 0)
target = args[0];
target = FFlag::LuauExtraFollows ? follow(args[0]) : args[0];
else
{
target = arena->addType(BlockedType{});
@ -2892,6 +3000,13 @@ void ConstraintGenerator::visitLValue(const ScopePtr& scope, AstExprGlobal* glob
DefId def = dfg->getDef(global);
rootScope->lvalueTypes[def] = rhsType;
if (FFlag::LuauGlobalSelfAssignmentCycle)
{
// Ignore possible self-assignment, it doesn't create a new constraint
if (annotatedTy == follow(rhsType))
return;
}
// Sketchy: We're specifically looking for BlockedTypes that were
// initially created by ConstraintGenerator::prepopulateGlobalScope.
if (auto bt = get<BlockedType>(follow(*annotatedTy)); bt && !bt->getOwner())
@ -3033,6 +3148,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
@ -3046,6 +3162,7 @@ Inference ConstraintGenerator::check(const ScopePtr& scope, AstExprTable* expr,
builtinTypes,
arena,
NotNull{&unifier},
NotNull{&sp},
*expectedType,
ty,
expr,
@ -3084,7 +3201,7 @@ ConstraintGenerator::FunctionSignature ConstraintGenerator::checkFunctionSignatu
signatureScope = childScope(fn, parent);
// We need to assign returnType before creating bodyScope so that the
// return type gets propogated to bodyScope.
// return type gets propagated to bodyScope.
returnType = freshTypePack(signatureScope);
signatureScope->returnType = returnType;
@ -3510,6 +3627,10 @@ TypeId ConstraintGenerator::resolveType(const ScopePtr& scope, AstType* ty, bool
TypeId exprType = check(scope, tof->expr).ty;
result = exprType;
}
else if (ty->is<AstTypeOptional>())
{
return builtinTypes->nilType;
}
else if (auto unionAnnotation = ty->as<AstTypeUnion>())
{
if (FFlag::LuauPreserveUnionIntersectionNodeForLeadingTokenSingleType)
@ -3872,9 +3993,18 @@ struct GlobalPrepopulator : AstVisitor
void ConstraintGenerator::prepopulateGlobalScopeForFragmentTypecheck(const ScopePtr& globalScope, const ScopePtr& resumeScope, AstStatBlock* program)
{
FragmentTypeCheckGlobalPrepopulator gp{NotNull{globalScope.get()}, NotNull{resumeScope.get()}, dfg, arena};
if (prepareModuleScope)
prepareModuleScope(module->name, resumeScope);
program->visit(&gp);
if (FFlag::LuauUserTypeFunTypecheck)
{
// Handle type function globals as well, without preparing a module scope since they have a separate environment
GlobalPrepopulator tfgp{NotNull{typeFunctionRuntime->rootScope.get()}, arena, dfg};
program->visit(&tfgp);
}
}
void ConstraintGenerator::prepopulateGlobalScope(const ScopePtr& globalScope, AstStatBlock* program)
@ -3885,6 +4015,13 @@ void ConstraintGenerator::prepopulateGlobalScope(const ScopePtr& globalScope, As
prepareModuleScope(module->name, globalScope);
program->visit(&gp);
if (FFlag::LuauUserTypeFunTypecheck)
{
// Handle type function globals as well, without preparing a module scope since they have a separate environment
GlobalPrepopulator tfgp{NotNull{typeFunctionRuntime->rootScope.get()}, arena, dfg};
program->visit(&tfgp);
}
}
bool ConstraintGenerator::recordPropertyAssignment(TypeId ty)

View file

@ -37,6 +37,7 @@ LUAU_FASTFLAG(LuauTrackInteriorFreeTypesOnScope)
LUAU_FASTFLAGVARIABLE(LuauTrackInteriorFreeTablesOnScope)
LUAU_FASTFLAGVARIABLE(LuauPrecalculateMutatedFreeTypes2)
LUAU_FASTFLAGVARIABLE(DebugLuauGreedyGeneralization)
LUAU_FASTFLAG(LuauSearchForRefineableType)
namespace Luau
{
@ -635,6 +636,7 @@ struct TypeSearcher : TypeVisitor
TypeId needle;
Polarity current = Polarity::Positive;
size_t count = 0;
Polarity result = Polarity::None;
explicit TypeSearcher(TypeId needle)
@ -649,7 +651,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 +754,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 +764,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);
@ -903,26 +908,16 @@ bool ConstraintSolver::tryDispatch(const GeneralizationConstraint& c, NotNull<co
else if (get<PendingExpansionType>(generalizedType))
return block(generalizedType, constraint);
std::optional<QuantifierResult> generalized;
std::optional<TypeId> generalizedTy = generalize(NotNull{arena}, builtinTypes, constraint->scope, generalizedTypes, c.sourceType);
if (generalizedTy)
generalized = QuantifierResult{*generalizedTy}; // FIXME insertedGenerics and insertedGenericPacks
else
if (!generalizedTy)
reportError(CodeTooComplex{}, constraint->location);
if (generalized)
if (generalizedTy)
{
if (get<BlockedType>(generalizedType))
bind(constraint, generalizedType, generalized->result);
bind(constraint, generalizedType, *generalizedTy);
else
unify(constraint, generalizedType, generalized->result);
for (auto [free, gen] : generalized->insertedGenerics.pairings)
unify(constraint, free, gen);
for (auto [free, gen] : generalized->insertedGenericPacks.pairings)
unify(constraint, free, gen);
unify(constraint, generalizedType, *generalizedTy);
}
else
{
@ -1352,15 +1347,29 @@ void ConstraintSolver::fillInDiscriminantTypes(NotNull<const Constraint> constra
if (!ty)
continue;
// If the discriminant type has been transmuted, we need to unblock them.
if (!isBlocked(*ty))
if (FFlag::LuauSearchForRefineableType)
{
if (isBlocked(*ty))
// We bind any unused discriminants to the `*no-refine*` type indicating that it can be safely ignored.
emplaceType<BoundType>(asMutable(follow(*ty)), builtinTypes->noRefineType);
// We also need to unconditionally unblock these types, otherwise
// you end up with funky looking "Blocked on *no-refine*."
unblock(*ty, constraint->location);
continue;
}
else
{
// We bind any unused discriminants to the `*no-refine*` type indicating that it can be safely ignored.
emplaceType<BoundType>(asMutable(follow(*ty)), builtinTypes->noRefineType);
// If the discriminant type has been transmuted, we need to unblock them.
if (!isBlocked(*ty))
{
unblock(*ty, constraint->location);
continue;
}
// We bind any unused discriminants to the `*no-refine*` type indicating that it can be safely ignored.
emplaceType<BoundType>(asMutable(follow(*ty)), builtinTypes->noRefineType);
}
}
}
@ -1370,9 +1379,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 +1675,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 +1703,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)
@ -1159,6 +1168,8 @@ void DataFlowGraphBuilder::visitType(AstType* t)
return visitType(f);
else if (auto tyof = t->as<AstTypeTypeof>())
return visitType(tyof);
else if (auto o = t->as<AstTypeOptional>())
return;
else if (auto u = t->as<AstTypeUnion>())
return visitType(u);
else if (auto i = t->as<AstTypeIntersection>())

View file

@ -317,4 +317,83 @@ std::string getBuiltinDefinitionSource()
return result;
}
// TODO: split into separate tagged unions when the new solver can appropriately handle that.
static const std::string kBuiltinDefinitionTypesSrc = R"BUILTIN_SRC(
export type type = {
tag: "nil" | "unknown" | "never" | "any" | "boolean" | "number" | "string" | "buffer" | "thread" |
"singleton" | "negation" | "union" | "intesection" | "table" | "function" | "class" | "generic",
is: (self: type, arg: string) -> boolean,
-- for singleton type
value: (self: type) -> (string | boolean | nil),
-- for negation type
inner: (self: type) -> type,
-- for union and intersection types
components: (self: type) -> {type},
-- for table type
setproperty: (self: type, key: type, value: type?) -> (),
setreadproperty: (self: type, key: type, value: type?) -> (),
setwriteproperty: (self: type, key: type, value: type?) -> (),
readproperty: (self: type, key: type) -> type?,
writeproperty: (self: type, key: type) -> type?,
properties: (self: type) -> { [type]: { read: type?, write: type? } },
setindexer: (self: type, index: type, result: type) -> (),
setreadindexer: (self: type, index: type, result: type) -> (),
setwriteindexer: (self: type, index: type, result: type) -> (),
indexer: (self: type) -> { index: type, readresult: type, writeresult: type }?,
readindexer: (self: type) -> { index: type, result: type }?,
writeindexer: (self: type) -> { index: type, result: type }?,
setmetatable: (self: type, arg: type) -> (),
metatable: (self: type) -> type?,
-- for function type
setparameters: (self: type, head: {type}?, tail: type?) -> (),
parameters: (self: type) -> { head: {type}?, tail: type? },
setreturns: (self: type, head: {type}?, tail: type? ) -> (),
returns: (self: type) -> { head: {type}?, tail: type? },
setgenerics: (self: type, {type}?) -> (),
generics: (self: type) -> {type},
-- for class type
-- 'properties', 'metatable', 'indexer', 'readindexer' and 'writeindexer' are shared with table type
readparent: (self: type) -> type?,
writeparent: (self: type) -> type?,
-- for generic type
name: (self: type) -> string?,
ispack: (self: type) -> boolean,
}
declare types: {
unknown: type,
never: type,
any: type,
boolean: type,
number: type,
string: type,
thread: type,
buffer: type,
singleton: @checked (arg: string | boolean | nil) -> type,
generic: @checked (name: string, ispack: boolean?) -> type,
negationof: @checked (arg: type) -> type,
unionof: @checked (...type) -> type,
intersectionof: @checked (...type) -> type,
newtable: @checked (props: {[type]: type} | {[type]: { read: type, write: type } } | nil, indexer: { index: type, readresult: type, writeresult: type }?, metatable: type?) -> type,
newfunction: @checked (parameters: { head: {type}?, tail: type? }?, returns: { head: {type}?, tail: type? }?, generics: {type}?) -> type,
copy: @checked (arg: type) -> type,
}
)BUILTIN_SRC";
std::string getTypeFunctionDefinitionSource()
{
return kBuiltinDefinitionTypesSrc;
}
} // namespace Luau

View file

@ -8,6 +8,7 @@
#include "Luau/StringUtils.h"
#include "Luau/ToString.h"
#include "Luau/Type.h"
#include "Luau/TypeChecker2.h"
#include "Luau/TypeFunction.h"
#include <optional>
@ -17,6 +18,7 @@
#include <unordered_set>
LUAU_FASTINTVARIABLE(LuauIndentTypeMismatchMaxTypeLength, 10)
LUAU_FASTFLAG(LuauNonStrictFuncDefErrorFix)
static std::string wrongNumberOfArgsString(
size_t expectedCount,
@ -116,7 +118,10 @@ struct ErrorConverter
size_t luauIndentTypeMismatchMaxTypeLength = size_t(FInt::LuauIndentTypeMismatchMaxTypeLength);
if (givenType.length() <= luauIndentTypeMismatchMaxTypeLength || wantedType.length() <= luauIndentTypeMismatchMaxTypeLength)
return "Type " + given + " could not be converted into " + wanted;
return "Type\n " + given + "\ncould not be converted into\n " + wanted;
if (FFlag::LuauImproveTypePathsInErrors)
return "Type\n\t" + given + "\ncould not be converted into\n\t" + wanted;
else
return "Type\n " + given + "\ncould not be converted into\n " + wanted;
};
if (givenTypeName == wantedTypeName)
@ -751,8 +756,15 @@ struct ErrorConverter
std::string operator()(const NonStrictFunctionDefinitionError& e) const
{
return "Argument " + e.argument + " with type '" + toString(e.argumentType) + "' in function '" + e.functionName +
"' is used in a way that will run time error";
if (FFlag::LuauNonStrictFuncDefErrorFix && e.functionName.empty())
{
return "Argument " + e.argument + " with type '" + toString(e.argumentType) + "' is used in a way that will run time error";
}
else
{
return "Argument " + e.argument + " with type '" + toString(e.argumentType) + "' in function '" + e.functionName +
"' is used in a way that will run time error";
}
}
std::string operator()(const PropertyAccessViolation& e) const

View file

@ -0,0 +1,172 @@
// This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details
#include "Luau/FileResolver.h"
#include "Luau/Common.h"
#include "Luau/StringUtils.h"
#include <algorithm>
#include <memory>
#include <optional>
#include <string_view>
#include <utility>
LUAU_FASTFLAGVARIABLE(LuauExposeRequireByStringAutocomplete)
LUAU_FASTFLAGVARIABLE(LuauEscapeCharactersInRequireSuggestions)
LUAU_FASTFLAGVARIABLE(LuauHideImpossibleRequireSuggestions)
namespace Luau
{
static std::optional<RequireSuggestions> processRequireSuggestions(std::optional<RequireSuggestions> suggestions)
{
if (!suggestions)
return suggestions;
if (FFlag::LuauEscapeCharactersInRequireSuggestions)
{
for (RequireSuggestion& suggestion : *suggestions)
{
suggestion.fullPath = escape(suggestion.fullPath);
}
}
return suggestions;
}
static RequireSuggestions makeSuggestionsFromAliases(std::vector<RequireAlias> aliases)
{
RequireSuggestions result;
for (RequireAlias& alias : aliases)
{
RequireSuggestion suggestion;
suggestion.label = "@" + std::move(alias.alias);
suggestion.fullPath = suggestion.label;
suggestion.tags = std::move(alias.tags);
result.push_back(std::move(suggestion));
}
return result;
}
static RequireSuggestions makeSuggestionsForFirstComponent(std::unique_ptr<RequireNode> node)
{
RequireSuggestions result = makeSuggestionsFromAliases(node->getAvailableAliases());
result.push_back(RequireSuggestion{"./", "./", {}});
result.push_back(RequireSuggestion{"../", "../", {}});
return result;
}
static RequireSuggestions makeSuggestionsFromNode(std::unique_ptr<RequireNode> node, const std::string_view path, bool isPartialPath)
{
LUAU_ASSERT(!path.empty());
RequireSuggestions result;
const size_t lastSlashInPath = path.find_last_of('/');
if (lastSlashInPath != std::string_view::npos)
{
// Add a suggestion for the parent directory
RequireSuggestion parentSuggestion;
parentSuggestion.label = "..";
// TODO: after exposing require-by-string's path normalization API, this
// if-else can be replaced. Instead, we can simply normalize the result
// of inserting ".." at the end of the current path.
if (lastSlashInPath >= 2 && path.substr(lastSlashInPath - 2, 3) == "../")
{
parentSuggestion.fullPath = path.substr(0, lastSlashInPath + 1);
parentSuggestion.fullPath += "..";
}
else
{
parentSuggestion.fullPath = path.substr(0, lastSlashInPath);
}
result.push_back(std::move(parentSuggestion));
}
std::string fullPathPrefix;
if (isPartialPath)
{
// ./path/to/chi -> ./path/to/
fullPathPrefix += path.substr(0, lastSlashInPath + 1);
}
else
{
if (path.back() == '/')
{
// ./path/to/ -> ./path/to/
fullPathPrefix += path;
}
else
{
// ./path/to -> ./path/to/
fullPathPrefix += path;
fullPathPrefix += "/";
}
}
for (const std::unique_ptr<RequireNode>& child : node->getChildren())
{
if (!child)
continue;
std::string pathComponent = child->getPathComponent();
if (FFlag::LuauHideImpossibleRequireSuggestions)
{
// If path component contains a slash, it cannot be required by string.
// There's no point suggesting it.
if (pathComponent.find('/') != std::string::npos)
continue;
}
RequireSuggestion suggestion;
suggestion.label = isPartialPath || path.back() == '/' ? child->getLabel() : "/" + child->getLabel();
suggestion.fullPath = fullPathPrefix + std::move(pathComponent);
suggestion.tags = child->getTags();
result.push_back(std::move(suggestion));
}
return result;
}
std::optional<RequireSuggestions> RequireSuggester::getRequireSuggestionsImpl(const ModuleName& requirer, const std::optional<std::string>& path)
const
{
if (!path)
return std::nullopt;
std::unique_ptr<RequireNode> requirerNode = getNode(requirer);
if (!requirerNode)
return std::nullopt;
const size_t slashPos = path->find_last_of('/');
if (slashPos == std::string::npos)
return makeSuggestionsForFirstComponent(std::move(requirerNode));
// If path already points at a Node, return the Node's children as paths.
if (std::unique_ptr<RequireNode> node = requirerNode->resolvePathToNode(*path))
return makeSuggestionsFromNode(std::move(node), *path, /* isPartialPath = */ false);
// Otherwise, recover a partial path and use this to generate suggestions.
if (std::unique_ptr<RequireNode> partialNode = requirerNode->resolvePathToNode(path->substr(0, slashPos)))
return makeSuggestionsFromNode(std::move(partialNode), *path, /* isPartialPath = */ true);
return std::nullopt;
}
std::optional<RequireSuggestions> RequireSuggester::getRequireSuggestions(const ModuleName& requirer, const std::optional<std::string>& path) const
{
return processRequireSuggestions(getRequireSuggestionsImpl(requirer, path));
}
std::optional<RequireSuggestions> FileResolver::getRequireSuggestions(const ModuleName& requirer, const std::optional<std::string>& path) const
{
if (!FFlag::LuauExposeRequireByStringAutocomplete)
return std::nullopt;
return requireSuggester ? requireSuggester->getRequireSuggestions(requirer, path) : std::nullopt;
}
} // namespace Luau

View file

@ -30,9 +30,19 @@ LUAU_FASTFLAG(LuauAutocompleteRefactorsForIncrementalAutocomplete)
LUAU_FASTFLAGVARIABLE(LuauIncrementalAutocompleteBugfixes)
LUAU_FASTFLAGVARIABLE(LuauMixedModeDefFinderTraversesTypeOf)
LUAU_FASTFLAG(LuauBetterReverseDependencyTracking)
LUAU_FASTFLAGVARIABLE(LuauCloneIncrementalModule)
LUAU_FASTFLAGVARIABLE(LogFragmentsFromAutocomplete)
LUAU_FASTFLAGVARIABLE(LuauBetterCursorInCommentDetection)
LUAU_FASTFLAGVARIABLE(LuauAllFreeTypesHaveScopes)
LUAU_FASTFLAGVARIABLE(LuauFragmentAcSupportsReporter)
LUAU_FASTFLAGVARIABLE(LuauPersistConstraintGenerationScopes)
LUAU_FASTFLAG(LuauModuleHoldsAstRoot)
LUAU_FASTFLAGVARIABLE(LuauCloneTypeAliasBindings)
LUAU_FASTFLAGVARIABLE(LuauCloneReturnTypePack)
LUAU_FASTFLAGVARIABLE(LuauIncrementalAutocompleteDemandBasedCloning)
LUAU_FASTFLAG(LuauUserTypeFunTypecheck)
LUAU_FASTFLAGVARIABLE(LuauFragmentNoTypeFunEval)
namespace
{
template<typename T>
@ -54,7 +64,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,8 +72,155 @@ 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 UsageFinder : public AstVisitor
{
explicit UsageFinder(NotNull<DataFlowGraph> dfg)
: dfg(dfg)
{
// We explicitly suggest that the usage finder propulate types for instance and enum by default
// These are common enough types that sticking them in the environment is a good idea
// and it lets magic functions work correctly too.
referencedBindings.emplace_back("Instance");
referencedBindings.emplace_back("Enum");
}
bool visit(AstExprConstantString* expr) override
{
// Populating strings in the referenced bindings is nice too, because it means that magic functions that look
// up types by names will work correctly too.
// Only if the actual type alias exists will we populate it over, otherwise, the strings will just get ignored
referencedBindings.emplace_back(expr->value.data, expr->value.size);
return true;
}
bool visit(AstType* node) override
{
return true;
}
bool visit(AstStatTypeAlias* alias) override
{
declaredAliases.insert(std::string(alias->name.value));
return true;
}
bool visit(AstTypeReference* ref) override
{
if (std::optional<AstName> prefix = ref->prefix)
referencedImportedBindings.emplace_back(prefix->value, ref->name.value);
else
referencedBindings.emplace_back(ref->name.value);
return true;
}
bool visit(AstExpr* expr) override
{
if (auto opt = dfg->getDefOptional(expr))
mentionedDefs.insert(opt->get());
if (auto ref = dfg->getRefinementKey(expr))
mentionedDefs.insert(ref->def);
if (auto local = expr->as<AstExprLocal>())
localBindingsReferenced.emplace_back(dfg->getDef(local), local->local);
return true;
}
NotNull<DataFlowGraph> dfg;
DenseHashSet<Name> declaredAliases{""};
std::vector<std::pair<const Def*, AstLocal*>> localBindingsReferenced;
DenseHashSet<const Def*> mentionedDefs{nullptr};
std::vector<Name> referencedBindings{""};
std::vector<std::pair<Name, Name>> referencedImportedBindings{{"", ""}};
};
// Runs the `UsageFinder` traversal on the fragment and grabs all of the types that are
// referenced in the fragment. We'll clone these and place them in the appropriate spots
// in the scope so that they are available during typechecking.
void cloneTypesFromFragment(
CloneState& cloneState,
const Scope* staleScope,
const ModulePtr& staleModule,
NotNull<TypeArena> destArena,
NotNull<DataFlowGraph> dfg,
AstStatBlock* program,
Scope* destScope
)
{
LUAU_TIMETRACE_SCOPE("Luau::cloneTypesFromFragment", "FragmentAutocomplete");
UsageFinder f{dfg};
program->visit(&f);
// These are defs that have been mentioned. find the appropriate lvalue type and rvalue types and place them in the scope
// First - any locals that have been mentioned in the fragment need to be placed in the bindings and lvalueTypes secionts.
for (const auto& d : f.mentionedDefs)
{
if (std::optional<TypeId> rValueRefinement = staleScope->lookupRValueRefinementType(NotNull{d}))
{
destScope->rvalueRefinements[d] = Luau::cloneIncremental(*rValueRefinement, *destArena, cloneState, destScope);
}
if (std::optional<TypeId> lValue = staleScope->lookupUnrefinedType(NotNull{d}))
{
destScope->lvalueTypes[d] = Luau::cloneIncremental(*lValue, *destArena, cloneState, destScope);
}
}
for (const auto& [d, loc] : f.localBindingsReferenced)
{
if (std::optional<std::pair<Symbol, Binding>> pair = staleScope->linearSearchForBindingPair(loc->name.value, true))
{
destScope->lvalueTypes[d] = Luau::cloneIncremental(pair->second.typeId, *destArena, cloneState, destScope);
destScope->bindings[pair->first] = Luau::cloneIncremental(pair->second, *destArena, cloneState, destScope);
}
}
// Second - any referenced type alias bindings need to be placed in scope so type annotation can be resolved.
// If the actual type alias appears in the fragment on the lhs as a definition (in declaredAliases), it will be processed during typechecking
// anyway
for (const auto& x : f.referencedBindings)
{
if (f.declaredAliases.contains(x))
continue;
if (std::optional<TypeFun> tf = staleScope->lookupType(x))
{
destScope->privateTypeBindings[x] = Luau::cloneIncremental(*tf, *destArena, cloneState, destScope);
}
}
// Third - any referenced imported type bindings need to be imported in
for (const auto& [mod, name] : f.referencedImportedBindings)
{
if (std::optional<TypeFun> tf = staleScope->lookupImportedType(mod, name))
{
destScope->importedTypeBindings[mod].insert_or_assign(name, Luau::cloneIncremental(*tf, *destArena, cloneState, destScope));
}
}
// Finally - clone the returnType on the staleScope. This helps avoid potential leaks of free types.
if (staleScope->returnType)
destScope->returnType = Luau::cloneIncremental(staleScope->returnType, *destArena, cloneState, destScope);
}
struct MixedModeIncrementalTCDefFinder : public AstVisitor
{
bool visit(AstExprLocal* local) override
{
referencedLocalDefs.emplace_back(local->local, local);
@ -80,14 +237,22 @@ struct MixedModeIncrementalTCDefFinder : public AstVisitor
return FFlag::LuauMixedModeDefFinderTraversesTypeOf;
}
bool visit(AstStatTypeAlias* alias) override
{
if (FFlag::LuauCloneTypeAliasBindings)
declaredAliases.insert(std::string(alias->name.value));
return true;
}
// ast defs is just a mapping from expr -> def in general
// will get built up by the dfg builder
// localDefs, we need to copy over
std::vector<std::pair<AstLocal*, AstExpr*>> referencedLocalDefs;
DenseHashSet<Name> declaredAliases{""};
};
void cloneAndSquashScopes(
void cloneAndSquashScopes_DEPRECATED(
CloneState& cloneState,
const Scope* staleScope,
const ModulePtr& staleModule,
@ -144,6 +309,87 @@ 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);
}
MixedModeIncrementalTCDefFinder finder;
if (FFlag::LuauCloneTypeAliasBindings)
program->visit(&finder);
// in reverse order (we need to clone the parents and override defs as we go down the list)
for (auto it = scopes.rbegin(); it != scopes.rend(); ++it)
{
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);
if (FFlag::LuauCloneTypeAliasBindings)
{
for (const auto& [n, tf] : curr->exportedTypeBindings)
{
if (!finder.declaredAliases.contains(n))
destScope->exportedTypeBindings[n] = Luau::cloneIncremental(tf, *destArena, cloneState, destScope);
}
for (const auto& [n, tf] : curr->privateTypeBindings)
{
if (!finder.declaredAliases.contains(n))
destScope->privateTypeBindings[n] = Luau::cloneIncremental(tf, *destArena, cloneState, destScope);
}
}
for (const auto& [n, m] : curr->importedTypeBindings)
{
std::unordered_map<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);
}
}
if (!FFlag::LuauCloneTypeAliasBindings)
program->visit(&finder);
// The above code associates defs with TypeId's in the scope
// so that lookup to locals will succeed.
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);
}
}
if (FFlag::LuauCloneReturnTypePack && destScope->returnType)
destScope->returnType = Luau::cloneIncremental(destScope->returnType, *destArena, cloneState, destScope);
return;
}
static FrontendModuleResolver& getModuleResolver(Frontend& frontend, std::optional<FrontendOptions> options)
{
if (FFlag::LuauSolverV2 || !options)
@ -152,6 +398,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 +424,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;
@ -203,14 +472,36 @@ FragmentAutocompleteAncestryResult findAncestryForFragmentParse(AstStatBlock* ro
}
}
}
else if (auto typeFun = stat->as<AstStatTypeFunction>(); typeFun && FFlag::LuauUserTypeFunTypecheck)
{
if (typeFun->location.contains(cursorPos))
{
for (AstLocal* loc : typeFun->body->args)
{
localStack.push_back(loc);
localMap[loc->name] = loc;
}
}
}
}
}
}
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 +587,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 +606,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 +622,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 +632,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 +653,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 +663,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);
@ -436,23 +752,49 @@ void mixedModeCompatibility(
}
}
FragmentTypeCheckResult typecheckFragment_(
static void reportWaypoint(IFragmentAutocompleteReporter* reporter, FragmentAutocompleteWaypoint type)
{
if (!FFlag::LuauFragmentAcSupportsReporter || !reporter)
return;
reporter->reportWaypoint(type);
}
static void reportFragmentString(IFragmentAutocompleteReporter* reporter, std::string_view fragment)
{
if (!FFlag::LuauFragmentAcSupportsReporter || !reporter)
return;
reporter->reportFragmentString(fragment);
}
FragmentTypeCheckResult typecheckFragmentHelper_DEPRECATED(
Frontend& frontend,
AstStatBlock* root,
const ModulePtr& stale,
const ScopePtr& closestScope,
const Position& cursorPos,
std::unique_ptr<Allocator> astAllocator,
const FrontendOptions& opts
const FrontendOptions& opts,
IFragmentAutocompleteReporter* reporter
)
{
LUAU_TIMETRACE_SCOPE("Luau::typecheckFragment_", "FragmentAutocomplete");
freeze(stale->internalTypes);
freeze(stale->interfaceTypes);
reportWaypoint(reporter, FragmentAutocompleteWaypoint::CloneModuleStart);
CloneState cloneState{frontend.builtinTypes};
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));
reportWaypoint(reporter, FragmentAutocompleteWaypoint::CloneModuleEnd);
incrementalModule->checkedInNewSolver = true;
unfreeze(incrementalModule->internalTypes);
unfreeze(incrementalModule->interfaceTypes);
@ -480,6 +822,7 @@ FragmentTypeCheckResult typecheckFragment_(
/// Create a DataFlowGraph just for the surrounding context
DataFlowGraph dfg = DataFlowGraphBuilder::build(root, NotNull{&incrementalModule->defArena}, NotNull{&incrementalModule->keyArena}, iceHandler);
reportWaypoint(reporter, FragmentAutocompleteWaypoint::DfgBuildEnd);
SimplifierPtr simplifier = newSimplifier(NotNull{&incrementalModule->internalTypes}, frontend.builtinTypes);
@ -495,32 +838,45 @@ FragmentTypeCheckResult typecheckFragment_(
frontend.builtinTypes,
iceHandler,
stale->getModuleScope(),
frontend.globals.globalTypeFunctionScope,
nullptr,
nullptr,
NotNull{&dfg},
{}
};
std::shared_ptr<Scope> freshChildOfNearestScope = nullptr;
reportWaypoint(reporter, FragmentAutocompleteWaypoint::CloneAndSquashScopeStart);
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()
);
reportWaypoint(reporter, FragmentAutocompleteWaypoint::CloneAndSquashScopeEnd);
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}
// We need to trim nearestChild from the scope hierarcy
// We need to trim nearestChild from the scope hierarchy
closestScope->children.emplace_back(freshChildOfNearestScope.get());
cg.visitFragmentRoot(freshChildOfNearestScope, root);
// Trim nearestChild from the closestScope
@ -528,6 +884,16 @@ FragmentTypeCheckResult typecheckFragment_(
LUAU_ASSERT(back == freshChildOfNearestScope.get());
closestScope->children.pop_back();
}
reportWaypoint(reporter, FragmentAutocompleteWaypoint::ConstraintSolverStart);
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{
@ -558,6 +924,8 @@ FragmentTypeCheckResult typecheckFragment_(
stale->cancelled = true;
}
reportWaypoint(reporter, FragmentAutocompleteWaypoint::ConstraintSolverEnd);
// In frontend we would forbid internal types
// because this is just for autocomplete, we don't actually care
// We also don't even need to typecheck - just synthesize types as best as we can
@ -567,6 +935,149 @@ FragmentTypeCheckResult typecheckFragment_(
return {std::move(incrementalModule), std::move(freshChildOfNearestScope)};
}
FragmentTypeCheckResult typecheckFragment_(
Frontend& frontend,
AstStatBlock* root,
const ModulePtr& stale,
const ScopePtr& closestScope,
const Position& cursorPos,
std::unique_ptr<Allocator> astAllocator,
const FrontendOptions& opts,
IFragmentAutocompleteReporter* reporter
)
{
LUAU_TIMETRACE_SCOPE("Luau::typecheckFragment_", "FragmentAutocomplete");
freeze(stale->internalTypes);
freeze(stale->interfaceTypes);
ModulePtr incrementalModule = std::make_shared<Module>();
incrementalModule->name = stale->name;
incrementalModule->humanReadableName = "Incremental$" + stale->humanReadableName;
incrementalModule->internalTypes.owningModule = incrementalModule.get();
incrementalModule->interfaceTypes.owningModule = incrementalModule.get();
incrementalModule->allocator = std::move(astAllocator);
incrementalModule->checkedInNewSolver = true;
unfreeze(incrementalModule->internalTypes);
unfreeze(incrementalModule->interfaceTypes);
/// Setup typecheck limits
TypeCheckLimits limits;
if (opts.moduleTimeLimitSec)
limits.finishTime = TimeTrace::getClock() + *opts.moduleTimeLimitSec;
else
limits.finishTime = std::nullopt;
limits.cancellationToken = opts.cancellationToken;
/// Icehandler
NotNull<InternalErrorReporter> iceHandler{&frontend.iceHandler};
/// Make the shared state for the unifier (recursion + iteration limits)
UnifierSharedState unifierState{iceHandler};
unifierState.counters.recursionLimit = FInt::LuauTypeInferRecursionLimit;
unifierState.counters.iterationLimit = limits.unifierIterationLimit.value_or(FInt::LuauTypeInferIterationLimit);
/// Initialize the normalizer
Normalizer normalizer{&incrementalModule->internalTypes, frontend.builtinTypes, NotNull{&unifierState}};
/// User defined type functions runtime
TypeFunctionRuntime typeFunctionRuntime(iceHandler, NotNull{&limits});
if (FFlag::LuauFragmentNoTypeFunEval || FFlag::LuauUserTypeFunTypecheck)
typeFunctionRuntime.allowEvaluation = false;
/// Create a DataFlowGraph just for the surrounding context
DataFlowGraph dfg = DataFlowGraphBuilder::build(root, NotNull{&incrementalModule->defArena}, NotNull{&incrementalModule->keyArena}, iceHandler);
reportWaypoint(reporter, FragmentAutocompleteWaypoint::DfgBuildEnd);
SimplifierPtr simplifier = newSimplifier(NotNull{&incrementalModule->internalTypes}, frontend.builtinTypes);
FrontendModuleResolver& resolver = getModuleResolver(frontend, opts);
/// Contraint Generator
ConstraintGenerator cg{
incrementalModule,
NotNull{&normalizer},
NotNull{simplifier.get()},
NotNull{&typeFunctionRuntime},
NotNull{&resolver},
frontend.builtinTypes,
iceHandler,
stale->getModuleScope(),
frontend.globals.globalTypeFunctionScope,
nullptr,
nullptr,
NotNull{&dfg},
{}
};
CloneState cloneState{frontend.builtinTypes};
std::shared_ptr<Scope> freshChildOfNearestScope = std::make_shared<Scope>(nullptr);
incrementalModule->scopes.emplace_back(root->location, freshChildOfNearestScope);
freshChildOfNearestScope->interiorFreeTypes.emplace();
cg.rootScope = freshChildOfNearestScope.get();
if (FFlag::LuauUserTypeFunTypecheck)
{
// Create module-local scope for the type function environment
ScopePtr localTypeFunctionScope = std::make_shared<Scope>(cg.typeFunctionScope);
localTypeFunctionScope->location = root->location;
cg.typeFunctionRuntime->rootScope = localTypeFunctionScope;
}
reportWaypoint(reporter, FragmentAutocompleteWaypoint::CloneAndSquashScopeStart);
cloneTypesFromFragment(
cloneState, closestScope.get(), stale, NotNull{&incrementalModule->internalTypes}, NotNull{&dfg}, root, freshChildOfNearestScope.get()
);
reportWaypoint(reporter, FragmentAutocompleteWaypoint::CloneAndSquashScopeEnd);
cg.visitFragmentRoot(freshChildOfNearestScope, root);
for (auto p : cg.scopes)
incrementalModule->scopes.emplace_back(std::move(p));
reportWaypoint(reporter, FragmentAutocompleteWaypoint::ConstraintSolverStart);
/// Initialize the constraint solver and run it
ConstraintSolver cs{
NotNull{&normalizer},
NotNull{simplifier.get()},
NotNull{&typeFunctionRuntime},
NotNull(cg.rootScope),
borrowConstraints(cg.constraints),
NotNull{&cg.scopeToFunction},
incrementalModule->name,
NotNull{&resolver},
{},
nullptr,
NotNull{&dfg},
limits
};
try
{
cs.run();
}
catch (const TimeLimitError&)
{
stale->timeout = true;
}
catch (const UserCancelError&)
{
stale->cancelled = true;
}
reportWaypoint(reporter, FragmentAutocompleteWaypoint::ConstraintSolverEnd);
// In frontend we would forbid internal types
// because this is just for autocomplete, we don't actually care
// We also don't even need to typecheck - just synthesize types as best as we can
freeze(incrementalModule->internalTypes);
freeze(incrementalModule->interfaceTypes);
freshChildOfNearestScope->parent = closestScope;
return {std::move(incrementalModule), std::move(freshChildOfNearestScope)};
}
std::pair<FragmentTypeCheckStatus, FragmentTypeCheckResult> typecheckFragment(
Frontend& frontend,
@ -574,24 +1085,15 @@ std::pair<FragmentTypeCheckStatus, FragmentTypeCheckResult> typecheckFragment(
const Position& cursorPos,
std::optional<FrontendOptions> opts,
std::string_view src,
std::optional<Position> fragmentEndPosition
std::optional<Position> fragmentEndPosition,
IFragmentAutocompleteReporter* reporter
)
{
LUAU_TIMETRACE_SCOPE("Luau::typecheckFragment", "FragmentAutocomplete");
LUAU_TIMETRACE_ARGUMENT("name", moduleName.c_str());
if (FFlag::LuauBetterReverseDependencyTracking)
{
if (!frontend.allModuleDependenciesValid(moduleName, opts && opts->forAutocomplete))
return {FragmentTypeCheckStatus::SkipAutocomplete, {}};
}
const SourceModule* sourceModule = frontend.getSourceModule(moduleName);
if (!sourceModule)
{
LUAU_ASSERT(!"Expected Source Module for fragment typecheck");
return {};
}
if (!frontend.allModuleDependenciesValid(moduleName, opts && opts->forAutocomplete))
return {FragmentTypeCheckStatus::SkipAutocomplete, {}};
FrontendModuleResolver& resolver = getModuleResolver(frontend, opts);
ModulePtr module = resolver.getModule(moduleName);
@ -601,15 +1103,31 @@ 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);
reportWaypoint(reporter, FragmentAutocompleteWaypoint::ParseFragmentEnd);
}
if (!tryParse)
return {FragmentTypeCheckStatus::SkipAutocomplete, {}};
@ -622,8 +1140,13 @@ std::pair<FragmentTypeCheckStatus, FragmentTypeCheckResult> typecheckFragment(
FrontendOptions frontendOptions = opts.value_or(frontend.options);
const ScopePtr& closestScope = findClosestScope(module, parseResult.nearestStatement);
FragmentTypeCheckResult result =
typecheckFragment_(frontend, parseResult.root, module, closestScope, cursorPos, std::move(parseResult.alloc), frontendOptions);
FFlag::LuauIncrementalAutocompleteDemandBasedCloning
? typecheckFragment_(frontend, parseResult.root, module, closestScope, cursorPos, std::move(parseResult.alloc), frontendOptions, reporter)
: typecheckFragmentHelper_DEPRECATED(
frontend, parseResult.root, module, closestScope, cursorPos, std::move(parseResult.alloc), frontendOptions, reporter
);
result.ancestry = std::move(parseResult.ancestry);
reportFragmentString(reporter, tryParse->fragmentToParse);
return {FragmentTypeCheckStatus::Success, result};
}
@ -635,6 +1158,11 @@ 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
{
@ -645,7 +1173,8 @@ FragmentAutocompleteStatusResult tryFragmentAutocomplete(
cursorPosition,
context.opts,
std::move(stringCompletionCB),
context.DEPRECATED_fragmentEndPosition
context.DEPRECATED_fragmentEndPosition,
FFlag::LuauFragmentAcSupportsReporter ? context.reporter : nullptr
);
return {FragmentAutocompleteStatus::Success, std::move(fragmentAutocomplete)};
}
@ -664,28 +1193,33 @@ FragmentAutocompleteResult fragmentAutocomplete(
Position cursorPosition,
std::optional<FrontendOptions> opts,
StringCompletionCallback callback,
std::optional<Position> fragmentEndPosition
std::optional<Position> fragmentEndPosition,
IFragmentAutocompleteReporter* reporter
)
{
LUAU_ASSERT(FFlag::LuauAutocompleteRefactorsForIncrementalAutocomplete);
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);
auto [tcStatus, tcResult] = typecheckFragment(frontend, moduleName, cursorPosition, opts, src, fragmentEndPosition, reporter);
if (tcStatus == FragmentTypeCheckStatus::SkipAutocomplete)
return {};
reportWaypoint(reporter, FragmentAutocompleteWaypoint::TypecheckFragmentEnd);
auto globalScope = (opts && opts->forAutocomplete) ? frontend.globalsForAutocomplete.globalScope.get() : frontend.globals.globalScope.get();
if (FFlag::LogFragmentsFromAutocomplete)
logLuau(src);
@ -702,6 +1236,7 @@ FragmentAutocompleteResult fragmentAutocomplete(
callback
);
reportWaypoint(reporter, FragmentAutocompleteWaypoint::AutocompleteEnd);
return {std::move(tcResult.incrementalModule), tcResult.freshScope.get(), std::move(arenaForFragmentAutocomplete), std::move(result)};
}

View file

@ -47,10 +47,11 @@ LUAU_FASTFLAGVARIABLE(DebugLuauForceStrictMode)
LUAU_FASTFLAGVARIABLE(DebugLuauForceNonStrictMode)
LUAU_DYNAMIC_FASTFLAGVARIABLE(LuauRunCustomModuleChecks, false)
LUAU_FASTFLAGVARIABLE(LuauBetterReverseDependencyTracking)
LUAU_FASTFLAGVARIABLE(LuauModuleHoldsAstRoot)
LUAU_FASTFLAGVARIABLE(LuauFixMultithreadTypecheck)
LUAU_FASTFLAG(StudioReportLuauAny2)
LUAU_FASTFLAGVARIABLE(LuauStoreSolverTypeOnModule)
LUAU_FASTFLAGVARIABLE(LuauSelectivelyRetainDFGArena)
@ -82,6 +83,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 +496,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;
@ -822,14 +1034,11 @@ bool Frontend::parseGraph(
buildQueue.push_back(top->name);
if (FFlag::LuauBetterReverseDependencyTracking)
// at this point we know all valid dependencies are processed into SourceNodes
for (const ModuleName& dep : top->requireSet)
{
// at this point we know all valid dependencies are processed into SourceNodes
for (const ModuleName& dep : top->requireSet)
{
if (auto it = sourceNodes.find(dep); it != sourceNodes.end())
it->second->dependents.insert(top->name);
}
if (auto it = sourceNodes.find(dep); it != sourceNodes.end())
it->second->dependents.insert(top->name);
}
}
else
@ -1118,51 +1327,35 @@ void Frontend::recordItemResult(const BuildQueueItem& item)
if (item.exception)
std::rethrow_exception(item.exception);
if (FFlag::LuauBetterReverseDependencyTracking)
bool replacedModule = false;
if (item.options.forAutocomplete)
{
bool replacedModule = false;
if (item.options.forAutocomplete)
{
replacedModule = moduleResolverForAutocomplete.setModule(item.name, item.module);
item.sourceNode->dirtyModuleForAutocomplete = false;
}
else
{
replacedModule = moduleResolver.setModule(item.name, item.module);
item.sourceNode->dirtyModule = false;
}
if (replacedModule)
{
LUAU_TIMETRACE_SCOPE("Frontend::invalidateDependentModules", "Frontend");
LUAU_TIMETRACE_ARGUMENT("name", item.name.c_str());
traverseDependents(
item.name,
[forAutocomplete = item.options.forAutocomplete](SourceNode& sourceNode)
{
bool traverseSubtree = !sourceNode.hasInvalidModuleDependency(forAutocomplete);
sourceNode.setInvalidModuleDependency(true, forAutocomplete);
return traverseSubtree;
}
);
}
item.sourceNode->setInvalidModuleDependency(false, item.options.forAutocomplete);
replacedModule = moduleResolverForAutocomplete.setModule(item.name, item.module);
item.sourceNode->dirtyModuleForAutocomplete = false;
}
else
{
if (item.options.forAutocomplete)
{
moduleResolverForAutocomplete.setModule(item.name, item.module);
item.sourceNode->dirtyModuleForAutocomplete = false;
}
else
{
moduleResolver.setModule(item.name, item.module);
item.sourceNode->dirtyModule = false;
}
replacedModule = moduleResolver.setModule(item.name, item.module);
item.sourceNode->dirtyModule = false;
}
if (replacedModule)
{
LUAU_TIMETRACE_SCOPE("Frontend::invalidateDependentModules", "Frontend");
LUAU_TIMETRACE_ARGUMENT("name", item.name.c_str());
traverseDependents(
item.name,
[forAutocomplete = item.options.forAutocomplete](SourceNode& sourceNode)
{
bool traverseSubtree = !sourceNode.hasInvalidModuleDependency(forAutocomplete);
sourceNode.setInvalidModuleDependency(true, forAutocomplete);
return traverseSubtree;
}
);
}
item.sourceNode->setInvalidModuleDependency(false, item.options.forAutocomplete);
stats.timeCheck += item.stats.timeCheck;
stats.timeLint += item.stats.timeLint;
@ -1170,6 +1363,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;
@ -1199,7 +1444,6 @@ ScopePtr Frontend::getModuleEnvironment(const SourceModule& module, const Config
bool Frontend::allModuleDependenciesValid(const ModuleName& name, bool forAutocomplete) const
{
LUAU_ASSERT(FFlag::LuauBetterReverseDependencyTracking);
auto it = sourceNodes.find(name);
return it != sourceNodes.end() && !it->second->hasInvalidModuleDependency(forAutocomplete);
}
@ -1221,72 +1465,27 @@ void Frontend::markDirty(const ModuleName& name, std::vector<ModuleName>* marked
LUAU_TIMETRACE_SCOPE("Frontend::markDirty", "Frontend");
LUAU_TIMETRACE_ARGUMENT("name", name.c_str());
if (FFlag::LuauBetterReverseDependencyTracking)
{
traverseDependents(
name,
[markedDirty](SourceNode& sourceNode)
{
if (markedDirty)
markedDirty->push_back(sourceNode.name);
if (sourceNode.dirtySourceModule && sourceNode.dirtyModule && sourceNode.dirtyModuleForAutocomplete)
return false;
sourceNode.dirtySourceModule = true;
sourceNode.dirtyModule = true;
sourceNode.dirtyModuleForAutocomplete = true;
return true;
}
);
}
else
{
if (sourceNodes.count(name) == 0)
return;
std::unordered_map<ModuleName, std::vector<ModuleName>> reverseDeps;
for (const auto& module : sourceNodes)
traverseDependents(
name,
[markedDirty](SourceNode& sourceNode)
{
for (const auto& dep : module.second->requireSet)
reverseDeps[dep].push_back(module.first);
}
std::vector<ModuleName> queue{name};
while (!queue.empty())
{
ModuleName next = std::move(queue.back());
queue.pop_back();
LUAU_ASSERT(sourceNodes.count(next) > 0);
SourceNode& sourceNode = *sourceNodes[next];
if (markedDirty)
markedDirty->push_back(next);
markedDirty->push_back(sourceNode.name);
if (sourceNode.dirtySourceModule && sourceNode.dirtyModule && sourceNode.dirtyModuleForAutocomplete)
continue;
return false;
sourceNode.dirtySourceModule = true;
sourceNode.dirtyModule = true;
sourceNode.dirtyModuleForAutocomplete = true;
if (0 == reverseDeps.count(next))
continue;
sourceModules.erase(next);
const std::vector<ModuleName>& dependents = reverseDeps[next];
queue.insert(queue.end(), dependents.begin(), dependents.end());
return true;
}
}
);
}
void Frontend::traverseDependents(const ModuleName& name, std::function<bool(SourceNode&)> processSubtree)
{
LUAU_ASSERT(FFlag::LuauBetterReverseDependencyTracking);
LUAU_TIMETRACE_SCOPE("Frontend::traverseDependents", "Frontend");
if (sourceNodes.count(name) == 0)
@ -1333,6 +1532,7 @@ ModulePtr check(
NotNull<ModuleResolver> moduleResolver,
NotNull<FileResolver> fileResolver,
const ScopePtr& parentScope,
const ScopePtr& typeFunctionScope,
std::function<void(const ModuleName&, const ScopePtr&)> prepareModuleScope,
FrontendOptions options,
TypeCheckLimits limits,
@ -1349,6 +1549,7 @@ ModulePtr check(
moduleResolver,
fileResolver,
parentScope,
typeFunctionScope,
std::move(prepareModuleScope),
options,
limits,
@ -1410,6 +1611,7 @@ ModulePtr check(
NotNull<ModuleResolver> moduleResolver,
NotNull<FileResolver> fileResolver,
const ScopePtr& parentScope,
const ScopePtr& typeFunctionScope,
std::function<void(const ModuleName&, const ScopePtr&)> prepareModuleScope,
FrontendOptions options,
TypeCheckLimits limits,
@ -1422,8 +1624,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 +1632,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;
@ -1466,6 +1669,7 @@ ModulePtr check(
builtinTypes,
iceHandler,
parentScope,
typeFunctionScope,
std::move(prepareModuleScope),
logger.get(),
NotNull{&dfg},
@ -1648,6 +1852,7 @@ ModulePtr Frontend::check(
NotNull{forAutocomplete ? &moduleResolverForAutocomplete : &moduleResolver},
NotNull{fileResolver},
environmentScope ? *environmentScope : globals.globalScope,
globals.globalTypeFunctionScope,
prepareModuleScopeWrap,
options,
typeCheckLimits,
@ -1746,14 +1951,11 @@ std::pair<SourceNode*, SourceModule*> Frontend::getSourceNode(const ModuleName&
sourceNode->name = sourceModule->name;
sourceNode->humanReadableName = sourceModule->humanReadableName;
if (FFlag::LuauBetterReverseDependencyTracking)
// clear all prior dependents. we will re-add them after parsing the rest of the graph
for (const auto& [moduleName, _] : sourceNode->requireLocations)
{
// clear all prior dependents. we will re-add them after parsing the rest of the graph
for (const auto& [moduleName, _] : sourceNode->requireLocations)
{
if (auto depIt = sourceNodes.find(moduleName); depIt != sourceNodes.end())
depIt->second->dependents.erase(sourceNode->name);
}
if (auto depIt = sourceNodes.find(moduleName); depIt != sourceNodes.end())
depIt->second->dependents.erase(sourceNode->name);
}
sourceNode->requireSet.clear();
@ -1881,17 +2083,9 @@ bool FrontendModuleResolver::setModule(const ModuleName& moduleName, ModulePtr m
{
std::scoped_lock lock(moduleMutex);
if (FFlag::LuauBetterReverseDependencyTracking)
{
bool replaced = modules.count(moduleName) > 0;
modules[moduleName] = std::move(module);
return replaced;
}
else
{
modules[moduleName] = std::move(module);
return false;
}
bool replaced = modules.count(moduleName) > 0;
modules[moduleName] = std::move(module);
return replaced;
}
void FrontendModuleResolver::clearModules()

View file

@ -359,7 +359,7 @@ struct FreeTypeSearcher : TypeVisitor
DenseHashSet<const void*> seenPositive{nullptr};
DenseHashSet<const void*> seenNegative{nullptr};
bool seenWithPolarity(const void* ty)
bool seenWithCurrentPolarity(const void* ty)
{
switch (polarity)
{
@ -401,7 +401,7 @@ struct FreeTypeSearcher : TypeVisitor
bool visit(TypeId ty) override
{
if (cachedTypes->contains(ty) || seenWithPolarity(ty))
if (cachedTypes->contains(ty) || seenWithCurrentPolarity(ty))
return false;
LUAU_ASSERT(ty);
@ -410,7 +410,7 @@ struct FreeTypeSearcher : TypeVisitor
bool visit(TypeId ty, const FreeType& ft) override
{
if (cachedTypes->contains(ty) || seenWithPolarity(ty))
if (cachedTypes->contains(ty) || seenWithCurrentPolarity(ty))
return false;
if (!subsumes(scope, ft.scope))
@ -435,7 +435,7 @@ struct FreeTypeSearcher : TypeVisitor
bool visit(TypeId ty, const TableType& tt) override
{
if (cachedTypes->contains(ty) || seenWithPolarity(ty))
if (cachedTypes->contains(ty) || seenWithCurrentPolarity(ty))
return false;
if ((tt.state == TableState::Free || tt.state == TableState::Unsealed) && subsumes(scope, tt.scope))
@ -481,7 +481,7 @@ struct FreeTypeSearcher : TypeVisitor
bool visit(TypeId ty, const FunctionType& ft) override
{
if (cachedTypes->contains(ty) || seenWithPolarity(ty))
if (cachedTypes->contains(ty) || seenWithCurrentPolarity(ty))
return false;
flip();
@ -500,7 +500,7 @@ struct FreeTypeSearcher : TypeVisitor
bool visit(TypePackId tp, const FreeTypePack& ftp) override
{
if (seenWithPolarity(tp))
if (seenWithCurrentPolarity(tp))
return false;
if (!subsumes(scope, ftp.scope))
@ -547,7 +547,7 @@ struct TypeCacher : TypeOnceVisitor
{
}
void cache(TypeId ty)
void cache(TypeId ty) const
{
cachedTypes->insert(ty);
}

View file

@ -9,6 +9,7 @@ GlobalTypes::GlobalTypes(NotNull<BuiltinTypes> builtinTypes)
: builtinTypes(builtinTypes)
{
globalScope = std::make_shared<Scope>(globalTypes.addTypePack(TypePackVar{FreeTypePack{TypeLevel{}}}));
globalTypeFunctionScope = std::make_shared<Scope>(globalTypes.addTypePack(TypePackVar{FreeTypePack{TypeLevel{}}}));
globalScope->addBuiltinTypeBinding("any", TypeFun{{}, builtinTypes->anyType});
globalScope->addBuiltinTypeBinding("nil", TypeFun{{}, builtinTypes->nilType});

View file

@ -22,6 +22,7 @@
LUAU_FASTFLAG(LuauFreeTypesMustHaveBounds)
LUAU_FASTFLAGVARIABLE(LuauNonStrictVisitorImprovements)
LUAU_FASTFLAGVARIABLE(LuauNewNonStrictWarnOnUnknownGlobals)
LUAU_FASTFLAGVARIABLE(LuauNonStrictFuncDefErrorFix)
namespace Luau
{
@ -763,7 +764,17 @@ struct NonStrictTypeChecker
for (AstLocal* local : exprFn->args)
{
if (std::optional<TypeId> ty = willRunTimeErrorFunctionDefinition(local, remainder))
reportError(NonStrictFunctionDefinitionError{exprFn->debugname.value, local->name.value, *ty}, local->location);
{
if (FFlag::LuauNonStrictFuncDefErrorFix)
{
const char* debugname = exprFn->debugname.value;
reportError(NonStrictFunctionDefinitionError{debugname ? debugname : "", local->name.value, *ty}, local->location);
}
else
{
reportError(NonStrictFunctionDefinitionError{exprFn->debugname.value, local->name.value, *ty}, local->location);
}
}
remainder.remove(dfg->getDef(local));
}
return remainder;

View file

@ -20,8 +20,9 @@ LUAU_FASTFLAGVARIABLE(DebugLuauCheckNormalizeInvariant)
LUAU_FASTINTVARIABLE(LuauNormalizeCacheLimit, 100000)
LUAU_FASTINTVARIABLE(LuauNormalizeIntersectionLimit, 200)
LUAU_FASTFLAG(LuauSolverV2)
LUAU_FASTFLAGVARIABLE(LuauNormalizeNegationFix)
LUAU_FASTFLAGVARIABLE(LuauFixInfiniteRecursionInNormalization)
LUAU_FASTFLAGVARIABLE(LuauFixNormalizedIntersectionOfNegatedClass)
LUAU_FASTFLAGVARIABLE(LuauNormalizedBufferIsNotUnknown)
namespace Luau
{
@ -303,7 +304,9 @@ bool NormalizedType::isUnknown() const
// Otherwise, we can still be unknown!
bool hasAllPrimitives = isPrim(booleans, PrimitiveType::Boolean) && isPrim(nils, PrimitiveType::NilType) && isNumber(numbers) &&
strings.isString() && isPrim(threads, PrimitiveType::Thread) && isThread(threads);
strings.isString() &&
(FFlag::LuauNormalizedBufferIsNotUnknown ? isThread(threads) && isBuffer(buffers)
: isPrim(threads, PrimitiveType::Thread) && isThread(threads));
// Check is class
bool isTopClass = false;
@ -2288,7 +2291,7 @@ void Normalizer::intersectClassesWithClass(NormalizedClassType& heres, TypeId th
for (auto nIt = negations.begin(); nIt != negations.end();)
{
if (FFlag::LuauFixNormalizedIntersectionOfNegatedClass && isSubclass(there, *nIt))
if (isSubclass(there, *nIt))
{
// Hitting this block means that the incoming class is a
// subclass of this type, _and_ one of its negations is a
@ -3305,7 +3308,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

@ -107,134 +107,4 @@ void quantify(TypeId ty, TypeLevel level)
ftv->genericPacks.insert(ftv->genericPacks.end(), q.genericPacks.begin(), q.genericPacks.end());
}
struct PureQuantifier : Substitution
{
Scope* scope;
OrderedMap<TypeId, TypeId> insertedGenerics;
OrderedMap<TypePackId, TypePackId> insertedGenericPacks;
bool seenMutableType = false;
bool seenGenericType = false;
PureQuantifier(TypeArena* arena, Scope* scope)
: Substitution(TxnLog::empty(), arena)
, scope(scope)
{
}
bool isDirty(TypeId ty) override
{
LUAU_ASSERT(ty == follow(ty));
if (auto ftv = get<FreeType>(ty))
{
bool result = subsumes(scope, ftv->scope);
seenMutableType |= result;
return result;
}
else if (auto ttv = get<TableType>(ty))
{
if (ttv->state == TableState::Free)
seenMutableType = true;
else if (ttv->state == TableState::Generic)
seenGenericType = true;
return (ttv->state == TableState::Unsealed || ttv->state == TableState::Free) && subsumes(scope, ttv->scope);
}
return false;
}
bool isDirty(TypePackId tp) override
{
if (auto ftp = get<FreeTypePack>(tp))
{
return subsumes(scope, ftp->scope);
}
return false;
}
TypeId clean(TypeId ty) override
{
if (auto ftv = get<FreeType>(ty))
{
TypeId result = arena->addType(GenericType{scope});
insertedGenerics.push(ty, result);
return result;
}
else if (auto ttv = get<TableType>(ty))
{
TypeId result = arena->addType(TableType{});
TableType* resultTable = getMutable<TableType>(result);
LUAU_ASSERT(resultTable);
*resultTable = *ttv;
resultTable->level = TypeLevel{};
resultTable->scope = scope;
if (ttv->state == TableState::Free)
{
resultTable->state = TableState::Generic;
insertedGenerics.push(ty, result);
}
else if (ttv->state == TableState::Unsealed)
resultTable->state = TableState::Sealed;
return result;
}
return ty;
}
TypePackId clean(TypePackId tp) override
{
if (auto ftp = get<FreeTypePack>(tp))
{
TypePackId result = arena->addTypePack(TypePackVar{GenericTypePack{scope}});
insertedGenericPacks.push(tp, result);
return result;
}
return tp;
}
bool ignoreChildren(TypeId ty) override
{
if (get<ClassType>(ty))
return true;
return ty->persistent;
}
bool ignoreChildren(TypePackId ty) override
{
return ty->persistent;
}
};
std::optional<QuantifierResult> quantify(TypeArena* arena, TypeId ty, Scope* scope)
{
PureQuantifier quantifier{arena, scope};
std::optional<TypeId> result = quantifier.substitute(ty);
if (!result)
return std::nullopt;
FunctionType* ftv = getMutable<FunctionType>(*result);
LUAU_ASSERT(ftv);
ftv->scope = scope;
for (auto k : quantifier.insertedGenerics.keys)
{
TypeId g = quantifier.insertedGenerics.pairings[k];
if (get<GenericType>(g))
ftv->generics.push_back(g);
}
for (auto k : quantifier.insertedGenericPacks.keys)
ftv->genericPacks.push_back(quantifier.insertedGenericPacks.pairings[k]);
ftv->hasNoFreeOrGenericTypes = ftv->generics.empty() && ftv->genericPacks.empty() && !quantifier.seenGenericType && !quantifier.seenMutableType;
return std::optional<QuantifierResult>({*result, std::move(quantifier.insertedGenerics), std::move(quantifier.insertedGenericPacks)});
}
} // namespace Luau

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,8 +4,6 @@
#include "Luau/Ast.h"
#include "Luau/Module.h"
LUAU_FASTFLAGVARIABLE(LuauExtendedSimpleRequire)
namespace Luau
{
@ -106,96 +104,50 @@ struct RequireTracer : AstVisitor
{
ModuleInfo moduleContext{currentModuleName};
if (FFlag::LuauExtendedSimpleRequire)
// seed worklist with require arguments
work.reserve(requireCalls.size());
for (AstExprCall* require : requireCalls)
work.push_back(require->args.data[0]);
// push all dependent expressions to the work stack; note that the vector is modified during traversal
for (size_t i = 0; i < work.size(); ++i)
{
// seed worklist with require arguments
work.reserve(requireCalls.size());
for (AstExprCall* require : requireCalls)
work.push_back(require->args.data[0]);
// push all dependent expressions to the work stack; note that the vector is modified during traversal
for (size_t i = 0; i < work.size(); ++i)
{
if (AstNode* dep = getDependent(work[i]))
work.push_back(dep);
}
// resolve all expressions to a module info
for (size_t i = work.size(); i > 0; --i)
{
AstNode* expr = work[i - 1];
// when multiple expressions depend on the same one we push it to work queue multiple times
if (result.exprs.contains(expr))
continue;
std::optional<ModuleInfo> info;
if (AstNode* dep = getDependent(expr))
{
const ModuleInfo* context = result.exprs.find(dep);
if (context && expr->is<AstExprLocal>())
info = *context; // locals just inherit their dependent context, no resolution required
else if (context && (expr->is<AstExprGroup>() || expr->is<AstTypeGroup>()))
info = *context; // simple group nodes propagate their value
else if (context && (expr->is<AstTypeTypeof>() || expr->is<AstExprTypeAssertion>()))
info = *context; // typeof type annotations will resolve to the typeof content
else if (AstExpr* asExpr = expr->asExpr())
info = fileResolver->resolveModule(context, asExpr);
}
else if (AstExpr* asExpr = expr->asExpr())
{
info = fileResolver->resolveModule(&moduleContext, asExpr);
}
if (info)
result.exprs[expr] = std::move(*info);
}
if (AstNode* dep = getDependent(work[i]))
work.push_back(dep);
}
else
// resolve all expressions to a module info
for (size_t i = work.size(); i > 0; --i)
{
// seed worklist with require arguments
work_DEPRECATED.reserve(requireCalls.size());
AstNode* expr = work[i - 1];
for (AstExprCall* require : requireCalls)
work_DEPRECATED.push_back(require->args.data[0]);
// when multiple expressions depend on the same one we push it to work queue multiple times
if (result.exprs.contains(expr))
continue;
// push all dependent expressions to the work stack; note that the vector is modified during traversal
for (size_t i = 0; i < work_DEPRECATED.size(); ++i)
if (AstExpr* dep = getDependent_DEPRECATED(work_DEPRECATED[i]))
work_DEPRECATED.push_back(dep);
std::optional<ModuleInfo> info;
// resolve all expressions to a module info
for (size_t i = work_DEPRECATED.size(); i > 0; --i)
if (AstNode* dep = getDependent(expr))
{
AstExpr* expr = work_DEPRECATED[i - 1];
const ModuleInfo* context = result.exprs.find(dep);
// when multiple expressions depend on the same one we push it to work queue multiple times
if (result.exprs.contains(expr))
continue;
std::optional<ModuleInfo> info;
if (AstExpr* dep = getDependent_DEPRECATED(expr))
{
const ModuleInfo* context = result.exprs.find(dep);
// locals just inherit their dependent context, no resolution required
if (expr->is<AstExprLocal>())
info = context ? std::optional<ModuleInfo>(*context) : std::nullopt;
else
info = fileResolver->resolveModule(context, expr);
}
else
{
info = fileResolver->resolveModule(&moduleContext, expr);
}
if (info)
result.exprs[expr] = std::move(*info);
if (context && expr->is<AstExprLocal>())
info = *context; // locals just inherit their dependent context, no resolution required
else if (context && (expr->is<AstExprGroup>() || expr->is<AstTypeGroup>()))
info = *context; // simple group nodes propagate their value
else if (context && (expr->is<AstTypeTypeof>() || expr->is<AstExprTypeAssertion>()))
info = *context; // typeof type annotations will resolve to the typeof content
else if (AstExpr* asExpr = expr->asExpr())
info = fileResolver->resolveModule(context, asExpr);
}
else if (AstExpr* asExpr = expr->asExpr())
{
info = fileResolver->resolveModule(&moduleContext, asExpr);
}
if (info)
result.exprs[expr] = std::move(*info);
}
// resolve all requires according to their argument
@ -224,7 +176,6 @@ struct RequireTracer : AstVisitor
ModuleName currentModuleName;
DenseHashMap<AstLocal*, AstExpr*> locals;
std::vector<AstExpr*> work_DEPRECATED;
std::vector<AstNode*> work;
std::vector<AstExprCall*> requireCalls;
};

View file

@ -84,6 +84,17 @@ std::optional<TypeId> Scope::lookupUnrefinedType(DefId def) const
return std::nullopt;
}
std::optional<TypeId> Scope::lookupRValueRefinementType(DefId def) const
{
for (const Scope* current = this; current; current = current->parent.get())
{
if (auto ty = current->rvalueRefinements.find(def))
return *ty;
}
return std::nullopt;
}
std::optional<TypeId> Scope::lookup(DefId def) const
{
for (const Scope* current = this; current; current = current->parent.get())
@ -181,6 +192,29 @@ std::optional<Binding> Scope::linearSearchForBinding(const std::string& name, bo
return std::nullopt;
}
std::optional<std::pair<Symbol, Binding>> Scope::linearSearchForBindingPair(const std::string& name, bool traverseScopeChain) const
{
const Scope* scope = this;
while (scope)
{
for (auto& [n, binding] : scope->bindings)
{
if (n.local && n.local->name == name.c_str())
return {{n, binding}};
else if (n.global.value && n.global == name.c_str())
return {{n, binding}};
}
scope = scope->parent.get();
if (!traverseScopeChain)
break;
}
return std::nullopt;
}
// Updates the `this` scope with the assignments from the `childScope` including ones that doesn't exist in `this`.
void Scope::inheritAssignments(const ScopePtr& childScope)
{

View file

@ -22,7 +22,6 @@
#include <algorithm>
LUAU_FASTFLAGVARIABLE(DebugLuauSubtypingCheckPathValidity)
LUAU_FASTFLAGVARIABLE(LuauSubtypingFixTailPack)
namespace Luau
{
@ -754,7 +753,8 @@ SubtypingResult Subtyping::isCovariantWith(SubtypingEnvironment& env, TypePackId
// Match head types pairwise
for (size_t i = 0; i < headSize; ++i)
results.push_back(isCovariantWith(env, subHead[i], superHead[i], scope).withBothComponent(TypePath::Index{i}));
results.push_back(isCovariantWith(env, subHead[i], superHead[i], scope).withBothComponent(TypePath::Index{i, TypePath::Index::Variant::Pack})
);
// Handle mismatched head sizes
@ -767,7 +767,7 @@ SubtypingResult Subtyping::isCovariantWith(SubtypingEnvironment& env, TypePackId
for (size_t i = headSize; i < superHead.size(); ++i)
results.push_back(isCovariantWith(env, vt->ty, superHead[i], scope)
.withSubPath(TypePath::PathBuilder().tail().variadic().build())
.withSuperComponent(TypePath::Index{i}));
.withSuperComponent(TypePath::Index{i, TypePath::Index::Variant::Pack}));
}
else if (auto gt = get<GenericTypePack>(*subTail))
{
@ -821,7 +821,7 @@ SubtypingResult Subtyping::isCovariantWith(SubtypingEnvironment& env, TypePackId
{
for (size_t i = headSize; i < subHead.size(); ++i)
results.push_back(isCovariantWith(env, subHead[i], vt->ty, scope)
.withSubComponent(TypePath::Index{i})
.withSubComponent(TypePath::Index{i, TypePath::Index::Variant::Pack})
.withSuperPath(TypePath::PathBuilder().tail().variadic().build()));
}
else if (auto gt = get<GenericTypePack>(*superTail))
@ -859,7 +859,7 @@ SubtypingResult Subtyping::isCovariantWith(SubtypingEnvironment& env, TypePackId
else
return SubtypingResult{false}
.withSuperComponent(TypePath::PackField::Tail)
.withError({scope->location, UnexpectedTypePackInSubtyping{FFlag::LuauSubtypingFixTailPack ? *superTail : *subTail}});
.withError({scope->location, UnexpectedTypePackInSubtyping{*superTail}});
}
else
return {false};
@ -1100,7 +1100,7 @@ SubtypingResult Subtyping::isCovariantWith(SubtypingEnvironment& env, const Unio
std::vector<SubtypingResult> subtypings;
size_t i = 0;
for (TypeId ty : subUnion)
subtypings.push_back(isCovariantWith(env, ty, superTy, scope).withSubComponent(TypePath::Index{i++}));
subtypings.push_back(isCovariantWith(env, ty, superTy, scope).withSubComponent(TypePath::Index{i++, TypePath::Index::Variant::Union}));
return SubtypingResult::all(subtypings);
}
@ -1110,7 +1110,7 @@ SubtypingResult Subtyping::isCovariantWith(SubtypingEnvironment& env, TypeId sub
std::vector<SubtypingResult> subtypings;
size_t i = 0;
for (TypeId ty : superIntersection)
subtypings.push_back(isCovariantWith(env, subTy, ty, scope).withSuperComponent(TypePath::Index{i++}));
subtypings.push_back(isCovariantWith(env, subTy, ty, scope).withSuperComponent(TypePath::Index{i++, TypePath::Index::Variant::Intersection}));
return SubtypingResult::all(subtypings);
}
@ -1120,7 +1120,7 @@ SubtypingResult Subtyping::isCovariantWith(SubtypingEnvironment& env, const Inte
std::vector<SubtypingResult> subtypings;
size_t i = 0;
for (TypeId ty : subIntersection)
subtypings.push_back(isCovariantWith(env, ty, superTy, scope).withSubComponent(TypePath::Index{i++}));
subtypings.push_back(isCovariantWith(env, ty, superTy, scope).withSubComponent(TypePath::Index{i++, TypePath::Index::Variant::Intersection}));
return SubtypingResult::any(subtypings);
}

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,16 +1,19 @@
// 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"
#include "Luau/TypeUtils.h"
#include "Luau/Unifier2.h"
LUAU_FASTFLAGVARIABLE(LuauDontInPlaceMutateTableType)
LUAU_FASTFLAGVARIABLE(LuauAllowNonSharedTableTypesInLiteral)
LUAU_FASTFLAGVARIABLE(LuauBidirectionalInferenceUpcast)
namespace Luau
{
@ -112,6 +115,7 @@ TypeId matchLiteralType(
NotNull<BuiltinTypes> builtinTypes,
NotNull<TypeArena> arena,
NotNull<Unifier2> unifier,
NotNull<Subtyping> subtyping,
TypeId expectedType,
TypeId exprType,
const AstExpr* expr,
@ -133,7 +137,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 +224,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 +252,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)});
@ -252,19 +275,11 @@ TypeId matchLiteralType(
Property& prop = it->second;
if (FFlag::LuauAllowNonSharedTableTypesInLiteral)
{
// If we encounter a duplcate property, we may have already
// set it to be read-only. If that's the case, the only thing
// that will definitely crash is trying to access a write
// only property.
LUAU_ASSERT(!prop.isWriteOnly());
}
else
{
// Table literals always initially result in shared read-write types
LUAU_ASSERT(prop.isShared());
}
// If we encounter a duplcate property, we may have already
// set it to be read-only. If that's the case, the only thing
// that will definitely crash is trying to access a write
// only property.
LUAU_ASSERT(!prop.isWriteOnly());
TypeId propTy = *prop.readTy;
auto it2 = expectedTableTy->props.find(keyStr);
@ -285,6 +300,7 @@ TypeId matchLiteralType(
builtinTypes,
arena,
unifier,
subtyping,
expectedTableTy->indexer->indexResultType,
propTy,
item.value,
@ -296,10 +312,8 @@ TypeId matchLiteralType(
else
tableTy->indexer = TableIndexer{expectedTableTy->indexer->indexType, matchedType};
if (FFlag::LuauDontInPlaceMutateTableType)
keysToDelete.insert(item.key->as<AstExprConstantString>());
else
tableTy->props.erase(keyStr);
keysToDelete.insert(item.key->as<AstExprConstantString>());
}
// If it's just an extra property and the expected type
@ -323,21 +337,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 +385,7 @@ TypeId matchLiteralType(
builtinTypes,
arena,
unifier,
subtyping,
expectedTableTy->indexer->indexResultType,
*propTy,
item.value,
@ -406,14 +421,11 @@ TypeId matchLiteralType(
LUAU_ASSERT(!"Unexpected");
}
if (FFlag::LuauDontInPlaceMutateTableType)
for (const auto& key : keysToDelete)
{
for (const auto& key : keysToDelete)
{
const AstArray<char>& s = key->value;
std::string keyStr{s.data, s.data + s.size};
tableTy->props.erase(keyStr);
}
const AstArray<char>& s = key->value;
std::string keyStr{s.data, s.data + s.size};
tableTy->props.erase(keyStr);
}
// Keys that the expectedType says we should have, but that aren't

View file

@ -12,8 +12,9 @@
LUAU_FASTFLAG(LuauStoreCSTData)
LUAU_FASTFLAG(LuauExtendStatEndPosWithSemicolon)
LUAU_FASTFLAG(LuauAstTypeGroup2)
LUAU_FASTFLAG(LuauAstTypeGroup3)
LUAU_FASTFLAG(LuauFixDoBlockEndLocation)
LUAU_FASTFLAG(LuauParseOptionalAsNode)
namespace
{
@ -271,6 +272,43 @@ private:
const Position* commaPosition;
};
class ArgNameInserter
{
public:
ArgNameInserter(Writer& w, AstArray<std::optional<AstArgumentName>> names, AstArray<std::optional<Position>> colonPositions)
: writer(w)
, names(names)
, colonPositions(colonPositions)
{
}
void operator()()
{
if (idx < names.size)
{
const auto name = names.data[idx];
if (name.has_value())
{
writer.advance(name->second.begin);
writer.identifier(name->first.value);
if (idx < colonPositions.size)
{
LUAU_ASSERT(colonPositions.data[idx].has_value());
writer.advance(*colonPositions.data[idx]);
}
writer.symbol(":");
}
}
idx++;
}
private:
Writer& writer;
AstArray<std::optional<AstArgumentName>> names;
AstArray<std::optional<Position>> colonPositions;
size_t idx = 0;
};
struct Printer_DEPRECATED
{
explicit Printer_DEPRECATED(Writer& writer)
@ -330,7 +368,7 @@ struct Printer_DEPRECATED
else if (typeCount == 1)
{
bool shouldParenthesize = unconditionallyParenthesize && (list.types.size == 0 || !list.types.data[0]->is<AstTypeGroup>());
if (FFlag::LuauAstTypeGroup2 ? shouldParenthesize : unconditionallyParenthesize)
if (FFlag::LuauAstTypeGroup3 ? shouldParenthesize : unconditionallyParenthesize)
writer.symbol("(");
// Only variadic tail
@ -343,7 +381,7 @@ struct Printer_DEPRECATED
visualizeTypeAnnotation(*list.types.data[0]);
}
if (FFlag::LuauAstTypeGroup2 ? shouldParenthesize : unconditionallyParenthesize)
if (FFlag::LuauAstTypeGroup3 ? shouldParenthesize : unconditionallyParenthesize)
writer.symbol(")");
}
else
@ -1216,6 +1254,15 @@ struct Printer_DEPRECATED
for (size_t i = 0; i < a->types.size; ++i)
{
if (FFlag::LuauParseOptionalAsNode)
{
if (a->types.data[i]->is<AstTypeOptional>())
{
writer.symbol("?");
continue;
}
}
if (i > 0)
{
writer.maybeSpace(a->types.data[i]->location.begin, 2);
@ -1312,7 +1359,7 @@ struct Printer
}
}
void visualizeTypePackAnnotation(const AstTypePack& annotation, bool forVarArg)
void visualizeTypePackAnnotation(AstTypePack& annotation, bool forVarArg)
{
advance(annotation.location.begin);
if (const AstTypePackVariadic* variadicTp = annotation.as<AstTypePackVariadic>())
@ -1322,15 +1369,22 @@ struct Printer
visualizeTypeAnnotation(*variadicTp->variadicType);
}
else if (const AstTypePackGeneric* genericTp = annotation.as<AstTypePackGeneric>())
else if (AstTypePackGeneric* genericTp = annotation.as<AstTypePackGeneric>())
{
writer.symbol(genericTp->genericName.value);
if (const auto cstNode = lookupCstNode<CstTypePackGeneric>(genericTp))
advance(cstNode->ellipsisPosition);
writer.symbol("...");
}
else if (const AstTypePackExplicit* explicitTp = annotation.as<AstTypePackExplicit>())
else if (AstTypePackExplicit* explicitTp = annotation.as<AstTypePackExplicit>())
{
LUAU_ASSERT(!forVarArg);
visualizeTypeList(explicitTp->typeList, true);
if (const auto cstNode = lookupCstNode<CstTypePackExplicit>(explicitTp))
visualizeTypeList(
explicitTp->typeList, true, cstNode->openParenthesesPosition, cstNode->closeParenthesesPosition, cstNode->commaPositions
);
else
visualizeTypeList(explicitTp->typeList, true);
}
else
{
@ -1338,19 +1392,37 @@ struct Printer
}
}
void visualizeTypeList(const AstTypeList& list, bool unconditionallyParenthesize)
void visualizeNamedTypeList(
const AstTypeList& list,
bool unconditionallyParenthesize,
std::optional<Position> openParenthesesPosition,
std::optional<Position> closeParenthesesPosition,
AstArray<Position> commaPositions,
AstArray<std::optional<AstArgumentName>> argNames,
AstArray<std::optional<Position>> argNamesColonPositions
)
{
size_t typeCount = list.types.size + (list.tailType != nullptr ? 1 : 0);
if (typeCount == 0)
{
if (openParenthesesPosition)
advance(*openParenthesesPosition);
writer.symbol("(");
if (closeParenthesesPosition)
advance(*closeParenthesesPosition);
writer.symbol(")");
}
else if (typeCount == 1)
{
bool shouldParenthesize = unconditionallyParenthesize && (list.types.size == 0 || !list.types.data[0]->is<AstTypeGroup>());
if (FFlag::LuauAstTypeGroup2 ? shouldParenthesize : unconditionallyParenthesize)
if (FFlag::LuauAstTypeGroup3 ? shouldParenthesize : unconditionallyParenthesize)
{
if (openParenthesesPosition)
advance(*openParenthesesPosition);
writer.symbol("(");
}
ArgNameInserter(writer, argNames, argNamesColonPositions)();
// Only variadic tail
if (list.types.size == 0)
@ -1362,34 +1434,51 @@ struct Printer
visualizeTypeAnnotation(*list.types.data[0]);
}
if (FFlag::LuauAstTypeGroup2 ? shouldParenthesize : unconditionallyParenthesize)
if (FFlag::LuauAstTypeGroup3 ? shouldParenthesize : unconditionallyParenthesize)
{
if (closeParenthesesPosition)
advance(*closeParenthesesPosition);
writer.symbol(")");
}
}
else
{
if (openParenthesesPosition)
advance(*openParenthesesPosition);
writer.symbol("(");
bool first = true;
CommaSeparatorInserter comma(writer, commaPositions.size > 0 ? commaPositions.begin() : nullptr);
ArgNameInserter argName(writer, argNames, argNamesColonPositions);
for (const auto& el : list.types)
{
if (first)
first = false;
else
writer.symbol(",");
comma();
argName();
visualizeTypeAnnotation(*el);
}
if (list.tailType)
{
writer.symbol(",");
comma();
visualizeTypePackAnnotation(*list.tailType, false);
}
if (closeParenthesesPosition)
advance(*closeParenthesesPosition);
writer.symbol(")");
}
}
void visualizeTypeList(
const AstTypeList& list,
bool unconditionallyParenthesize,
std::optional<Position> openParenthesesPosition = std::nullopt,
std::optional<Position> closeParenthesesPosition = std::nullopt,
AstArray<Position> commaPositions = {}
)
{
visualizeNamedTypeList(list, unconditionallyParenthesize, openParenthesesPosition, closeParenthesesPosition, commaPositions, {}, {});
}
bool isIntegerish(double d)
{
if (d <= std::numeric_limits<int>::max() && d >= std::numeric_limits<int>::min())
@ -1406,7 +1495,7 @@ struct Printer
{
writer.symbol("(");
visualize(*a->expr);
advance(Position{a->location.end.line, a->location.end.column - 1});
advanceBefore(a->location.end, 1);
writer.symbol(")");
}
else if (expr.is<AstExprConstantNil>())
@ -1775,6 +1864,14 @@ struct Printer
writer.advance(newPos);
}
void advanceBefore(const Position& newPos, unsigned int tokenLength)
{
if (newPos.column >= tokenLength)
advance(Position{newPos.line, newPos.column - tokenLength});
else
advance(newPos);
}
void visualize(AstStat& program)
{
advance(program.location.begin);
@ -1817,8 +1914,8 @@ struct Printer
visualizeBlock(*a->body);
if (const auto cstNode = lookupCstNode<CstStatRepeat>(a))
writer.advance(cstNode->untilPosition);
else if (a->condition->location.begin.column > 5)
writer.advance(Position{a->condition->location.begin.line, a->condition->location.begin.column - 6});
else
advanceBefore(a->condition->location.begin, 6);
writer.keyword("until");
visualize(*a->condition);
}
@ -2121,7 +2218,20 @@ struct Printer
{
if (writeTypes)
{
writer.keyword("type function");
const auto cstNode = lookupCstNode<CstStatTypeFunction>(t);
if (t->exported)
writer.keyword("export");
if (cstNode)
advance(cstNode->typeKeywordPosition);
else
writer.space();
writer.keyword("type");
if (cstNode)
advance(cstNode->functionKeywordPosition);
else
writer.space();
writer.keyword("function");
advance(t->nameLocation.begin);
writer.identifier(t->name.value);
visualizeFunctionBody(*t->body);
}
@ -2152,16 +2262,22 @@ struct Printer
if (program.hasSemicolon)
{
if (FFlag::LuauStoreCSTData)
advance(Position{program.location.end.line, program.location.end.column - 1});
advanceBefore(program.location.end, 1);
writer.symbol(";");
}
}
void visualizeFunctionBody(AstExprFunction& func)
{
const auto cstNode = lookupCstNode<CstExprFunction>(&func);
// TODO(CLI-139347): need to handle attributes, argument types, and return type (incl. parentheses of return type)
if (func.generics.size > 0 || func.genericPacks.size > 0)
{
CommaSeparatorInserter comma(writer);
CommaSeparatorInserter comma(writer, cstNode ? cstNode->genericsCommaPositions.begin() : nullptr);
if (cstNode)
advance(cstNode->openGenericsPosition);
writer.symbol("<");
for (const auto& o : func.generics)
{
@ -2176,13 +2292,19 @@ struct Printer
writer.advance(o->location.begin);
writer.identifier(o->name.value);
if (const auto* genericTypePackCstNode = lookupCstNode<CstGenericTypePack>(o))
advance(genericTypePackCstNode->ellipsisPosition);
writer.symbol("...");
}
if (cstNode)
advance(cstNode->closeGenericsPosition);
writer.symbol(">");
}
if (func.argLocation)
advance(func.argLocation->begin);
writer.symbol("(");
CommaSeparatorInserter comma(writer);
CommaSeparatorInserter comma(writer, cstNode ? cstNode->argsCommaPositions.begin() : nullptr);
for (size_t i = 0; i < func.args.size; ++i)
{
@ -2212,10 +2334,14 @@ struct Printer
}
}
if (func.argLocation)
advanceBefore(func.argLocation->end, 1);
writer.symbol(")");
if (writeTypes && func.returnAnnotation)
{
if (cstNode)
advance(cstNode->returnSpecifierPosition);
writer.symbol(":");
writer.space();
@ -2340,9 +2466,13 @@ struct Printer
}
else if (const auto& a = typeAnnotation.as<AstTypeFunction>())
{
const auto cstNode = lookupCstNode<CstTypeFunction>(a);
if (a->generics.size > 0 || a->genericPacks.size > 0)
{
CommaSeparatorInserter comma(writer);
CommaSeparatorInserter comma(writer, cstNode ? cstNode->genericsCommaPositions.begin() : nullptr);
if (cstNode)
advance(cstNode->openGenericsPosition);
writer.symbol("<");
for (const auto& o : a->generics)
{
@ -2357,15 +2487,29 @@ struct Printer
writer.advance(o->location.begin);
writer.identifier(o->name.value);
if (const auto* genericTypePackCstNode = lookupCstNode<CstGenericTypePack>(o))
advance(genericTypePackCstNode->ellipsisPosition);
writer.symbol("...");
}
if (cstNode)
advance(cstNode->closeGenericsPosition);
writer.symbol(">");
}
{
visualizeTypeList(a->argTypes, true);
visualizeNamedTypeList(
a->argTypes,
true,
cstNode ? std::make_optional(cstNode->openArgsPosition) : std::nullopt,
cstNode ? std::make_optional(cstNode->closeArgsPosition) : std::nullopt,
cstNode ? cstNode->argumentsCommaPositions : Luau::AstArray<Position>{},
a->argNames,
cstNode ? cstNode->argumentNameColonPositions : Luau::AstArray<std::optional<Position>>{}
);
}
if (cstNode)
advance(cstNode->returnArrowPosition);
writer.symbol("->");
visualizeTypeList(a->returnTypes, true);
}
@ -2557,6 +2701,15 @@ struct Printer
for (size_t i = 0; i < a->types.size; ++i)
{
if (FFlag::LuauParseOptionalAsNode)
{
if (a->types.data[i]->is<AstTypeOptional>())
{
writer.symbol("?");
continue;
}
}
if (i > 0)
{
writer.maybeSpace(a->types.data[i]->location.begin, 2);
@ -2599,7 +2752,7 @@ struct Printer
{
writer.symbol("(");
visualizeTypeAnnotation(*a->type);
advance(Position{a->location.end.line, a->location.end.column - 1});
advanceBefore(a->location.end, 1);
writer.symbol(")");
}
else if (const auto& a = typeAnnotation.as<AstTypeSingletonBool>())

View file

@ -13,6 +13,8 @@
#include <string>
LUAU_FASTFLAG(LuauStoreCSTData)
static char* allocateString(Luau::Allocator& allocator, std::string_view contents)
{
char* result = (char*)allocator.allocate(contents.size() + 1);
@ -305,7 +307,8 @@ public:
std::optional<AstArgumentName>* arg = &argNames.data[i++];
if (el)
new (arg) std::optional<AstArgumentName>(AstArgumentName(AstName(el->name.c_str()), el->location));
new (arg)
std::optional<AstArgumentName>(AstArgumentName(AstName(el->name.c_str()), FFlag::LuauStoreCSTData ? Location() : el->location));
else
new (arg) std::optional<AstArgumentName>();
}

View file

@ -26,10 +26,13 @@
#include "Luau/VisitType.h"
#include <algorithm>
#include <sstream>
LUAU_FASTFLAG(DebugLuauMagicTypes)
LUAU_FASTFLAG(LuauFreeTypesMustHaveBounds)
LUAU_FASTFLAGVARIABLE(LuauImproveTypePathsInErrors)
LUAU_FASTFLAG(LuauUserTypeFunTypecheck)
namespace Luau
{
@ -1201,7 +1204,8 @@ void TypeChecker2::visit(AstStatTypeAlias* stat)
void TypeChecker2::visit(AstStatTypeFunction* stat)
{
// TODO: add type checking for user-defined type functions
if (FFlag::LuauUserTypeFunTypecheck)
visit(stat->body);
}
void TypeChecker2::visit(AstTypeList types)
@ -2701,20 +2705,61 @@ Reasonings TypeChecker2::explainReasonings_(TID subTy, TID superTy, Location loc
if (!subLeafTy && !superLeafTy && !subLeafTp && !superLeafTp)
ice->ice("Subtyping test returned a reasoning where one path ends at a type and the other ends at a pack.", location);
std::string relation = "a subtype of";
if (reasoning.variance == SubtypingVariance::Invariant)
relation = "exactly";
else if (reasoning.variance == SubtypingVariance::Contravariant)
relation = "a supertype of";
if (FFlag::LuauImproveTypePathsInErrors)
{
std::string relation = "a subtype of";
if (reasoning.variance == SubtypingVariance::Invariant)
relation = "exactly";
else if (reasoning.variance == SubtypingVariance::Contravariant)
relation = "a supertype of";
std::string reason;
if (reasoning.subPath == reasoning.superPath)
reason = "at " + toString(reasoning.subPath) + ", " + toString(subLeaf) + " is not " + relation + " " + toString(superLeaf);
std::string subLeafAsString = toString(subLeaf);
// if the string is empty, it must be an empty type pack
if (subLeafAsString.empty())
subLeafAsString = "()";
std::string superLeafAsString = toString(superLeaf);
// if the string is empty, it must be an empty type pack
if (superLeafAsString.empty())
superLeafAsString = "()";
std::stringstream baseReasonBuilder;
baseReasonBuilder << "`" << subLeafAsString << "` is not " << relation << " `" << superLeafAsString << "`";
std::string baseReason = baseReasonBuilder.str();
std::stringstream reason;
if (reasoning.subPath == reasoning.superPath)
reason << toStringHuman(reasoning.subPath) << "`" << subLeafAsString << "` in the former type and `" << superLeafAsString
<< "` in the latter type, and " << baseReason;
else if (!reasoning.subPath.empty() && !reasoning.superPath.empty())
reason << toStringHuman(reasoning.subPath) << "`" << subLeafAsString << "` and " << toStringHuman(reasoning.superPath) << "`"
<< superLeafAsString << "`, and " << baseReason;
else if (!reasoning.subPath.empty())
reason << toStringHuman(reasoning.subPath) << "`" << subLeafAsString << "`, which is not " << relation << " `" << superLeafAsString
<< "`";
else
reason << toStringHuman(reasoning.superPath) << "`" << superLeafAsString << "`, and " << baseReason;
reasons.push_back(reason.str());
}
else
reason = "type " + toString(subTy) + toString(reasoning.subPath, /* prefixDot */ true) + " (" + toString(subLeaf) + ") is not " +
relation + " " + toString(superTy) + toString(reasoning.superPath, /* prefixDot */ true) + " (" + toString(superLeaf) + ")";
{
std::string relation = "a subtype of";
if (reasoning.variance == SubtypingVariance::Invariant)
relation = "exactly";
else if (reasoning.variance == SubtypingVariance::Contravariant)
relation = "a supertype of";
reasons.push_back(reason);
std::string reason;
if (reasoning.subPath == reasoning.superPath)
reason = "at " + toString(reasoning.subPath) + ", " + toString(subLeaf) + " is not " + relation + " " + toString(superLeaf);
else
reason = "type " + toString(subTy) + toString(reasoning.subPath, /* prefixDot */ true) + " (" + toString(subLeaf) + ") is not " +
relation + " " + toString(superTy) + toString(reasoning.superPath, /* prefixDot */ true) + " (" + toString(superLeaf) + ")";
reasons.push_back(reason);
}
// if we haven't already proved this isn't suppressing, we have to keep checking.
if (suppressed)

View file

@ -18,6 +18,7 @@
#include "Luau/ToString.h"
#include "Luau/TxnLog.h"
#include "Luau/Type.h"
#include "Luau/TypeChecker2.h"
#include "Luau/TypeFunctionReductionGuesser.h"
#include "Luau/TypeFunctionRuntime.h"
#include "Luau/TypeFunctionRuntimeBuilder.h"
@ -49,10 +50,13 @@ LUAU_FASTFLAGVARIABLE(DebugLuauLogTypeFamilies)
LUAU_FASTFLAG(DebugLuauEqSatSimplification)
LUAU_FASTFLAGVARIABLE(LuauMetatableTypeFunctions)
LUAU_FASTFLAGVARIABLE(LuauClipNestedAndRecursiveUnion)
LUAU_FASTFLAGVARIABLE(LuauDoNotGeneralizeInTypeFunctions)
LUAU_FASTFLAGVARIABLE(LuauPreventReentrantTypeFunctionReduction)
LUAU_FASTFLAGVARIABLE(LuauIndexTypeFunctionImprovements)
LUAU_FASTFLAGVARIABLE(LuauIndexTypeFunctionFunctionMetamethods)
LUAU_FASTFLAGVARIABLE(LuauIntersectNotNil)
LUAU_FASTFLAGVARIABLE(LuauSkipNoRefineDuringRefinement)
LUAU_FASTFLAGVARIABLE(LuauDontForgetToReduceUnionFunc)
LUAU_FASTFLAGVARIABLE(LuauSearchForRefineableType)
LUAU_FASTFLAGVARIABLE(LuauIndexAnyIsAny)
namespace Luau
{
@ -449,48 +453,29 @@ static FunctionGraphReductionResult reduceFunctionsInternal(
TypeFunctionReducer reducer{std::move(queuedTys), std::move(queuedTps), std::move(shouldGuess), std::move(cyclics), location, ctx, force};
int iterationCount = 0;
if (FFlag::LuauPreventReentrantTypeFunctionReduction)
// If we are reducing a type function while reducing a type function,
// we're probably doing something clowny. One known place this can
// occur is type function reduction => overload selection => subtyping
// => back to type function reduction. At worst, if there's a reduction
// that _doesn't_ loop forever and _needs_ reentrancy, we'll fail to
// handle that and potentially emit an error when we didn't need to.
if (ctx.normalizer->sharedState->reentrantTypeReduction)
return {};
TypeReductionRentrancyGuard _{ctx.normalizer->sharedState};
while (!reducer.done())
{
// If we are reducing a type function while reducing a type function,
// we're probably doing something clowny. One known place this can
// occur is type function reduction => overload selection => subtyping
// => back to type function reduction. At worst, if there's a reduction
// that _doesn't_ loop forever and _needs_ reentrancy, we'll fail to
// handle that and potentially emit an error when we didn't need to.
if (ctx.normalizer->sharedState->reentrantTypeReduction)
return {};
reducer.step();
TypeReductionRentrancyGuard _{ctx.normalizer->sharedState};
while (!reducer.done())
++iterationCount;
if (iterationCount > DFInt::LuauTypeFamilyGraphReductionMaximumSteps)
{
reducer.step();
++iterationCount;
if (iterationCount > DFInt::LuauTypeFamilyGraphReductionMaximumSteps)
{
reducer.result.errors.emplace_back(location, CodeTooComplex{});
break;
}
reducer.result.errors.emplace_back(location, CodeTooComplex{});
break;
}
return std::move(reducer.result);
}
else
{
while (!reducer.done())
{
reducer.step();
++iterationCount;
if (iterationCount > DFInt::LuauTypeFamilyGraphReductionMaximumSteps)
{
reducer.result.errors.emplace_back(location, CodeTooComplex{});
break;
}
}
return std::move(reducer.result);
}
return std::move(reducer.result);
}
FunctionGraphReductionResult reduceTypeFunctions(TypeId entrypoint, Location location, TypeFunctionContext ctx, bool force)
@ -630,6 +615,9 @@ static std::optional<TypeFunctionReductionResult<TypeId>> tryDistributeTypeFunct
{},
});
if (FFlag::LuauDontForgetToReduceUnionFunc && ctx->solver)
ctx->pushConstraint(ReduceConstraint{resultTy});
return {{resultTy, Reduction::MaybeOk, {}, {}}};
}
@ -856,15 +844,6 @@ TypeFunctionReductionResult<TypeId> lenTypeFunction(
if (isPending(operandTy, ctx->solver))
return {std::nullopt, Reduction::MaybeOk, {operandTy}, {}};
// if the type is free but has only one remaining reference, we can generalize it to its upper bound here.
if (ctx->solver && !FFlag::LuauDoNotGeneralizeInTypeFunctions)
{
std::optional<TypeId> maybeGeneralized = ctx->solver->generalizeFreeType(ctx->scope, operandTy, /* avoidSealingTables */ true);
if (!maybeGeneralized)
return {std::nullopt, Reduction::MaybeOk, {operandTy}, {}};
operandTy = *maybeGeneralized;
}
std::shared_ptr<const NormalizedType> normTy = ctx->normalizer->normalize(operandTy);
NormalizationResult inhabited = ctx->normalizer->isInhabited(normTy.get());
@ -948,15 +927,6 @@ TypeFunctionReductionResult<TypeId> unmTypeFunction(
if (isPending(operandTy, ctx->solver))
return {std::nullopt, Reduction::MaybeOk, {operandTy}, {}};
// if the type is free but has only one remaining reference, we can generalize it to its upper bound here.
if (ctx->solver && !FFlag::LuauDoNotGeneralizeInTypeFunctions)
{
std::optional<TypeId> maybeGeneralized = ctx->solver->generalizeFreeType(ctx->scope, operandTy);
if (!maybeGeneralized)
return {std::nullopt, Reduction::MaybeOk, {operandTy}, {}};
operandTy = *maybeGeneralized;
}
std::shared_ptr<const NormalizedType> normTy = ctx->normalizer->normalize(operandTy);
// if the operand failed to normalize, we can't reduce, but know nothing about inhabitance.
@ -1191,21 +1161,6 @@ TypeFunctionReductionResult<TypeId> numericBinopTypeFunction(
else if (isPending(rhsTy, ctx->solver))
return {std::nullopt, Reduction::MaybeOk, {rhsTy}, {}};
// if either type is free but has only one remaining reference, we can generalize it to its upper bound here.
if (ctx->solver && !FFlag::LuauDoNotGeneralizeInTypeFunctions)
{
std::optional<TypeId> lhsMaybeGeneralized = ctx->solver->generalizeFreeType(ctx->scope, lhsTy);
std::optional<TypeId> rhsMaybeGeneralized = ctx->solver->generalizeFreeType(ctx->scope, rhsTy);
if (!lhsMaybeGeneralized)
return {std::nullopt, Reduction::MaybeOk, {lhsTy}, {}};
else if (!rhsMaybeGeneralized)
return {std::nullopt, Reduction::MaybeOk, {rhsTy}, {}};
lhsTy = *lhsMaybeGeneralized;
rhsTy = *rhsMaybeGeneralized;
}
// TODO: Normalization needs to remove cyclic type functions from a `NormalizedType`.
std::shared_ptr<const NormalizedType> normLhsTy = ctx->normalizer->normalize(lhsTy);
std::shared_ptr<const NormalizedType> normRhsTy = ctx->normalizer->normalize(rhsTy);
@ -1428,21 +1383,6 @@ TypeFunctionReductionResult<TypeId> concatTypeFunction(
else if (isPending(rhsTy, ctx->solver))
return {std::nullopt, Reduction::MaybeOk, {rhsTy}, {}};
// if either type is free but has only one remaining reference, we can generalize it to its upper bound here.
if (ctx->solver && !FFlag::LuauDoNotGeneralizeInTypeFunctions)
{
std::optional<TypeId> lhsMaybeGeneralized = ctx->solver->generalizeFreeType(ctx->scope, lhsTy);
std::optional<TypeId> rhsMaybeGeneralized = ctx->solver->generalizeFreeType(ctx->scope, rhsTy);
if (!lhsMaybeGeneralized)
return {std::nullopt, Reduction::MaybeOk, {lhsTy}, {}};
else if (!rhsMaybeGeneralized)
return {std::nullopt, Reduction::MaybeOk, {rhsTy}, {}};
lhsTy = *lhsMaybeGeneralized;
rhsTy = *rhsMaybeGeneralized;
}
std::shared_ptr<const NormalizedType> normLhsTy = ctx->normalizer->normalize(lhsTy);
std::shared_ptr<const NormalizedType> normRhsTy = ctx->normalizer->normalize(rhsTy);
@ -1543,21 +1483,6 @@ TypeFunctionReductionResult<TypeId> andTypeFunction(
else if (isPending(rhsTy, ctx->solver))
return {std::nullopt, Reduction::MaybeOk, {rhsTy}, {}};
// if either type is free but has only one remaining reference, we can generalize it to its upper bound here.
if (ctx->solver && !FFlag::LuauDoNotGeneralizeInTypeFunctions)
{
std::optional<TypeId> lhsMaybeGeneralized = ctx->solver->generalizeFreeType(ctx->scope, lhsTy);
std::optional<TypeId> rhsMaybeGeneralized = ctx->solver->generalizeFreeType(ctx->scope, rhsTy);
if (!lhsMaybeGeneralized)
return {std::nullopt, Reduction::MaybeOk, {lhsTy}, {}};
else if (!rhsMaybeGeneralized)
return {std::nullopt, Reduction::MaybeOk, {rhsTy}, {}};
lhsTy = *lhsMaybeGeneralized;
rhsTy = *rhsMaybeGeneralized;
}
// And evalutes to a boolean if the LHS is falsey, and the RHS type if LHS is truthy.
SimplifyResult filteredLhs = simplifyIntersection(ctx->builtins, ctx->arena, lhsTy, ctx->builtins->falsyType);
SimplifyResult overallResult = simplifyUnion(ctx->builtins, ctx->arena, rhsTy, filteredLhs.result);
@ -1598,21 +1523,6 @@ TypeFunctionReductionResult<TypeId> orTypeFunction(
else if (isPending(rhsTy, ctx->solver))
return {std::nullopt, Reduction::MaybeOk, {rhsTy}, {}};
// if either type is free but has only one remaining reference, we can generalize it to its upper bound here.
if (ctx->solver && !FFlag::LuauDoNotGeneralizeInTypeFunctions)
{
std::optional<TypeId> lhsMaybeGeneralized = ctx->solver->generalizeFreeType(ctx->scope, lhsTy);
std::optional<TypeId> rhsMaybeGeneralized = ctx->solver->generalizeFreeType(ctx->scope, rhsTy);
if (!lhsMaybeGeneralized)
return {std::nullopt, Reduction::MaybeOk, {lhsTy}, {}};
else if (!rhsMaybeGeneralized)
return {std::nullopt, Reduction::MaybeOk, {rhsTy}, {}};
lhsTy = *lhsMaybeGeneralized;
rhsTy = *rhsMaybeGeneralized;
}
// Or evalutes to the LHS type if the LHS is truthy, and the RHS type if LHS is falsy.
SimplifyResult filteredLhs = simplifyIntersection(ctx->builtins, ctx->arena, lhsTy, ctx->builtins->truthyType);
SimplifyResult overallResult = simplifyUnion(ctx->builtins, ctx->arena, rhsTy, filteredLhs.result);
@ -1684,21 +1594,6 @@ static TypeFunctionReductionResult<TypeId> comparisonTypeFunction(
lhsTy = follow(lhsTy);
rhsTy = follow(rhsTy);
// if either type is free but has only one remaining reference, we can generalize it to its upper bound here.
if (ctx->solver && !FFlag::LuauDoNotGeneralizeInTypeFunctions)
{
std::optional<TypeId> lhsMaybeGeneralized = ctx->solver->generalizeFreeType(ctx->scope, lhsTy);
std::optional<TypeId> rhsMaybeGeneralized = ctx->solver->generalizeFreeType(ctx->scope, rhsTy);
if (!lhsMaybeGeneralized)
return {std::nullopt, Reduction::MaybeOk, {lhsTy}, {}};
else if (!rhsMaybeGeneralized)
return {std::nullopt, Reduction::MaybeOk, {rhsTy}, {}};
lhsTy = *lhsMaybeGeneralized;
rhsTy = *rhsMaybeGeneralized;
}
// check to see if both operand types are resolved enough, and wait to reduce if not
std::shared_ptr<const NormalizedType> normLhsTy = ctx->normalizer->normalize(lhsTy);
@ -1822,21 +1717,6 @@ TypeFunctionReductionResult<TypeId> eqTypeFunction(
else if (isPending(rhsTy, ctx->solver))
return {std::nullopt, Reduction::MaybeOk, {rhsTy}, {}};
// if either type is free but has only one remaining reference, we can generalize it to its upper bound here.
if (ctx->solver && !FFlag::LuauDoNotGeneralizeInTypeFunctions)
{
std::optional<TypeId> lhsMaybeGeneralized = ctx->solver->generalizeFreeType(ctx->scope, lhsTy);
std::optional<TypeId> rhsMaybeGeneralized = ctx->solver->generalizeFreeType(ctx->scope, rhsTy);
if (!lhsMaybeGeneralized)
return {std::nullopt, Reduction::MaybeOk, {lhsTy}, {}};
else if (!rhsMaybeGeneralized)
return {std::nullopt, Reduction::MaybeOk, {rhsTy}, {}};
lhsTy = *lhsMaybeGeneralized;
rhsTy = *rhsMaybeGeneralized;
}
std::shared_ptr<const NormalizedType> normLhsTy = ctx->normalizer->normalize(lhsTy);
std::shared_ptr<const NormalizedType> normRhsTy = ctx->normalizer->normalize(rhsTy);
NormalizationResult lhsInhabited = ctx->normalizer->isInhabited(normLhsTy.get());
@ -1934,6 +1814,33 @@ struct FindRefinementBlockers : TypeOnceVisitor
}
};
struct ContainsRefinableType : TypeOnceVisitor
{
bool found = false;
ContainsRefinableType() : TypeOnceVisitor(/* skipBoundTypes */ true) {}
bool visit(TypeId ty) override {
// Default case: if we find *some* type that's worth refining against,
// then we can claim that this type contains a refineable type.
found = true;
return false;
}
bool visit(TypeId Ty, const NoRefineType&) override {
// No refine types aren't interesting
return false;
}
bool visit(TypeId ty, const TableType&) override { return !found; }
bool visit(TypeId ty, const MetatableType&) override { return !found; }
bool visit(TypeId ty, const FunctionType&) override { return !found; }
bool visit(TypeId ty, const UnionType&) override { return !found; }
bool visit(TypeId ty, const IntersectionType&) override { return !found; }
bool visit(TypeId ty, const NegationType&) override { return !found; }
};
TypeFunctionReductionResult<TypeId> refineTypeFunction(
TypeId instance,
const std::vector<TypeId>& typeParams,
@ -1968,20 +1875,6 @@ TypeFunctionReductionResult<TypeId> refineTypeFunction(
auto stepRefine = [&ctx](TypeId target, TypeId discriminant) -> std::pair<TypeId, std::vector<TypeId>>
{
std::vector<TypeId> toBlock;
if (ctx->solver && !FFlag::LuauDoNotGeneralizeInTypeFunctions)
{
std::optional<TypeId> targetMaybeGeneralized = ctx->solver->generalizeFreeType(ctx->scope, target);
std::optional<TypeId> discriminantMaybeGeneralized = ctx->solver->generalizeFreeType(ctx->scope, discriminant);
if (!targetMaybeGeneralized)
return std::pair<TypeId, std::vector<TypeId>>{nullptr, {target}};
else if (!discriminantMaybeGeneralized)
return std::pair<TypeId, std::vector<TypeId>>{nullptr, {discriminant}};
target = *targetMaybeGeneralized;
discriminant = *discriminantMaybeGeneralized;
}
// we need a more complex check for blocking on the discriminant in particular
FindRefinementBlockers frb;
frb.traverse(discriminant);
@ -2007,14 +1900,28 @@ TypeFunctionReductionResult<TypeId> refineTypeFunction(
}
else
{
if (FFlag::LuauSkipNoRefineDuringRefinement)
if (get<NoRefineType>(discriminant))
return {target, {}};
if (auto nt = get<NegationType>(discriminant))
if (FFlag::LuauSearchForRefineableType)
{
if (get<NoRefineType>(follow(nt->ty)))
// If the discriminant type is only:
// - The `*no-refine*` type or,
// - tables, metatables, unions, intersections, functions, or negations _containing_ `*no-refine*`.
// There's no point in refining against it.
ContainsRefinableType crt;
crt.traverse(discriminant);
if (!crt.found)
return {target, {}};
}
else
{
if (FFlag::LuauSkipNoRefineDuringRefinement)
if (get<NoRefineType>(discriminant))
return {target, {}};
if (auto nt = get<NegationType>(discriminant))
{
if (get<NoRefineType>(follow(nt->ty)))
return {target, {}};
}
}
// If the target type is a table, then simplification already implements the logic to deal with refinements properly since the
// type of the discriminant is guaranteed to only ever be an (arbitrarily-nested) table of a single property type.
@ -2085,15 +1992,6 @@ TypeFunctionReductionResult<TypeId> singletonTypeFunction(
if (isPending(type, ctx->solver))
return {std::nullopt, Reduction::MaybeOk, {type}, {}};
// if the type is free but has only one remaining reference, we can generalize it to its upper bound here.
if (ctx->solver && !FFlag::LuauDoNotGeneralizeInTypeFunctions)
{
std::optional<TypeId> maybeGeneralized = ctx->solver->generalizeFreeType(ctx->scope, type);
if (!maybeGeneralized)
return {std::nullopt, Reduction::MaybeOk, {type}, {}};
type = *maybeGeneralized;
}
TypeId followed = type;
// we want to follow through a negation here as well.
if (auto negation = get<NegationType>(followed))
@ -2161,73 +2059,24 @@ TypeFunctionReductionResult<TypeId> unionTypeFunction(
if (typeParams.size() == 1)
return {follow(typeParams[0]), Reduction::MaybeOk, {}, {}};
if (FFlag::LuauClipNestedAndRecursiveUnion)
CollectUnionTypeOptions collector{ctx};
collector.traverse(instance);
if (!collector.blockingTypes.empty())
{
CollectUnionTypeOptions collector{ctx};
collector.traverse(instance);
if (!collector.blockingTypes.empty())
{
std::vector<TypeId> blockingTypes{collector.blockingTypes.begin(), collector.blockingTypes.end()};
return {std::nullopt, Reduction::MaybeOk, std::move(blockingTypes), {}};
}
TypeId resultTy = ctx->builtins->neverType;
for (auto ty : collector.options)
{
SimplifyResult result = simplifyUnion(ctx->builtins, ctx->arena, resultTy, ty);
// This condition might fire if one of the arguments to this type
// function is a free type somewhere deep in a nested union or
// intersection type, even though we ran a pass above to capture
// some blocked types.
if (!result.blockedTypes.empty())
return {std::nullopt, Reduction::MaybeOk, {result.blockedTypes.begin(), result.blockedTypes.end()}, {}};
resultTy = result.result;
}
return {resultTy, Reduction::MaybeOk, {}, {}};
std::vector<TypeId> blockingTypes{collector.blockingTypes.begin(), collector.blockingTypes.end()};
return {std::nullopt, Reduction::MaybeOk, std::move(blockingTypes), {}};
}
// we need to follow all of the type parameters.
std::vector<TypeId> types;
types.reserve(typeParams.size());
for (auto ty : typeParams)
types.emplace_back(follow(ty));
// unfortunately, we need this short-circuit: if all but one type is `never`, we will return that one type.
// this also will early return if _everything_ is `never`, since we already have to check that.
std::optional<TypeId> lastType = std::nullopt;
for (auto ty : types)
{
// if we have a previous type and it's not `never` and the current type isn't `never`...
if (lastType && !get<NeverType>(lastType) && !get<NeverType>(ty))
{
// we know we are not taking the short-circuited path.
lastType = std::nullopt;
break;
}
if (get<NeverType>(ty))
continue;
lastType = ty;
}
// if we still have a `lastType` at the end, we're taking the short-circuit and reducing early.
if (lastType)
return {lastType, Reduction::MaybeOk, {}, {}};
// check to see if the operand types are resolved enough, and wait to reduce if not
for (auto ty : types)
if (isPending(ty, ctx->solver))
return {std::nullopt, Reduction::MaybeOk, {ty}, {}};
// fold over the types with `simplifyUnion`
TypeId resultTy = ctx->builtins->neverType;
for (auto ty : types)
for (auto ty : collector.options)
{
SimplifyResult result = simplifyUnion(ctx->builtins, ctx->arena, resultTy, ty);
// This condition might fire if one of the arguments to this type
// function is a free type somewhere deep in a nested union or
// intersection type, even though we ran a pass above to capture
// some blocked types.
if (!result.blockedTypes.empty())
return {std::nullopt, Reduction::MaybeOk, {result.blockedTypes.begin(), result.blockedTypes.end()}, {}};
@ -2235,6 +2084,7 @@ TypeFunctionReductionResult<TypeId> unionTypeFunction(
}
return {resultTy, Reduction::MaybeOk, {}, {}};
}
@ -2575,7 +2425,12 @@ bool searchPropsAndIndexer(
if (auto propUnionTy = get<UnionType>(propTy))
{
for (TypeId option : propUnionTy->options)
result.insert(option);
{
if (FFlag::LuauIndexTypeFunctionImprovements)
result.insert(follow(option));
else
result.insert(option);
}
}
else // property is a singular type or intersection type -> we can simply append
result.insert(propTy);
@ -2595,7 +2450,12 @@ bool searchPropsAndIndexer(
if (auto idxResUnionTy = get<UnionType>(idxResultTy))
{
for (TypeId option : idxResUnionTy->options)
result.insert(option);
{
if (FFlag::LuauIndexTypeFunctionImprovements)
result.insert(follow(option));
else
result.insert(option);
}
}
else // indexResultType is a singular type or intersection type -> we can simply append
result.insert(idxResultTy);
@ -2610,7 +2470,7 @@ bool searchPropsAndIndexer(
/* Handles recursion / metamethods of tables/classes
`isRaw` parameter indicates whether or not we should follow __index metamethods
returns false if property of `ty` could not be found */
bool tblIndexInto(TypeId indexer, TypeId indexee, DenseHashSet<TypeId>& result, NotNull<TypeFunctionContext> ctx, bool isRaw)
bool tblIndexInto_DEPRECATED(TypeId indexer, TypeId indexee, DenseHashSet<TypeId>& result, NotNull<TypeFunctionContext> ctx, bool isRaw)
{
indexer = follow(indexer);
indexee = follow(indexee);
@ -2640,13 +2500,113 @@ bool tblIndexInto(TypeId indexer, TypeId indexee, DenseHashSet<TypeId>& result,
ErrorVec dummy;
std::optional<TypeId> mmType = findMetatableEntry(ctx->builtins, dummy, indexee, "__index", Location{});
if (mmType)
return tblIndexInto(indexer, *mmType, result, ctx, isRaw);
return tblIndexInto_DEPRECATED(indexer, *mmType, result, ctx, isRaw);
}
}
return false;
}
bool tblIndexInto(TypeId indexer, TypeId indexee, DenseHashSet<TypeId>& result, DenseHashSet<TypeId>& seenSet, NotNull<TypeFunctionContext> ctx, bool isRaw)
{
indexer = follow(indexer);
indexee = follow(indexee);
if (seenSet.contains(indexee))
return false;
seenSet.insert(indexee);
if (FFlag::LuauIndexTypeFunctionFunctionMetamethods)
{
if (auto unionTy = get<UnionType>(indexee))
{
bool res = true;
for (auto component : unionTy)
{
// if the component is in the seen set and isn't the indexee itself,
// we can skip it cause it means we encountered it in an earlier component in the union.
if (seenSet.contains(component) && component != indexee)
continue;
res = res && tblIndexInto(indexer, component, result, seenSet, ctx, isRaw);
}
return res;
}
if (get<FunctionType>(indexee))
{
TypePackId argPack = ctx->arena->addTypePack({indexer});
SolveResult solveResult = solveFunctionCall(
ctx->arena,
ctx->builtins,
ctx->simplifier,
ctx->normalizer,
ctx->typeFunctionRuntime,
ctx->ice,
ctx->limits,
ctx->scope,
ctx->scope->location,
indexee,
argPack
);
if (!solveResult.typePackId.has_value())
return false;
TypePack extracted = extendTypePack(*ctx->arena, ctx->builtins, *solveResult.typePackId, 1);
if (extracted.head.empty())
return false;
result.insert(follow(extracted.head.front()));
return true;
}
}
// we have a table type to try indexing
if (auto tableTy = get<TableType>(indexee))
{
return searchPropsAndIndexer(indexer, tableTy->props, tableTy->indexer, result, ctx);
}
// we have a metatable type to try indexing
if (auto metatableTy = get<MetatableType>(indexee))
{
if (auto tableTy = get<TableType>(follow(metatableTy->table)))
{
// try finding all properties within the current scope of the table
if (searchPropsAndIndexer(indexer, tableTy->props, tableTy->indexer, result, ctx))
return true;
}
// if the code reached here, it means we weren't able to find all properties -> look into __index metamethod
if (!isRaw)
{
// findMetatableEntry demands the ability to emit errors, so we must give it
// the necessary state to do that, even if we intend to just eat the errors.
ErrorVec dummy;
std::optional<TypeId> mmType = findMetatableEntry(ctx->builtins, dummy, indexee, "__index", Location{});
if (mmType)
return tblIndexInto(indexer, *mmType, result, seenSet, ctx, isRaw);
}
}
return false;
}
bool tblIndexInto(TypeId indexer, TypeId indexee, DenseHashSet<TypeId>& result, NotNull<TypeFunctionContext> ctx, bool isRaw)
{
if (FFlag::LuauIndexTypeFunctionImprovements)
{
DenseHashSet<TypeId> seenSet{{}};
return tblIndexInto(indexer, indexee, result, seenSet, ctx, isRaw);
}
else
{
return tblIndexInto_DEPRECATED(indexer, indexee, result, ctx, isRaw);
}
}
/* Vocabulary note: indexee refers to the type that contains the properties,
indexer refers to the type that is used to access indexee
Example: index<Person, "name"> => `Person` is the indexee and `"name"` is the indexer */
@ -2664,6 +2624,13 @@ TypeFunctionReductionResult<TypeId> indexFunctionImpl(
if (!indexeeNormTy)
return {std::nullopt, Reduction::MaybeOk, {}, {}};
if (FFlag::LuauIndexAnyIsAny)
{
// if the indexee is `any`, then indexing also gives us `any`.
if (indexeeNormTy->shouldSuppressErrors())
return {ctx->builtins->anyType, Reduction::MaybeOk, {}, {}};
}
// if we don't have either just tables or just classes, we've got nothing to index into
if (indexeeNormTy->hasTables() == indexeeNormTy->hasClasses())
return {std::nullopt, Reduction::Erroneous, {}, {}};
@ -2763,17 +2730,19 @@ TypeFunctionReductionResult<TypeId> indexFunctionImpl(
}
}
// Call `follow()` on each element to resolve all Bound types before returning
std::transform(
properties.begin(),
properties.end(),
properties.begin(),
[](TypeId ty)
{
return follow(ty);
}
if (!FFlag::LuauIndexTypeFunctionImprovements)
{
// Call `follow()` on each element to resolve all Bound types before returning
std::transform(
properties.begin(),
properties.end(),
properties.begin(),
[](TypeId ty)
{
return follow(ty);
}
);
}
// If the type being reduced to is a single type, no need to union
if (properties.size() == 1)
return {*properties.begin(), Reduction::MaybeOk, {}, {}};

View file

@ -13,10 +13,7 @@
#include <set>
#include <vector>
LUAU_FASTFLAGVARIABLE(LuauTypeFunFixHydratedClasses)
LUAU_DYNAMIC_FASTINT(LuauTypeFunctionSerdeIterationLimit)
LUAU_FASTFLAGVARIABLE(LuauTypeFunSingletonEquality)
LUAU_FASTFLAGVARIABLE(LuauUserTypeFunTypeofReturnsType)
LUAU_FASTFLAGVARIABLE(LuauTypeFunPrintFix)
LUAU_FASTFLAGVARIABLE(LuauTypeFunReadWriteParents)
@ -1617,11 +1614,8 @@ void registerTypeUserData(lua_State* L)
// Create and register metatable for type userdata
luaL_newmetatable(L, "type");
if (FFlag::LuauUserTypeFunTypeofReturnsType)
{
lua_pushstring(L, "type");
lua_setfield(L, -2, "__type");
}
lua_pushstring(L, "type");
lua_setfield(L, -2, "__type");
// Protect metatable from being changed
lua_pushstring(L, "The metatable is locked");
@ -1758,14 +1752,14 @@ bool areEqual(SeenSet& seen, const TypeFunctionSingletonType& lhs, const TypeFun
{
const TypeFunctionBooleanSingleton* lp = get<TypeFunctionBooleanSingleton>(&lhs);
const TypeFunctionBooleanSingleton* rp = get<TypeFunctionBooleanSingleton>(FFlag::LuauTypeFunSingletonEquality ? &rhs : &lhs);
const TypeFunctionBooleanSingleton* rp = get<TypeFunctionBooleanSingleton>(&rhs);
if (lp && rp)
return lp->value == rp->value;
}
{
const TypeFunctionStringSingleton* lp = get<TypeFunctionStringSingleton>(&lhs);
const TypeFunctionStringSingleton* rp = get<TypeFunctionStringSingleton>(FFlag::LuauTypeFunSingletonEquality ? &rhs : &lhs);
const TypeFunctionStringSingleton* rp = get<TypeFunctionStringSingleton>(&rhs);
if (lp && rp)
return lp->value == rp->value;
}
@ -1918,10 +1912,7 @@ bool areEqual(SeenSet& seen, const TypeFunctionClassType& lhs, const TypeFunctio
if (seenSetContains(seen, &lhs, &rhs))
return true;
if (FFlag::LuauTypeFunFixHydratedClasses)
return lhs.classTy == rhs.classTy;
else
return lhs.name_DEPRECATED == rhs.name_DEPRECATED;
return lhs.classTy == rhs.classTy;
}
bool areEqual(SeenSet& seen, const TypeFunctionType& lhs, const TypeFunctionType& rhs)

View file

@ -19,7 +19,6 @@
// used to control the recursion limit of any operations done by user-defined type functions
// currently, controls serialization, deserialization, and `type.copy`
LUAU_DYNAMIC_FASTINTVARIABLE(LuauTypeFunctionSerdeIterationLimit, 100'000);
LUAU_FASTFLAG(LuauTypeFunFixHydratedClasses)
LUAU_FASTFLAG(LuauTypeFunReadWriteParents)
namespace Luau
@ -209,19 +208,11 @@ private:
}
else if (auto c = get<ClassType>(ty))
{
if (FFlag::LuauTypeFunFixHydratedClasses)
{
// Since there aren't any new class types being created in type functions, we will deserialize by using a direct reference to the
// original class
target = typeFunctionRuntime->typeArena.allocate(TypeFunctionClassType{{}, std::nullopt, std::nullopt, std::nullopt, std::nullopt, std::nullopt, ty});
}
else
{
state->classesSerialized_DEPRECATED[c->name] = ty;
target = typeFunctionRuntime->typeArena.allocate(
TypeFunctionClassType{{}, std::nullopt, std::nullopt, std::nullopt, std::nullopt, std::nullopt, /* classTy */ nullptr, c->name}
);
}
// Since there aren't any new class types being created in type functions, we will deserialize by using a direct reference to the original
// class
target = typeFunctionRuntime->typeArena.allocate(
TypeFunctionClassType{{}, std::nullopt, std::nullopt, std::nullopt, std::nullopt, std::nullopt, ty}
);
}
else if (auto g = get<GenericType>(ty))
{
@ -713,17 +704,7 @@ private:
}
else if (auto c = get<TypeFunctionClassType>(ty))
{
if (FFlag::LuauTypeFunFixHydratedClasses)
{
target = c->classTy;
}
else
{
if (auto result = state->classesSerialized_DEPRECATED.find(c->name_DEPRECATED))
target = *result;
else
state->ctx->ice->ice("Deserializing user defined type function arguments: mysterious class type is being deserialized");
}
target = c->classTy;
}
else if (auto g = get<TypeFunctionGenericType>(ty))
{

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;
@ -5724,6 +5721,10 @@ TypeId TypeChecker::resolveTypeWorker(const ScopePtr& scope, const AstType& anno
TypeId ty = checkExpr(scope, *typeOf->expr).type;
return ty;
}
else if (annotation.is<AstTypeOptional>())
{
return builtinTypes->nilType;
}
else if (const auto& un = annotation.as<AstTypeUnion>())
{
if (FFlag::LuauPreserveUnionIntersectionNodeForLeadingTokenSingleType)

View file

@ -14,7 +14,8 @@
#include <type_traits>
LUAU_FASTFLAG(LuauSolverV2);
LUAU_FASTFLAGVARIABLE(LuauDisableNewSolverAssertsInMixedMode);
LUAU_FASTFLAGVARIABLE(LuauDisableNewSolverAssertsInMixedMode)
// Maximum number of steps to follow when traversing a path. May not always
// equate to the number of components in a path, depending on the traversal
// logic.
@ -638,6 +639,247 @@ std::string toString(const TypePath::Path& path, bool prefixDot)
return result.str();
}
std::string toStringHuman(const TypePath::Path& path)
{
LUAU_ASSERT(FFlag::LuauSolverV2);
enum class State
{
Initial,
Normal,
Property,
PendingIs,
PendingAs,
PendingWhich,
};
std::stringstream result;
State state = State::Initial;
bool last = false;
auto strComponent = [&](auto&& c)
{
using T = std::decay_t<decltype(c)>;
if constexpr (std::is_same_v<T, TypePath::Property>)
{
if (state == State::PendingIs)
result << ", ";
switch (state)
{
case State::Initial:
case State::PendingIs:
if (c.isRead)
result << "accessing `";
else
result << "writing to `";
break;
case State::Property:
// if the previous state was a property, then we're doing a sequence of indexing
result << '.';
break;
default:
break;
}
result << c.name;
state = State::Property;
}
else if constexpr (std::is_same_v<T, TypePath::Index>)
{
size_t humanIndex = c.index + 1;
if (state == State::Initial && !last)
result << "in" << ' ';
else if (state == State::PendingIs)
result << ' ' << "has" << ' ';
else if (state == State::Property)
result << '`' << ' ' << "has" << ' ';
result << "the " << humanIndex;
switch (humanIndex)
{
case 1:
result << "st";
break;
case 2:
result << "nd";
break;
case 3:
result << "rd";
break;
default:
result << "th";
}
switch (c.variant)
{
case TypePath::Index::Variant::Pack:
result << ' ' << "entry in the type pack";
break;
case TypePath::Index::Variant::Union:
result << ' ' << "component of the union";
break;
case TypePath::Index::Variant::Intersection:
result << ' ' << "component of the intersection";
break;
}
if (state == State::PendingWhich)
result << ' ' << "which";
if (state == State::PendingIs || state == State::Property)
state = State::PendingAs;
else
state = State::PendingIs;
}
else if constexpr (std::is_same_v<T, TypePath::TypeField>)
{
if (state == State::Initial && !last)
result << "in" << ' ';
else if (state == State::PendingIs)
result << ", ";
else if (state == State::Property)
result << '`' << ' ' << "has" << ' ';
switch (c)
{
case TypePath::TypeField::Table:
result << "the table portion";
if (state == State::Property)
state = State::PendingAs;
else
state = State::PendingIs;
break;
case TypePath::TypeField::Metatable:
result << "the metatable portion";
if (state == State::Property)
state = State::PendingAs;
else
state = State::PendingIs;
break;
case TypePath::TypeField::LowerBound:
result << "the lower bound of" << ' ';
state = State::Normal;
break;
case TypePath::TypeField::UpperBound:
result << "the upper bound of" << ' ';
state = State::Normal;
break;
case TypePath::TypeField::IndexLookup:
result << "the index type";
if (state == State::Property)
state = State::PendingAs;
else
state = State::PendingIs;
break;
case TypePath::TypeField::IndexResult:
result << "the result of indexing";
if (state == State::Property)
state = State::PendingAs;
else
state = State::PendingIs;
break;
case TypePath::TypeField::Negated:
result << "the negation" << ' ';
state = State::Normal;
break;
case TypePath::TypeField::Variadic:
result << "the variadic" << ' ';
state = State::Normal;
break;
}
}
else if constexpr (std::is_same_v<T, TypePath::PackField>)
{
if (state == State::PendingIs)
result << ", ";
else if (state == State::Property)
result << "`, ";
switch (c)
{
case TypePath::PackField::Arguments:
if (state == State::Initial)
result << "it" << ' ';
else if (state == State::PendingIs)
result << "the function" << ' ';
result << "takes";
break;
case TypePath::PackField::Returns:
if (state == State::Initial)
result << "it" << ' ';
else if (state == State::PendingIs)
result << "the function" << ' ';
result << "returns";
break;
case TypePath::PackField::Tail:
if (state == State::Initial)
result << "it has" << ' ';
result << "a tail of";
break;
}
if (state == State::PendingIs)
{
result << ' ';
state = State::PendingWhich;
}
else
{
result << ' ';
state = State::Normal;
}
}
else if constexpr (std::is_same_v<T, TypePath::Reduction>)
{
if (state == State::Initial)
result << "it" << ' ';
result << "reduces to" << ' ';
state = State::Normal;
}
else
{
static_assert(always_false_v<T>, "Unhandled Component variant");
}
};
size_t count = 0;
for (const TypePath::Component& component : path.components)
{
count++;
if (count == path.components.size())
last = true;
Luau::visit(strComponent, component);
}
switch (state)
{
case State::Property:
result << "` results in ";
break;
case State::PendingWhich:
// pending `which` becomes `is` if it's at the end
result << "is" << ' ';
break;
case State::PendingIs:
result << ' ' << "is" << ' ';
break;
case State::PendingAs:
result << ' ' << "as" << ' ';
break;
default:
break;
}
return result.str();
}
static bool traverse(TraversalState& state, const Path& path)
{
auto step = [&state](auto&& c)

View file

@ -24,6 +24,7 @@ const size_t kPageSize = sysconf(_SC_PAGESIZE);
#endif
#endif
#include <stdint.h>
#include <stdlib.h>
LUAU_FASTFLAG(DebugLuauFreezeArena)

View file

@ -18,6 +18,8 @@
#include <optional>
LUAU_FASTINT(LuauTypeInferRecursionLimit)
LUAU_FASTFLAGVARIABLE(LuauUnifyMetatableWithAny)
LUAU_FASTFLAG(LuauExtraFollows)
namespace Luau
{
@ -235,6 +237,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
@ -277,7 +283,7 @@ bool Unifier2::unifyFreeWithType(TypeId subTy, TypeId superTy)
if (superArgTail)
return doDefault();
const IntersectionType* upperBoundIntersection = get<IntersectionType>(subFree->upperBound);
const IntersectionType* upperBoundIntersection = get<IntersectionType>(FFlag::LuauExtraFollows ? upperBound : subFree->upperBound);
if (!upperBoundIntersection)
return doDefault();
@ -524,6 +530,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)
@ -661,7 +677,7 @@ struct FreeTypeSearcher : TypeVisitor
DenseHashSet<const void*> seenPositive{nullptr};
DenseHashSet<const void*> seenNegative{nullptr};
bool seenWithPolarity(const void* ty)
bool seenWithCurrentPolarity(const void* ty)
{
switch (polarity)
{
@ -703,7 +719,7 @@ struct FreeTypeSearcher : TypeVisitor
bool visit(TypeId ty) override
{
if (seenWithPolarity(ty))
if (seenWithCurrentPolarity(ty))
return false;
LUAU_ASSERT(ty);
@ -712,7 +728,7 @@ struct FreeTypeSearcher : TypeVisitor
bool visit(TypeId ty, const FreeType& ft) override
{
if (seenWithPolarity(ty))
if (seenWithCurrentPolarity(ty))
return false;
if (!subsumes(scope, ft.scope))
@ -737,7 +753,7 @@ struct FreeTypeSearcher : TypeVisitor
bool visit(TypeId ty, const TableType& tt) override
{
if (seenWithPolarity(ty))
if (seenWithCurrentPolarity(ty))
return false;
if ((tt.state == TableState::Free || tt.state == TableState::Unsealed) && subsumes(scope, tt.scope))
@ -783,7 +799,7 @@ struct FreeTypeSearcher : TypeVisitor
bool visit(TypeId ty, const FunctionType& ft) override
{
if (seenWithPolarity(ty))
if (seenWithCurrentPolarity(ty))
return false;
flip();
@ -802,7 +818,7 @@ struct FreeTypeSearcher : TypeVisitor
bool visit(TypePackId tp, const FreeTypePack& ftp) override
{
if (seenWithPolarity(tp))
if (seenWithCurrentPolarity(tp))
return false;
if (!subsumes(scope, ftp.scope))

View file

@ -1127,6 +1127,16 @@ public:
AstExpr* expr;
};
class AstTypeOptional : public AstType
{
public:
LUAU_RTTI(AstTypeOptional)
AstTypeOptional(const Location& location);
void visit(AstVisitor* visitor) override;
};
class AstTypeUnion : public AstType
{
public:
@ -1488,6 +1498,10 @@ public:
{
return visit(static_cast<AstType*>(node));
}
virtual bool visit(class AstTypeOptional* node)
{
return visit(static_cast<AstType*>(node));
}
virtual bool visit(class AstTypeUnion* node)
{
return visit(static_cast<AstType*>(node));

View file

@ -105,6 +105,20 @@ public:
Position closeBracketPosition;
};
class CstExprFunction : public CstNode
{
public:
LUAU_CST_RTTI(CstExprFunction)
CstExprFunction();
Position openGenericsPosition{0,0};
AstArray<Position> genericsCommaPositions;
Position closeGenericsPosition{0,0};
AstArray<Position> argsCommaPositions;
Position returnSpecifierPosition{0,0};
};
class CstExprTable : public CstNode
{
public:
@ -311,6 +325,17 @@ public:
Position equalsPosition;
};
class CstStatTypeFunction : public CstNode
{
public:
LUAU_CST_RTTI(CstStatTypeFunction)
CstStatTypeFunction(Position typeKeywordPosition, Position functionKeywordPosition);
Position typeKeywordPosition;
Position functionKeywordPosition;
};
class CstTypeReference : public CstNode
{
public:
@ -359,6 +384,32 @@ public:
bool isArray = false;
};
class CstTypeFunction : public CstNode
{
public:
LUAU_CST_RTTI(CstTypeFunction)
CstTypeFunction(
Position openGenericsPosition,
AstArray<Position> genericsCommaPositions,
Position closeGenericsPosition,
Position openArgsPosition,
AstArray<std::optional<Position>> argumentNameColonPositions,
AstArray<Position> argumentsCommaPositions,
Position closeArgsPosition,
Position returnArrowPosition
);
Position openGenericsPosition;
AstArray<Position> genericsCommaPositions;
Position closeGenericsPosition;
Position openArgsPosition;
AstArray<std::optional<Position>> argumentNameColonPositions;
AstArray<Position> argumentsCommaPositions;
Position closeArgsPosition;
Position returnArrowPosition;
};
class CstTypeTypeof : public CstNode
{
public:
@ -382,4 +433,26 @@ public:
unsigned int blockDepth;
};
class CstTypePackExplicit : public CstNode
{
public:
LUAU_CST_RTTI(CstTypePackExplicit)
CstTypePackExplicit(Position openParenthesesPosition, Position closeParenthesesPosition, AstArray<Position> commaPositions);
Position openParenthesesPosition;
Position closeParenthesesPosition;
AstArray<Position> commaPositions;
};
class CstTypePackGeneric : public CstNode
{
public:
LUAU_CST_RTTI(CstTypePackGeneric)
explicit CstTypePackGeneric(Position ellipsisPosition);
Position ellipsisPosition;
};
} // namespace Luau

View file

@ -155,7 +155,7 @@ private:
AstStat* parseTypeAlias(const Location& start, bool exported, Position typeKeywordPosition);
// type function Name ... end
AstStat* parseTypeFunction(const Location& start, bool exported);
AstStat* parseTypeFunction(const Location& start, bool exported, Position typeKeywordPosition);
AstDeclaredClassProp parseDeclaredClassMethod();
@ -192,7 +192,8 @@ private:
std::tuple<bool, Location, AstTypePack*> parseBindingList(
TempVector<Binding>& result,
bool allowDot3 = false,
TempVector<Position>* commaPositions = nullptr
AstArray<Position>* commaPositions = nullptr,
std::optional<Position> initialCommaPosition = std::nullopt
);
AstType* parseOptionalType();
@ -209,9 +210,14 @@ private:
// | `(' [TypeList] `)' `->` ReturnType
// Returns the variadic annotation, if it exists.
AstTypePack* parseTypeList(TempVector<AstType*>& result, TempVector<std::optional<AstArgumentName>>& resultNames);
AstTypePack* parseTypeList(
TempVector<AstType*>& result,
TempVector<std::optional<AstArgumentName>>& resultNames,
TempVector<Position>* commaPositions = nullptr,
TempVector<std::optional<Position>>* nameColonPositions = nullptr
);
std::optional<AstTypeList> parseOptionalReturnType();
std::optional<AstTypeList> parseOptionalReturnType(Position* returnSpecifierPosition = nullptr);
std::pair<Location, AstTypeList> parseReturnType();
struct TableIndexerResult
@ -305,7 +311,7 @@ private:
std::pair<AstArray<AstGenericType*>, AstArray<AstGenericTypePack*>> parseGenericTypeList(
bool withDefaultValues,
Position* openPosition = nullptr,
TempVector<Position>* commaPositions = nullptr,
AstArray<Position>* commaPositions = nullptr,
Position* closePosition = nullptr
);
@ -491,6 +497,7 @@ private:
std::vector<AstGenericTypePack*> scratchGenericTypePacks;
std::vector<std::optional<AstArgumentName>> scratchOptArgName;
std::vector<Position> scratchPosition;
std::vector<std::optional<Position>> scratchOptPosition;
std::string scratchData;
CstNodeMap cstNodeMap;

View file

@ -1069,6 +1069,16 @@ void AstTypeTypeof::visit(AstVisitor* visitor)
expr->visit(visitor);
}
AstTypeOptional::AstTypeOptional(const Location& location)
: AstType(ClassIndex(), location)
{
}
void AstTypeOptional::visit(AstVisitor* visitor)
{
visitor->visit(this);
}
AstTypeUnion::AstTypeUnion(const Location& location, const AstArray<AstType*>& types)
: AstType(ClassIndex(), location)
, types(types)

View file

@ -38,6 +38,10 @@ CstExprIndexExpr::CstExprIndexExpr(Position openBracketPosition, Position closeB
{
}
CstExprFunction::CstExprFunction() : CstNode(CstClassIndex())
{
}
CstExprTable::CstExprTable(const AstArray<Item>& items)
: CstNode(CstClassIndex())
, items(items)
@ -160,6 +164,13 @@ CstStatTypeAlias::CstStatTypeAlias(
{
}
CstStatTypeFunction::CstStatTypeFunction(Position typeKeywordPosition, Position functionKeywordPosition)
: CstNode(CstClassIndex())
, typeKeywordPosition(typeKeywordPosition)
, functionKeywordPosition(functionKeywordPosition)
{
}
CstTypeReference::CstTypeReference(
std::optional<Position> prefixPointPosition,
Position openParametersPosition,
@ -181,6 +192,28 @@ CstTypeTable::CstTypeTable(AstArray<Item> items, bool isArray)
{
}
CstTypeFunction::CstTypeFunction(
Position openGenericsPosition,
AstArray<Position> genericsCommaPositions,
Position closeGenericsPosition,
Position openArgsPosition,
AstArray<std::optional<Position>> argumentNameColonPositions,
AstArray<Position> argumentsCommaPositions,
Position closeArgsPosition,
Position returnArrowPosition
)
: CstNode(CstClassIndex())
, openGenericsPosition(openGenericsPosition)
, genericsCommaPositions(genericsCommaPositions)
, closeGenericsPosition(closeGenericsPosition)
, openArgsPosition(openArgsPosition)
, argumentNameColonPositions(argumentNameColonPositions)
, argumentsCommaPositions(argumentsCommaPositions)
, closeArgsPosition(closeArgsPosition)
, returnArrowPosition(returnArrowPosition)
{
}
CstTypeTypeof::CstTypeTypeof(Position openPosition, Position closePosition)
: CstNode(CstClassIndex())
, openPosition(openPosition)
@ -197,4 +230,18 @@ CstTypeSingletonString::CstTypeSingletonString(AstArray<char> sourceString, CstE
LUAU_ASSERT(quoteStyle != CstExprConstantString::QuotedInterp);
}
CstTypePackExplicit::CstTypePackExplicit(Position openParenthesesPosition, Position closeParenthesesPosition, AstArray<Position> commaPositions)
: CstNode(CstClassIndex())
, openParenthesesPosition(openParenthesesPosition)
, closeParenthesesPosition(closeParenthesesPosition)
, commaPositions(commaPositions)
{
}
CstTypePackGeneric::CstTypePackGeneric(Position ellipsisPosition)
: CstNode(CstClassIndex())
, ellipsisPosition(ellipsisPosition)
{
}
} // namespace Luau

View file

@ -8,9 +8,6 @@
#include <limits.h>
LUAU_FASTFLAGVARIABLE(LexerResumesFromPosition2)
LUAU_FASTFLAGVARIABLE(LexerFixInterpStringStart)
namespace Luau
{
@ -342,12 +339,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)
@ -793,7 +787,7 @@ Lexeme Lexer::readNext()
return Lexeme(Location(start, 1), '}');
}
return readInterpolatedStringSection(FFlag::LexerFixInterpStringStart ? start : position(), Lexeme::InterpStringMid, Lexeme::InterpStringEnd);
return readInterpolatedStringSection(start, Lexeme::InterpStringMid, Lexeme::InterpStringEnd);
}
case '=':

View file

@ -20,14 +20,14 @@ LUAU_FASTINTVARIABLE(LuauParseErrorLimit, 100)
LUAU_FASTFLAGVARIABLE(LuauSolverV2)
LUAU_FASTFLAGVARIABLE(LuauAllowComplexTypesInGenericParams)
LUAU_FASTFLAGVARIABLE(LuauErrorRecoveryForTableTypes)
LUAU_FASTFLAGVARIABLE(LuauErrorRecoveryForClassNames)
LUAU_FASTFLAGVARIABLE(LuauFixFunctionNameStartPosition)
LUAU_FASTFLAGVARIABLE(LuauExtendStatEndPosWithSemicolon)
LUAU_FASTFLAGVARIABLE(LuauStoreCSTData)
LUAU_FASTFLAGVARIABLE(LuauPreserveUnionIntersectionNodeForLeadingTokenSingleType)
LUAU_FASTFLAGVARIABLE(LuauAstTypeGroup2)
LUAU_FASTFLAGVARIABLE(LuauAstTypeGroup3)
LUAU_FASTFLAGVARIABLE(ParserNoErrorLimit)
LUAU_FASTFLAGVARIABLE(LuauFixDoBlockEndLocation)
LUAU_FASTFLAGVARIABLE(LuauParseOptionalAsNode)
namespace Luau
{
@ -648,16 +648,16 @@ AstStat* Parser::parseFor()
else
{
TempVector<Binding> names(scratchBinding);
TempVector<Position> varsCommaPosition(scratchPosition);
AstArray<Position> varsCommaPosition;
names.push_back(varname);
if (lexer.current().type == ',')
{
if (FFlag::LuauStoreCSTData && options.storeCstData)
{
varsCommaPosition.push_back(lexer.current().location.begin);
Position initialCommaPosition = lexer.current().location.begin;
nextLexeme();
parseBindingList(names, false, &varsCommaPosition);
parseBindingList(names, false, &varsCommaPosition, initialCommaPosition);
}
else
{
@ -702,7 +702,7 @@ AstStat* Parser::parseFor()
AstStatForIn* node =
allocator.alloc<AstStatForIn>(Location(start, end), copy(vars), copy(values), body, hasIn, inLocation, hasDo, matchDo.location);
if (options.storeCstData)
cstNodeMap[node] = allocator.alloc<CstStatForIn>(copy(varsCommaPosition), copy(valuesCommaPositions));
cstNodeMap[node] = allocator.alloc<CstStatForIn>(varsCommaPosition, copy(valuesCommaPositions));
return node;
}
else
@ -958,7 +958,7 @@ AstStat* Parser::parseLocal(const AstArray<AstAttr*>& attributes)
matchRecoveryStopOnToken['=']++;
TempVector<Binding> names(scratchBinding);
TempVector<Position> varsCommaPositions(scratchPosition);
AstArray<Position> varsCommaPositions;
if (FFlag::LuauStoreCSTData && options.storeCstData)
parseBindingList(names, false, &varsCommaPositions);
else
@ -991,7 +991,7 @@ AstStat* Parser::parseLocal(const AstArray<AstAttr*>& attributes)
{
AstStatLocal* node = allocator.alloc<AstStatLocal>(Location(start, end), copy(vars), copy(values), equalsSignLocation);
if (options.storeCstData)
cstNodeMap[node] = allocator.alloc<CstStatLocal>(copy(varsCommaPositions), copy(valuesCommaPositions));
cstNodeMap[node] = allocator.alloc<CstStatLocal>(varsCommaPositions, copy(valuesCommaPositions));
return node;
}
else
@ -1034,7 +1034,7 @@ AstStat* Parser::parseTypeAlias(const Location& start, bool exported, Position t
{
// parsing a type function
if (lexer.current().type == Lexeme::ReservedFunction)
return parseTypeFunction(start, exported);
return parseTypeFunction(start, exported, typeKeywordPosition);
// parsing a type alias
@ -1047,7 +1047,7 @@ AstStat* Parser::parseTypeAlias(const Location& start, bool exported, Position t
name = Name(nameError, lexer.current().location);
Position genericsOpenPosition{0, 0};
TempVector<Position> genericsCommaPositions(scratchPosition);
AstArray<Position> genericsCommaPositions;
Position genericsClosePosition{0, 0};
auto [generics, genericPacks] = FFlag::LuauStoreCSTData && options.storeCstData
? parseGenericTypeList(
@ -1066,7 +1066,7 @@ AstStat* Parser::parseTypeAlias(const Location& start, bool exported, Position t
allocator.alloc<AstStatTypeAlias>(Location(start, type->location), name->name, name->location, generics, genericPacks, type, exported);
if (options.storeCstData)
cstNodeMap[node] = allocator.alloc<CstStatTypeAlias>(
typeKeywordPosition, genericsOpenPosition, copy(genericsCommaPositions), genericsClosePosition, equalsPosition
typeKeywordPosition, genericsOpenPosition, genericsCommaPositions, genericsClosePosition, equalsPosition
);
return node;
}
@ -1077,7 +1077,7 @@ AstStat* Parser::parseTypeAlias(const Location& start, bool exported, Position t
}
// type function Name `(' arglist `)' `=' funcbody `end'
AstStat* Parser::parseTypeFunction(const Location& start, bool exported)
AstStat* Parser::parseTypeFunction(const Location& start, bool exported, Position typeKeywordPosition)
{
Lexeme matchFn = lexer.current();
nextLexeme();
@ -1098,7 +1098,18 @@ AstStat* Parser::parseTypeFunction(const Location& start, bool exported)
matchRecoveryStopOnToken[Lexeme::ReservedEnd]--;
return allocator.alloc<AstStatTypeFunction>(Location(start, body->location), fnName->name, fnName->location, body, exported);
if (FFlag::LuauStoreCSTData)
{
AstStatTypeFunction* node =
allocator.alloc<AstStatTypeFunction>(Location(start, body->location), fnName->name, fnName->location, body, exported);
if (options.storeCstData)
cstNodeMap[node] = allocator.alloc<CstStatTypeFunction>(typeKeywordPosition, matchFn.location.begin);
return node;
}
else
{
return allocator.alloc<AstStatTypeFunction>(Location(start, body->location), fnName->name, fnName->location, body, exported);
}
}
AstDeclaredClassProp Parser::parseDeclaredClassMethod()
@ -1307,30 +1318,17 @@ AstStat* Parser::parseDeclaration(const Location& start, const AstArray<AstAttr*
}
else
{
if (FFlag::LuauErrorRecoveryForClassNames)
{
Location propStart = lexer.current().location;
std::optional<Name> propName = parseNameOpt("property name");
Location propStart = lexer.current().location;
std::optional<Name> propName = parseNameOpt("property name");
if (!propName)
break;
if (!propName)
break;
expectAndConsume(':', "property type annotation");
AstType* propType = parseType();
props.push_back(
AstDeclaredClassProp{propName->name, propName->location, propType, false, Location(propStart, lexer.previousLocation())}
);
}
else
{
Location propStart = lexer.current().location;
Name propName = parseName("property name");
expectAndConsume(':', "property type annotation");
AstType* propType = parseType();
props.push_back(
AstDeclaredClassProp{propName.name, propName.location, propType, false, Location(propStart, lexer.previousLocation())}
);
}
expectAndConsume(':', "property type annotation");
AstType* propType = parseType();
props.push_back(
AstDeclaredClassProp{propName->name, propName->location, propType, false, Location(propStart, lexer.previousLocation())}
);
}
}
@ -1453,7 +1451,15 @@ std::pair<AstExprFunction*, AstLocal*> Parser::parseFunctionBody(
{
Location start = matchFunction.location;
auto [generics, genericPacks] = parseGenericTypeList(/* withDefaultValues= */ false);
auto* cstNode = FFlag::LuauStoreCSTData && options.storeCstData ? allocator.alloc<CstExprFunction>() : nullptr;
auto [generics, genericPacks] = FFlag::LuauStoreCSTData && cstNode ? parseGenericTypeList(
/* withDefaultValues= */ false,
&cstNode->openGenericsPosition,
&cstNode->genericsCommaPositions,
&cstNode->closeGenericsPosition
)
: parseGenericTypeList(/* withDefaultValues= */ false);
MatchLexeme matchParen = lexer.current();
expectAndConsume('(', "function");
@ -1478,7 +1484,8 @@ std::pair<AstExprFunction*, AstLocal*> Parser::parseFunctionBody(
AstTypePack* varargAnnotation = nullptr;
if (lexer.current().type != ')')
std::tie(vararg, varargLocation, varargAnnotation) = parseBindingList(args, /* allowDot3= */ true);
std::tie(vararg, varargLocation, varargAnnotation) =
parseBindingList(args, /* allowDot3= */ true, cstNode ? &cstNode->argsCommaPositions : nullptr);
std::optional<Location> argLocation;
@ -1490,7 +1497,7 @@ std::pair<AstExprFunction*, AstLocal*> Parser::parseFunctionBody(
if (FFlag::LuauErrorRecoveryForTableTypes)
matchRecoveryStopOnToken[')']--;
std::optional<AstTypeList> typelist = parseOptionalReturnType();
std::optional<AstTypeList> typelist = parseOptionalReturnType(cstNode ? &cstNode->returnSpecifierPosition : nullptr);
AstLocal* funLocal = nullptr;
@ -1517,8 +1524,9 @@ std::pair<AstExprFunction*, AstLocal*> Parser::parseFunctionBody(
bool hasEnd = expectMatchEndAndConsume(Lexeme::ReservedEnd, matchFunction);
body->hasEnd = hasEnd;
return {
allocator.alloc<AstExprFunction>(
if (FFlag::LuauStoreCSTData)
{
AstExprFunction* node = allocator.alloc<AstExprFunction>(
Location(start, end),
attributes,
generics,
@ -1533,9 +1541,34 @@ std::pair<AstExprFunction*, AstLocal*> Parser::parseFunctionBody(
typelist,
varargAnnotation,
argLocation
),
funLocal
};
);
if (options.storeCstData)
cstNodeMap[node] = cstNode;
return {node, funLocal};
}
else
{
return {
allocator.alloc<AstExprFunction>(
Location(start, end),
attributes,
generics,
genericPacks,
self,
vars,
vararg,
varargLocation,
body,
functionStack.size(),
debugname,
typelist,
varargAnnotation,
argLocation
),
funLocal
};
}
}
// explist ::= {exp `,'} exp
@ -1573,8 +1606,13 @@ Parser::Binding Parser::parseBinding()
}
// bindinglist ::= (binding | `...') [`,' bindinglist]
std::tuple<bool, Location, AstTypePack*> Parser::parseBindingList(TempVector<Binding>& result, bool allowDot3, TempVector<Position>* commaPositions)
std::tuple<bool, Location, AstTypePack*> Parser::parseBindingList(TempVector<Binding>& result, bool allowDot3, AstArray<Position>* commaPositions, std::optional<Position> initialCommaPosition)
{
TempVector<Position> localCommaPositions(scratchPosition);
if (FFlag::LuauStoreCSTData && commaPositions && initialCommaPosition)
localCommaPositions.push_back(*initialCommaPosition);
while (true)
{
if (lexer.current().type == Lexeme::Dot3 && allowDot3)
@ -1589,6 +1627,9 @@ std::tuple<bool, Location, AstTypePack*> Parser::parseBindingList(TempVector<Bin
tailAnnotation = parseVariadicArgumentTypePack();
}
if (FFlag::LuauStoreCSTData && commaPositions)
*commaPositions = copy(localCommaPositions);
return {true, varargLocation, tailAnnotation};
}
@ -1597,10 +1638,13 @@ std::tuple<bool, Location, AstTypePack*> Parser::parseBindingList(TempVector<Bin
if (lexer.current().type != ',')
break;
if (FFlag::LuauStoreCSTData && commaPositions)
commaPositions->push_back(lexer.current().location.begin);
localCommaPositions.push_back(lexer.current().location.begin);
nextLexeme();
}
if (FFlag::LuauStoreCSTData && commaPositions)
*commaPositions = copy(localCommaPositions);
return {false, Location(), nullptr};
}
@ -1616,7 +1660,12 @@ AstType* Parser::parseOptionalType()
}
// TypeList ::= Type [`,' TypeList] | ...Type
AstTypePack* Parser::parseTypeList(TempVector<AstType*>& result, TempVector<std::optional<AstArgumentName>>& resultNames)
AstTypePack* Parser::parseTypeList(
TempVector<AstType*>& result,
TempVector<std::optional<AstArgumentName>>& resultNames,
TempVector<Position>* commaPositions,
TempVector<std::optional<Position>>* nameColonPositions
)
{
while (true)
{
@ -1628,22 +1677,33 @@ AstTypePack* Parser::parseTypeList(TempVector<AstType*>& result, TempVector<std:
// Fill in previous argument names with empty slots
while (resultNames.size() < result.size())
resultNames.push_back({});
if (FFlag::LuauStoreCSTData && nameColonPositions)
{
while (nameColonPositions->size() < result.size())
nameColonPositions->push_back({});
}
resultNames.push_back(AstArgumentName{AstName(lexer.current().name), lexer.current().location});
nextLexeme();
if (FFlag::LuauStoreCSTData && nameColonPositions)
nameColonPositions->push_back(lexer.current().location.begin);
expectAndConsume(':');
}
else if (!resultNames.empty())
{
// If we have a type with named arguments, provide elements for all types
resultNames.push_back({});
if (FFlag::LuauStoreCSTData && nameColonPositions)
nameColonPositions->push_back({});
}
result.push_back(parseType());
if (lexer.current().type != ',')
break;
if (FFlag::LuauStoreCSTData && commaPositions)
commaPositions->push_back(lexer.current().location.begin);
nextLexeme();
if (lexer.current().type == ')')
@ -1656,13 +1716,15 @@ AstTypePack* Parser::parseTypeList(TempVector<AstType*>& result, TempVector<std:
return nullptr;
}
std::optional<AstTypeList> Parser::parseOptionalReturnType()
std::optional<AstTypeList> Parser::parseOptionalReturnType(Position* returnSpecifierPosition)
{
if (lexer.current().type == ':' || lexer.current().type == Lexeme::SkinnyArrow)
{
if (lexer.current().type == Lexeme::SkinnyArrow)
report(lexer.current().location, "Function return type annotations are written after ':' instead of '->'");
if (FFlag::LuauStoreCSTData && returnSpecifierPosition)
*returnSpecifierPosition = lexer.current().location.begin;
nextLexeme();
unsigned int oldRecursionCount = recursionCounter;
@ -1732,11 +1794,13 @@ std::pair<Location, AstTypeList> Parser::parseReturnType()
if (lexer.current().type != Lexeme::SkinnyArrow && resultNames.empty())
{
// If it turns out that it's just '(A)', it's possible that there are unions/intersections to follow, so fold over it.
if (FFlag::LuauAstTypeGroup2)
if (FFlag::LuauAstTypeGroup3)
{
if (result.size() == 1 && varargAnnotation == nullptr)
if (result.size() == 1)
{
AstType* returnType = parseTypeSuffix(allocator.alloc<AstTypeGroup>(location, result[0]), begin.location);
// TODO(CLI-140667): stop parsing type suffix when varargAnnotation != nullptr - this should be a parse error
AstType* inner = varargAnnotation == nullptr ? allocator.alloc<AstTypeGroup>(location, result[0]) : result[0];
AstType* returnType = parseTypeSuffix(inner, begin.location);
// If parseType parses nothing, then returnType->location.end only points at the last non-type-pack
// type to successfully parse. We need the span of the whole annotation.
@ -2028,7 +2092,14 @@ AstTypeOrPack Parser::parseFunctionType(bool allowPack, const AstArray<AstAttr*>
Lexeme begin = lexer.current();
auto [generics, genericPacks] = parseGenericTypeList(/* withDefaultValues= */ false);
Position genericsOpenPosition{0, 0};
AstArray<Position> genericsCommaPositions;
Position genericsClosePosition{0, 0};
auto [generics, genericPacks] = FFlag::LuauStoreCSTData && options.storeCstData
? parseGenericTypeList(
/* withDefaultValues= */ false, &genericsOpenPosition, &genericsCommaPositions, &genericsClosePosition
)
: parseGenericTypeList(/* withDefaultValues= */ false);
Lexeme parameterStart = lexer.current();
@ -2038,10 +2109,17 @@ AstTypeOrPack Parser::parseFunctionType(bool allowPack, const AstArray<AstAttr*>
TempVector<AstType*> params(scratchType);
TempVector<std::optional<AstArgumentName>> names(scratchOptArgName);
TempVector<std::optional<Position>> nameColonPositions(scratchOptPosition);
TempVector<Position> argCommaPositions(scratchPosition);
AstTypePack* varargAnnotation = nullptr;
if (lexer.current().type != ')')
varargAnnotation = parseTypeList(params, names);
{
if (FFlag::LuauStoreCSTData && options.storeCstData)
varargAnnotation = parseTypeList(params, names, &argCommaPositions, &nameColonPositions);
else
varargAnnotation = parseTypeList(params, names);
}
Location closeArgsLocation = lexer.current().location;
expectMatchAndConsume(')', parameterStart, true);
@ -2059,10 +2137,23 @@ AstTypeOrPack Parser::parseFunctionType(bool allowPack, const AstArray<AstAttr*>
if (params.size() == 1 && !varargAnnotation && !forceFunctionType && !returnTypeIntroducer)
{
if (allowPack)
return {{}, allocator.alloc<AstTypePackExplicit>(begin.location, AstTypeList{paramTypes, nullptr})};
{
if (FFlag::LuauStoreCSTData)
{
AstTypePackExplicit* node = allocator.alloc<AstTypePackExplicit>(begin.location, AstTypeList{paramTypes, nullptr});
if (options.storeCstData)
cstNodeMap[node] =
allocator.alloc<CstTypePackExplicit>(parameterStart.location.begin, closeArgsLocation.begin, copy(argCommaPositions));
return {{}, node};
}
else
{
return {{}, allocator.alloc<AstTypePackExplicit>(begin.location, AstTypeList{paramTypes, nullptr})};
}
}
else
{
if (FFlag::LuauAstTypeGroup2)
if (FFlag::LuauAstTypeGroup3)
return {allocator.alloc<AstTypeGroup>(Location(parameterStart.location, closeArgsLocation), params[0]), {}};
else
return {params[0], {}};
@ -2070,11 +2161,46 @@ AstTypeOrPack Parser::parseFunctionType(bool allowPack, const AstArray<AstAttr*>
}
if (!forceFunctionType && !returnTypeIntroducer && allowPack)
return {{}, allocator.alloc<AstTypePackExplicit>(begin.location, AstTypeList{paramTypes, varargAnnotation})};
{
if (FFlag::LuauStoreCSTData)
{
AstTypePackExplicit* node = allocator.alloc<AstTypePackExplicit>(begin.location, AstTypeList{paramTypes, varargAnnotation});
if (options.storeCstData)
cstNodeMap[node] =
allocator.alloc<CstTypePackExplicit>(parameterStart.location.begin, closeArgsLocation.begin, copy(argCommaPositions));
return {{}, node};
}
else
{
return {{}, allocator.alloc<AstTypePackExplicit>(begin.location, AstTypeList{paramTypes, varargAnnotation})};
}
}
AstArray<std::optional<AstArgumentName>> paramNames = copy(names);
return {parseFunctionTypeTail(begin, attributes, generics, genericPacks, paramTypes, paramNames, varargAnnotation), {}};
if (FFlag::LuauStoreCSTData)
{
Position returnArrowPosition = lexer.current().location.begin;
AstType* node = parseFunctionTypeTail(begin, attributes, generics, genericPacks, paramTypes, paramNames, varargAnnotation);
if (options.storeCstData && node->is<AstTypeFunction>())
{
cstNodeMap[node] = allocator.alloc<CstTypeFunction>(
genericsOpenPosition,
genericsCommaPositions,
genericsClosePosition,
parameterStart.location.begin,
copy(nameColonPositions),
copy(argCommaPositions),
closeArgsLocation.begin,
returnArrowPosition
);
}
return {node, {}};
}
else
{
return {parseFunctionTypeTail(begin, attributes, generics, genericPacks, paramTypes, paramNames, varargAnnotation), {}};
}
}
AstType* Parser::parseFunctionTypeTail(
@ -2136,7 +2262,9 @@ AstType* Parser::parseTypeSuffix(AstType* type, const Location& begin)
bool isUnion = false;
bool isIntersection = false;
bool hasOptional = false;
// Clip with FFlag::LuauParseOptionalAsNode
bool hasOptional_DEPRECATED = false;
unsigned int optionalCount = 0;
Location location = begin;
@ -2160,11 +2288,19 @@ AstType* Parser::parseTypeSuffix(AstType* type, const Location& begin)
Location loc = lexer.current().location;
nextLexeme();
if (!hasOptional)
parts.push_back(allocator.alloc<AstTypeReference>(loc, std::nullopt, nameNil, std::nullopt, loc));
if (FFlag::LuauParseOptionalAsNode)
{
parts.push_back(allocator.alloc<AstTypeOptional>(Location(loc)));
optionalCount++;
}
else
{
if (!hasOptional_DEPRECATED)
parts.push_back(allocator.alloc<AstTypeReference>(loc, std::nullopt, nameNil, std::nullopt, loc));
}
isUnion = true;
hasOptional = true;
hasOptional_DEPRECATED = true;
}
else if (c == '&')
{
@ -2184,8 +2320,16 @@ AstType* Parser::parseTypeSuffix(AstType* type, const Location& begin)
else
break;
if (parts.size() > unsigned(FInt::LuauTypeLengthLimit) + hasOptional)
ParseError::raise(parts.back()->location, "Exceeded allowed type length; simplify your type annotation to make the code compile");
if (FFlag::LuauParseOptionalAsNode)
{
if (parts.size() > unsigned(FInt::LuauTypeLengthLimit) + optionalCount)
ParseError::raise(parts.back()->location, "Exceeded allowed type length; simplify your type annotation to make the code compile");
}
else
{
if (parts.size() > unsigned(FInt::LuauTypeLengthLimit) + hasOptional_DEPRECATED)
ParseError::raise(parts.back()->location, "Exceeded allowed type length; simplify your type annotation to make the code compile");
}
}
if (FFlag::LuauPreserveUnionIntersectionNodeForLeadingTokenSingleType)
@ -2477,7 +2621,17 @@ AstTypePack* Parser::parseVariadicArgumentTypePack()
// This will not fail because of the lookahead guard.
expectAndConsume(Lexeme::Dot3, "generic type pack annotation");
return allocator.alloc<AstTypePackGeneric>(Location(name.location, end), name.name);
if (FFlag::LuauStoreCSTData)
{
AstTypePackGeneric* node = allocator.alloc<AstTypePackGeneric>(Location(name.location, end), name.name);
if (options.storeCstData)
cstNodeMap[node] = allocator.alloc<CstTypePackGeneric>(end.begin);
return node;
}
else
{
return allocator.alloc<AstTypePackGeneric>(Location(name.location, end), name.name);
}
}
// Variadic: T
else
@ -2505,7 +2659,17 @@ AstTypePack* Parser::parseTypePack()
// This will not fail because of the lookahead guard.
expectAndConsume(Lexeme::Dot3, "generic type pack annotation");
return allocator.alloc<AstTypePackGeneric>(Location(name.location, end), name.name);
if (FFlag::LuauStoreCSTData)
{
AstTypePackGeneric* node = allocator.alloc<AstTypePackGeneric>(Location(name.location, end), name.name);
if (options.storeCstData)
cstNodeMap[node] = allocator.alloc<CstTypePackGeneric>(end.begin);
return node;
}
else
{
return allocator.alloc<AstTypePackGeneric>(Location(name.location, end), name.name);
}
}
// TODO: shouldParseTypePack can be removed and parseTypePack can be called unconditionally instead
@ -3360,12 +3524,13 @@ Parser::Name Parser::parseIndexName(const char* context, const Position& previou
std::pair<AstArray<AstGenericType*>, AstArray<AstGenericTypePack*>> Parser::parseGenericTypeList(
bool withDefaultValues,
Position* openPosition,
TempVector<Position>* commaPositions,
AstArray<Position>* commaPositions,
Position* closePosition
)
{
TempVector<AstGenericType*> names{scratchGenericTypes};
TempVector<AstGenericTypePack*> namePacks{scratchGenericTypePacks};
TempVector<Position> localCommaPositions{scratchPosition};
if (lexer.current().type == '<')
{
@ -3495,7 +3660,7 @@ std::pair<AstArray<AstGenericType*>, AstArray<AstGenericTypePack*>> Parser::pars
if (lexer.current().type == ',')
{
if (FFlag::LuauStoreCSTData && commaPositions)
commaPositions->push_back(lexer.current().location.begin);
localCommaPositions.push_back(lexer.current().location.begin);
nextLexeme();
if (lexer.current().type == '>')
@ -3513,6 +3678,9 @@ std::pair<AstArray<AstGenericType*>, AstArray<AstGenericTypePack*>> Parser::pars
expectMatchAndConsume('>', begin);
}
if (FFlag::LuauStoreCSTData && commaPositions)
*commaPositions = copy(localCommaPositions);
AstArray<AstGenericType*> generics = copy(names);
AstArray<AstGenericTypePack*> genericPacks = copy(namePacks);
return {generics, genericPacks};
@ -3572,7 +3740,7 @@ AstArray<AstTypeOrPack> Parser::parseTypeParams(Position* openingPosition, TempV
// the next lexeme is one that follows a type
// (&, |, ?), then assume that this was actually a
// parenthesized type.
if (FFlag::LuauAstTypeGroup2)
if (FFlag::LuauAstTypeGroup3)
{
auto parenthesizedType = explicitTypePack->typeList.types.data[0];
parameters.push_back(

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

View file

@ -26,6 +26,8 @@ LUAU_FASTINTVARIABLE(LuauCompileInlineThreshold, 25)
LUAU_FASTINTVARIABLE(LuauCompileInlineThresholdMaxBoost, 300)
LUAU_FASTINTVARIABLE(LuauCompileInlineDepth, 5)
LUAU_FASTFLAGVARIABLE(LuauSeparateCompilerTypeInfo)
namespace Luau
{
@ -4269,20 +4271,40 @@ void compileOrThrow(BytecodeBuilder& bytecode, const ParseResult& parseResult, c
}
// computes type information for all functions based on type annotations
if (options.typeInfoLevel >= 1)
buildTypeMap(
compiler.functionTypes,
compiler.localTypes,
compiler.exprTypes,
root,
options.vectorType,
compiler.userdataTypes,
compiler.builtinTypes,
compiler.builtins,
compiler.globals,
options.libraryMemberTypeCb,
bytecode
);
if (FFlag::LuauSeparateCompilerTypeInfo)
{
if (options.typeInfoLevel >= 1 || options.optimizationLevel >= 2)
buildTypeMap(
compiler.functionTypes,
compiler.localTypes,
compiler.exprTypes,
root,
options.vectorType,
compiler.userdataTypes,
compiler.builtinTypes,
compiler.builtins,
compiler.globals,
options.libraryMemberTypeCb,
bytecode
);
}
else
{
if (options.typeInfoLevel >= 1)
buildTypeMap(
compiler.functionTypes,
compiler.localTypes,
compiler.exprTypes,
root,
options.vectorType,
compiler.userdataTypes,
compiler.builtinTypes,
compiler.builtins,
compiler.globals,
options.libraryMemberTypeCb,
bytecode
);
}
for (AstExprFunction* expr : functions)
{

View file

@ -125,6 +125,10 @@ static LuauBytecodeType getType(
{
return getType(group->type, generics, typeAliases, resolveAliases, hostVectorType, userdataTypes, bytecode);
}
else if (const AstTypeOptional* optional = ty->as<AstTypeOptional>())
{
return LBC_TYPE_NIL;
}
return LBC_TYPE_ANY;
}

View file

@ -266,6 +266,7 @@ target_sources(Luau.Analysis PRIVATE
Analysis/src/EmbeddedBuiltinDefinitions.cpp
Analysis/src/Error.cpp
Analysis/src/EqSatSimplification.cpp
Analysis/src/FileResolver.cpp
Analysis/src/FragmentAutocomplete.cpp
Analysis/src/Frontend.cpp
Analysis/src/Generalization.cpp

View file

@ -327,9 +327,12 @@ LUA_API void lua_setuserdatadtor(lua_State* L, int tag, lua_Destructor dtor);
LUA_API lua_Destructor lua_getuserdatadtor(lua_State* L, int tag);
// alternative access for metatables already registered with luaL_newmetatable
LUA_API void lua_setuserdatametatable(lua_State* L, int tag, int idx);
// used by lua_newuserdatataggedwithmetatable to create tagged userdata with the associated metatable assigned
LUA_API void lua_setuserdatametatable(lua_State* L, int tag);
LUA_API void lua_getuserdatametatable(lua_State* L, int tag);
LUA_API void lua_setuserdatametatable_DEPRECATED(lua_State* L, int tag, int idx); // Deprecated for incorrect behavior with 'idx != -1'
LUA_API void lua_setlightuserdataname(lua_State* L, int tag, const char* name);
LUA_API const char* lua_getlightuserdataname(lua_State* L, int tag);

View file

@ -1470,7 +1470,16 @@ lua_Destructor lua_getuserdatadtor(lua_State* L, int tag)
return L->global->udatagc[tag];
}
void lua_setuserdatametatable(lua_State* L, int tag, int idx)
void lua_setuserdatametatable(lua_State* L, int tag)
{
api_check(L, unsigned(tag) < LUA_UTAG_LIMIT);
api_check(L, !L->global->udatamt[tag]); // reassignment not supported
api_check(L, ttistable(L->top - 1));
L->global->udatamt[tag] = hvalue(L->top - 1);
L->top--;
}
void lua_setuserdatametatable_DEPRECATED(lua_State* L, int tag, int idx)
{
api_check(L, unsigned(tag) < LUA_UTAG_LIMIT);
api_check(L, !L->global->udatamt[tag]); // reassignment not supported

View file

@ -20,7 +20,7 @@ LUAU_FASTFLAG(DebugLuauMagicTypes)
LUAU_FASTFLAG(StudioReportLuauAny2)
LUAU_FASTFLAG(LuauTrackInteriorFreeTypesOnScope)
LUAU_FASTFLAG(LuauStoreCSTData)
LUAU_FASTFLAG(LuauAstTypeGroup2)
LUAU_FASTFLAG(LuauAstTypeGroup3)
LUAU_FASTFLAG(LuauDeferBidirectionalInferenceForTableAssignment)
LUAU_FASTFLAG(LuauSkipNoRefineDuringRefinement)
@ -53,9 +53,9 @@ type A = (number, string) -> ...any
ModulePtr module = frontend.moduleResolver.getModule("game/Gui/Modules/A");
LUAU_ASSERT(module->ats.typeInfo.size() == 1);
LUAU_ASSERT(module->ats.typeInfo[0].code == Pattern::Alias);
LUAU_ASSERT(module->ats.typeInfo[0].node == "type A = (number, string)->( ...any)");
REQUIRE(module->ats.typeInfo.size() == 1);
CHECK(module->ats.typeInfo[0].code == Pattern::Alias);
CHECK(module->ats.typeInfo[0].node == "type A = (number, string)->( ...any)");
}
TEST_CASE_FIXTURE(ATSFixture, "export_alias")
@ -74,23 +74,23 @@ export type t8<t8> = t0 &(<t0 ...>(true | any)->(''))
ModulePtr module = frontend.moduleResolver.getModule("game/Gui/Modules/A");
LUAU_ASSERT(module->ats.typeInfo.size() == 1);
LUAU_ASSERT(module->ats.typeInfo[0].code == Pattern::Alias);
if (FFlag::LuauStoreCSTData && FFlag::LuauAstTypeGroup2)
REQUIRE(module->ats.typeInfo.size() == 1);
CHECK(module->ats.typeInfo[0].code == Pattern::Alias);
if (FFlag::LuauStoreCSTData && FFlag::LuauAstTypeGroup3)
{
LUAU_ASSERT(module->ats.typeInfo[0].node == "export type t8<t8> = t0& (<t0...>( true | any)->(''))");
CHECK(module->ats.typeInfo[0].node == "export type t8<t8> = t0& (<t0...>( true | any)->(''))");
}
else if (FFlag::LuauStoreCSTData)
{
LUAU_ASSERT(module->ats.typeInfo[0].node == "export type t8<t8> = t0 &(<t0...>( true | any)->(''))");
CHECK(module->ats.typeInfo[0].node == "export type t8<t8> = t0 &(<t0...>( true | any)->(''))");
}
else if (FFlag::LuauAstTypeGroup2)
else if (FFlag::LuauAstTypeGroup3)
{
LUAU_ASSERT(module->ats.typeInfo[0].node == "export type t8<t8> = t0& (<t0 ...>(true | any)->(''))");
CHECK(module->ats.typeInfo[0].node == "export type t8<t8> = t0& (<t0 ...>(true | any)->(''))");
}
else
{
LUAU_ASSERT(module->ats.typeInfo[0].node == "export type t8<t8> = t0 &(<t0 ...>(true | any)->(''))");
CHECK(module->ats.typeInfo[0].node == "export type t8<t8> = t0 &(<t0 ...>(true | any)->(''))");
}
}
@ -115,9 +115,9 @@ end
ModulePtr module = frontend.moduleResolver.getModule("game/Gui/Modules/A");
LUAU_ASSERT(module->ats.typeInfo.size() == 3);
LUAU_ASSERT(module->ats.typeInfo[1].code == Pattern::TypePk);
LUAU_ASSERT(
REQUIRE(module->ats.typeInfo.size() == 3);
CHECK(module->ats.typeInfo[1].code == Pattern::TypePk);
CHECK(
module->ats.typeInfo[0].node ==
"local function fallible(t: number): ...any\n if t > 0 then\n return true, t\n end\n return false, 'must be positive'\nend"
);
@ -145,7 +145,7 @@ end
ModulePtr module = frontend.moduleResolver.getModule("game/Gui/Modules/A");
LUAU_ASSERT(module->ats.typeInfo.size() == 0);
REQUIRE(module->ats.typeInfo.size() == 0);
}
TEST_CASE_FIXTURE(ATSFixture, "var_typepack_any_gen_table")
@ -164,9 +164,9 @@ type Pair<T> = {first: T, second: any}
ModulePtr module = frontend.moduleResolver.getModule("game/Gui/Modules/A");
LUAU_ASSERT(module->ats.typeInfo.size() == 1);
LUAU_ASSERT(module->ats.typeInfo[0].code == Pattern::Alias);
LUAU_ASSERT(module->ats.typeInfo[0].node == "type Pair<T> = {first: T, second: any}");
REQUIRE(module->ats.typeInfo.size() == 1);
CHECK(module->ats.typeInfo[0].code == Pattern::Alias);
CHECK(module->ats.typeInfo[0].node == "type Pair<T> = {first: T, second: any}");
}
TEST_CASE_FIXTURE(ATSFixture, "assign_uneq")
@ -190,7 +190,7 @@ local x, y, z = greetings("Dibri") -- mismatch
LUAU_REQUIRE_ERROR_COUNT(1, result1);
ModulePtr module = frontend.moduleResolver.getModule("game/Gui/Modules/B");
LUAU_ASSERT(module->ats.typeInfo.size() == 0);
REQUIRE(module->ats.typeInfo.size() == 0);
}
TEST_CASE_FIXTURE(ATSFixture, "var_typepack_any_gen")
@ -210,9 +210,9 @@ type Pair<T> = (boolean, T) -> ...any
ModulePtr module = frontend.moduleResolver.getModule("game/Gui/Modules/A");
LUAU_ASSERT(module->ats.typeInfo.size() == 1);
LUAU_ASSERT(module->ats.typeInfo[0].code == Pattern::Alias);
LUAU_ASSERT(module->ats.typeInfo[0].node == "type Pair<T> = (boolean, T)->( ...any)");
REQUIRE(module->ats.typeInfo.size() == 1);
CHECK(module->ats.typeInfo[0].code == Pattern::Alias);
CHECK(module->ats.typeInfo[0].node == "type Pair<T> = (boolean, T)->( ...any)");
}
TEST_CASE_FIXTURE(ATSFixture, "typeof_any_in_func")
@ -234,9 +234,9 @@ TEST_CASE_FIXTURE(ATSFixture, "typeof_any_in_func")
ModulePtr module = frontend.moduleResolver.getModule("game/Gui/Modules/A");
LUAU_ASSERT(module->ats.typeInfo.size() == 2);
LUAU_ASSERT(module->ats.typeInfo[0].code == Pattern::VarAnnot);
LUAU_ASSERT(module->ats.typeInfo[0].node == "local function f()\n local a: any = 1\n local b: typeof(a) = 1\n end");
REQUIRE(module->ats.typeInfo.size() == 2);
CHECK(module->ats.typeInfo[0].code == Pattern::VarAnnot);
CHECK(module->ats.typeInfo[0].node == "local function f()\n local a: any = 1\n local b: typeof(a) = 1\n end");
}
TEST_CASE_FIXTURE(ATSFixture, "generic_types")
@ -264,9 +264,9 @@ foo(addNumbers)
ModulePtr module = frontend.moduleResolver.getModule("game/Gui/Modules/A");
LUAU_ASSERT(module->ats.typeInfo.size() == 3);
LUAU_ASSERT(module->ats.typeInfo[1].code == Pattern::FuncApp);
LUAU_ASSERT(module->ats.typeInfo[0].node == "local function foo<A>(a: (...A)->( any),...: A)\n return a(...)\nend");
REQUIRE(module->ats.typeInfo.size() == 3);
CHECK(module->ats.typeInfo[1].code == Pattern::FuncApp);
CHECK(module->ats.typeInfo[0].node == "local function foo<A>(a: (...A)->( any),...: A)\n return a(...)\nend");
}
TEST_CASE_FIXTURE(ATSFixture, "no_annot")
@ -285,7 +285,7 @@ local character = script.Parent
ModulePtr module = frontend.moduleResolver.getModule("game/Gui/Modules/A");
LUAU_ASSERT(module->ats.typeInfo.size() == 0);
REQUIRE(module->ats.typeInfo.size() == 0);
}
TEST_CASE_FIXTURE(ATSFixture, "if_any")
@ -314,9 +314,9 @@ end
ModulePtr module = frontend.moduleResolver.getModule("game/Gui/Modules/A");
LUAU_ASSERT(module->ats.typeInfo.size() == 1);
LUAU_ASSERT(module->ats.typeInfo[0].code == Pattern::FuncArg);
LUAU_ASSERT(
REQUIRE(module->ats.typeInfo.size() == 1);
CHECK(module->ats.typeInfo[0].code == Pattern::FuncArg);
CHECK(
module->ats.typeInfo[0].node == "function f(x: any)\nif not x then\nx = {\n y = math.random(0, 2^31-1),\n left = nil,\n right = "
"nil\n}\nelse\n local expected = x * 5\nend\nend"
);
@ -342,9 +342,9 @@ TEST_CASE_FIXTURE(ATSFixture, "variadic_any")
ModulePtr module = frontend.moduleResolver.getModule("game/Gui/Modules/A");
LUAU_ASSERT(module->ats.typeInfo.size() == 2);
LUAU_ASSERT(module->ats.typeInfo[0].code == Pattern::FuncRet);
LUAU_ASSERT(module->ats.typeInfo[0].node == "local function f(): (number, ...any)\n return 1, 5\n end");
REQUIRE(module->ats.typeInfo.size() == 2);
CHECK(module->ats.typeInfo[0].code == Pattern::FuncRet);
CHECK(module->ats.typeInfo[0].node == "local function f(): (number, ...any)\n return 1, 5\n end");
}
TEST_CASE_FIXTURE(ATSFixture, "type_alias_intersection")
@ -366,9 +366,9 @@ TEST_CASE_FIXTURE(ATSFixture, "type_alias_intersection")
ModulePtr module = frontend.moduleResolver.getModule("game/Gui/Modules/A");
LUAU_ASSERT(module->ats.typeInfo.size() == 3);
LUAU_ASSERT(module->ats.typeInfo[2].code == Pattern::VarAnnot);
LUAU_ASSERT(module->ats.typeInfo[2].node == "local vec2: Vector2 = {x = 1, y = 2}");
REQUIRE(module->ats.typeInfo.size() == 3);
CHECK(module->ats.typeInfo[2].code == Pattern::VarAnnot);
CHECK(module->ats.typeInfo[2].node == "local vec2: Vector2 = {x = 1, y = 2}");
}
TEST_CASE_FIXTURE(ATSFixture, "var_func_arg")
@ -394,9 +394,9 @@ TEST_CASE_FIXTURE(ATSFixture, "var_func_arg")
ModulePtr module = frontend.moduleResolver.getModule("game/Gui/Modules/A");
LUAU_ASSERT(module->ats.typeInfo.size() == 4);
LUAU_ASSERT(module->ats.typeInfo[0].code == Pattern::VarAny);
LUAU_ASSERT(module->ats.typeInfo[0].node == "local function f(...: any)\n end");
REQUIRE(module->ats.typeInfo.size() == 4);
CHECK(module->ats.typeInfo[0].code == Pattern::VarAny);
CHECK(module->ats.typeInfo[0].node == "local function f(...: any)\n end");
}
TEST_CASE_FIXTURE(ATSFixture, "var_func_apps")
@ -418,9 +418,9 @@ TEST_CASE_FIXTURE(ATSFixture, "var_func_apps")
ModulePtr module = frontend.moduleResolver.getModule("game/Gui/Modules/A");
LUAU_ASSERT(module->ats.typeInfo.size() == 3);
LUAU_ASSERT(module->ats.typeInfo[0].code == Pattern::VarAny);
LUAU_ASSERT(module->ats.typeInfo[0].node == "local function f(...: any)\n end");
REQUIRE(module->ats.typeInfo.size() == 3);
CHECK(module->ats.typeInfo[0].code == Pattern::VarAny);
CHECK(module->ats.typeInfo[0].node == "local function f(...: any)\n end");
}
@ -455,7 +455,7 @@ end
CHECK_EQ(module->ats.typeInfo[0].node, "descendant.CollisionGroup = CAR_COLLISION_GROUP");
}
else
LUAU_ASSERT(module->ats.typeInfo.size() == 0);
REQUIRE(module->ats.typeInfo.size() == 0);
}
TEST_CASE_FIXTURE(ATSFixture, "unknown_symbol")
@ -477,9 +477,9 @@ end
ModulePtr module = frontend.moduleResolver.getModule("game/Gui/Modules/A");
LUAU_ASSERT(module->ats.typeInfo.size() == 2);
LUAU_ASSERT(module->ats.typeInfo[0].code == Pattern::FuncArg);
LUAU_ASSERT(module->ats.typeInfo[0].node == "local function manageRace(raceContainer: Model)\n RaceManager.new(raceContainer)\nend");
REQUIRE(module->ats.typeInfo.size() == 2);
CHECK(module->ats.typeInfo[0].code == Pattern::FuncArg);
CHECK(module->ats.typeInfo[0].node == "local function manageRace(raceContainer: Model)\n RaceManager.new(raceContainer)\nend");
}
TEST_CASE_FIXTURE(ATSFixture, "racing_3_short")
@ -518,9 +518,9 @@ initialize()
ModulePtr module = frontend.moduleResolver.getModule("game/Gui/Modules/A");
LUAU_ASSERT(module->ats.typeInfo.size() == 5);
LUAU_ASSERT(module->ats.typeInfo[0].code == Pattern::FuncArg);
LUAU_ASSERT(module->ats.typeInfo[0].node == "local function manageRace(raceContainer: Model)\n RaceManager.new(raceContainer)\nend");
REQUIRE(module->ats.typeInfo.size() == 5);
CHECK(module->ats.typeInfo[0].code == Pattern::FuncArg);
CHECK(module->ats.typeInfo[0].node == "local function manageRace(raceContainer: Model)\n RaceManager.new(raceContainer)\nend");
}
TEST_CASE_FIXTURE(ATSFixture, "racing_collision_2")
@ -596,10 +596,10 @@ initialize()
ModulePtr module = frontend.moduleResolver.getModule("game/Gui/Modules/A");
if (FFlag::LuauSkipNoRefineDuringRefinement)
CHECK_EQ(module->ats.typeInfo.size(), 12);
REQUIRE_EQ(module->ats.typeInfo.size(), 12);
else
LUAU_ASSERT(module->ats.typeInfo.size() == 11);
LUAU_ASSERT(module->ats.typeInfo[0].code == Pattern::FuncArg);
REQUIRE(module->ats.typeInfo.size() == 11);
CHECK(module->ats.typeInfo[0].code == Pattern::FuncArg);
if (FFlag::LuauStoreCSTData)
{
CHECK_EQ(
@ -612,7 +612,7 @@ initialize()
}
else
{
LUAU_ASSERT(
CHECK(
module->ats.typeInfo[0].node ==
"local function onCharacterAdded(character: Model)\n\n character.DescendantAdded:Connect(function(descendant)\n if "
"descendant:IsA('BasePart')then\n descendant.CollisionGroup = CHARACTER_COLLISION_GROUP\n end\n end)\n\n\n for _, descendant in "
@ -685,9 +685,9 @@ initialize()
ModulePtr module = frontend.moduleResolver.getModule("game/Gui/Modules/A");
LUAU_ASSERT(module->ats.typeInfo.size() == 7);
LUAU_ASSERT(module->ats.typeInfo[0].code == Pattern::FuncArg);
LUAU_ASSERT(
REQUIRE(module->ats.typeInfo.size() == 7);
CHECK(module->ats.typeInfo[0].code == Pattern::FuncArg);
CHECK(
module->ats.typeInfo[0].node ==
"local function setupKiosk(kiosk: Model)\n local spawnLocation = kiosk:FindFirstChild('SpawnLocation')\n assert(spawnLocation, "
"`{kiosk:GetFullName()} has no SpawnLocation part`)\n local promptPart = kiosk:FindFirstChild('Prompt')\n assert(promptPart, "
@ -719,7 +719,7 @@ TEST_CASE_FIXTURE(ATSFixture, "mutually_recursive_generic")
ModulePtr module = frontend.moduleResolver.getModule("game/Gui/Modules/A");
LUAU_ASSERT(module->ats.typeInfo.size() == 0);
REQUIRE(module->ats.typeInfo.size() == 0);
}
TEST_CASE_FIXTURE(ATSFixture, "explicit_pack")
@ -739,9 +739,9 @@ type Bar = Foo<(number, any)>
ModulePtr module = frontend.moduleResolver.getModule("game/Gui/Modules/A");
LUAU_ASSERT(module->ats.typeInfo.size() == 1);
LUAU_ASSERT(module->ats.typeInfo[0].code == Pattern::Alias);
LUAU_ASSERT(module->ats.typeInfo[0].node == "type Bar = Foo<(number, any)>");
REQUIRE(module->ats.typeInfo.size() == 1);
CHECK(module->ats.typeInfo[0].code == Pattern::Alias);
CHECK(module->ats.typeInfo[0].node == "type Bar = Foo<(number, any)>");
}
TEST_CASE_FIXTURE(ATSFixture, "local_val")
@ -760,9 +760,9 @@ local a, b, c = 1 :: any
ModulePtr module = frontend.moduleResolver.getModule("game/Gui/Modules/A");
LUAU_ASSERT(module->ats.typeInfo.size() == 1);
LUAU_ASSERT(module->ats.typeInfo[0].code == Pattern::Casts);
LUAU_ASSERT(module->ats.typeInfo[0].node == "local a, b, c = 1 :: any");
REQUIRE(module->ats.typeInfo.size() == 1);
CHECK(module->ats.typeInfo[0].code == Pattern::Casts);
CHECK(module->ats.typeInfo[0].node == "local a, b, c = 1 :: any");
}
TEST_CASE_FIXTURE(ATSFixture, "var_any_local")
@ -784,9 +784,9 @@ local x: number, y: any, z, h: nil = 1, nil
ModulePtr module = frontend.moduleResolver.getModule("game/Gui/Modules/A");
LUAU_ASSERT(module->ats.typeInfo.size() == 3);
LUAU_ASSERT(module->ats.typeInfo[0].code == Pattern::VarAnnot);
LUAU_ASSERT(module->ats.typeInfo[0].node == "local x: any = 2, 3");
REQUIRE(module->ats.typeInfo.size() == 3);
CHECK(module->ats.typeInfo[0].code == Pattern::VarAnnot);
CHECK(module->ats.typeInfo[0].node == "local x: any = 2, 3");
}
TEST_CASE_FIXTURE(ATSFixture, "table_uses_any")
@ -807,9 +807,9 @@ TEST_CASE_FIXTURE(ATSFixture, "table_uses_any")
ModulePtr module = frontend.moduleResolver.getModule("game/Gui/Modules/A");
LUAU_ASSERT(module->ats.typeInfo.size() == 1);
LUAU_ASSERT(module->ats.typeInfo[0].code == Pattern::VarAnnot);
LUAU_ASSERT(module->ats.typeInfo[0].node == "local x: any = 0");
REQUIRE(module->ats.typeInfo.size() == 1);
CHECK(module->ats.typeInfo[0].code == Pattern::VarAnnot);
CHECK(module->ats.typeInfo[0].node == "local x: any = 0");
}
TEST_CASE_FIXTURE(ATSFixture, "typeof_any")
@ -830,9 +830,9 @@ TEST_CASE_FIXTURE(ATSFixture, "typeof_any")
ModulePtr module = frontend.moduleResolver.getModule("game/Gui/Modules/A");
LUAU_ASSERT(module->ats.typeInfo.size() == 2);
LUAU_ASSERT(module->ats.typeInfo[1].code == Pattern::FuncArg);
LUAU_ASSERT(module->ats.typeInfo[0].node == "function some1(x: typeof(x))\n end");
REQUIRE(module->ats.typeInfo.size() == 2);
CHECK(module->ats.typeInfo[1].code == Pattern::FuncArg);
CHECK(module->ats.typeInfo[0].node == "function some1(x: typeof(x))\n end");
}
TEST_CASE_FIXTURE(ATSFixture, "table_type_assigned")
@ -854,9 +854,9 @@ TEST_CASE_FIXTURE(ATSFixture, "table_type_assigned")
ModulePtr module = frontend.moduleResolver.getModule("game/Gui/Modules/A");
LUAU_ASSERT(module->ats.typeInfo.size() == 2);
LUAU_ASSERT(module->ats.typeInfo[1].code == Pattern::Assign);
LUAU_ASSERT(module->ats.typeInfo[0].node == "local x: { x: any?} = {x = 1}");
REQUIRE(module->ats.typeInfo.size() == 2);
CHECK(module->ats.typeInfo[1].code == Pattern::Assign);
CHECK(module->ats.typeInfo[0].node == "local x: { x: any?} = {x = 1}");
}
TEST_CASE_FIXTURE(ATSFixture, "simple_func_wo_ret")
@ -876,9 +876,9 @@ TEST_CASE_FIXTURE(ATSFixture, "simple_func_wo_ret")
ModulePtr module = frontend.moduleResolver.getModule("game/Gui/Modules/A");
LUAU_ASSERT(module->ats.typeInfo.size() == 1);
LUAU_ASSERT(module->ats.typeInfo[0].code == Pattern::FuncArg);
LUAU_ASSERT(module->ats.typeInfo[0].node == "function some(x: any)\n end");
REQUIRE(module->ats.typeInfo.size() == 1);
CHECK(module->ats.typeInfo[0].code == Pattern::FuncArg);
CHECK(module->ats.typeInfo[0].node == "function some(x: any)\n end");
}
TEST_CASE_FIXTURE(ATSFixture, "simple_func_w_ret")
@ -899,9 +899,9 @@ TEST_CASE_FIXTURE(ATSFixture, "simple_func_w_ret")
ModulePtr module = frontend.moduleResolver.getModule("game/Gui/Modules/A");
LUAU_ASSERT(module->ats.typeInfo.size() == 1);
LUAU_ASSERT(module->ats.typeInfo[0].code == Pattern::FuncRet);
LUAU_ASSERT(module->ats.typeInfo[0].node == "function other(y: number): any\n return 'gotcha!'\n end");
REQUIRE(module->ats.typeInfo.size() == 1);
CHECK(module->ats.typeInfo[0].code == Pattern::FuncRet);
CHECK(module->ats.typeInfo[0].node == "function other(y: number): any\n return 'gotcha!'\n end");
}
TEST_CASE_FIXTURE(ATSFixture, "nested_local")
@ -923,9 +923,9 @@ TEST_CASE_FIXTURE(ATSFixture, "nested_local")
ModulePtr module = frontend.moduleResolver.getModule("game/Gui/Modules/A");
LUAU_ASSERT(module->ats.typeInfo.size() == 1);
LUAU_ASSERT(module->ats.typeInfo[0].code == Pattern::VarAnnot);
LUAU_ASSERT(module->ats.typeInfo[0].node == "function cool(y: number): number\n local g: any = 'gratatataaa'\n return y\n end");
REQUIRE(module->ats.typeInfo.size() == 1);
CHECK(module->ats.typeInfo[0].code == Pattern::VarAnnot);
CHECK(module->ats.typeInfo[0].node == "function cool(y: number): number\n local g: any = 'gratatataaa'\n return y\n end");
}
TEST_CASE_FIXTURE(ATSFixture, "generic_func")
@ -946,9 +946,9 @@ TEST_CASE_FIXTURE(ATSFixture, "generic_func")
ModulePtr module = frontend.moduleResolver.getModule("game/Gui/Modules/A");
LUAU_ASSERT(module->ats.typeInfo.size() == 1);
LUAU_ASSERT(module->ats.typeInfo[0].code == Pattern::FuncArg);
LUAU_ASSERT(module->ats.typeInfo[0].node == "function reverse<T>(a: {T}, b: any): {T}\n return a\n end");
REQUIRE(module->ats.typeInfo.size() == 1);
CHECK(module->ats.typeInfo[0].code == Pattern::FuncArg);
CHECK(module->ats.typeInfo[0].node == "function reverse<T>(a: {T}, b: any): {T}\n return a\n end");
}
TEST_CASE_FIXTURE(ATSFixture, "type_alias_any")
@ -968,9 +968,9 @@ TEST_CASE_FIXTURE(ATSFixture, "type_alias_any")
ModulePtr module = frontend.moduleResolver.getModule("game/Gui/Modules/A");
LUAU_ASSERT(module->ats.typeInfo.size() == 2);
LUAU_ASSERT(module->ats.typeInfo[0].code == Pattern::Alias);
LUAU_ASSERT(module->ats.typeInfo[0].node == "type Clear = any");
REQUIRE(module->ats.typeInfo.size() == 2);
CHECK(module->ats.typeInfo[0].code == Pattern::Alias);
CHECK(module->ats.typeInfo[0].node == "type Clear = any");
}
TEST_CASE_FIXTURE(ATSFixture, "multi_module_any")
@ -1001,9 +1001,9 @@ TEST_CASE_FIXTURE(ATSFixture, "multi_module_any")
ModulePtr module = frontend.moduleResolver.getModule("game/B");
LUAU_ASSERT(module->ats.typeInfo.size() == 2);
LUAU_ASSERT(module->ats.typeInfo[0].code == Pattern::Alias);
LUAU_ASSERT(module->ats.typeInfo[0].node == "type Clear = any");
REQUIRE(module->ats.typeInfo.size() == 2);
CHECK(module->ats.typeInfo[0].code == Pattern::Alias);
CHECK(module->ats.typeInfo[0].node == "type Clear = any");
}
TEST_CASE_FIXTURE(ATSFixture, "cast_on_cyclic_req")
@ -1029,9 +1029,9 @@ TEST_CASE_FIXTURE(ATSFixture, "cast_on_cyclic_req")
ModulePtr module = frontend.moduleResolver.getModule("game/B");
LUAU_ASSERT(module->ats.typeInfo.size() == 3);
LUAU_ASSERT(module->ats.typeInfo[1].code == Pattern::Alias);
LUAU_ASSERT(module->ats.typeInfo[1].node == "type Clear = any");
REQUIRE(module->ats.typeInfo.size() == 3);
CHECK(module->ats.typeInfo[1].code == Pattern::Alias);
CHECK(module->ats.typeInfo[1].node == "type Clear = any");
}

View file

@ -11,7 +11,7 @@
using namespace Luau;
LUAU_FASTFLAG(LuauAstTypeGroup2)
LUAU_FASTFLAG(LuauAstTypeGroup3)
struct JsonEncoderFixture
{
@ -473,7 +473,7 @@ TEST_CASE_FIXTURE(JsonEncoderFixture, "encode_annotation")
{
AstStat* statement = expectParseStatement("type T = ((number) -> (string | nil)) & ((string) -> ())");
if (FFlag::LuauAstTypeGroup2)
if (FFlag::LuauAstTypeGroup3)
{
std::string_view expected =
R"({"type":"AstStatTypeAlias","location":"0,0 - 0,56","name":"T","generics":[],"genericPacks":[],"value":{"type":"AstTypeIntersection","location":"0,9 - 0,56","types":[{"type":"AstTypeGroup","location":"0,9 - 0,37","inner":{"type":"AstTypeFunction","location":"0,10 - 0,36","attributes":[],"generics":[],"genericPacks":[],"argTypes":{"type":"AstTypeList","types":[{"type":"AstTypeReference","location":"0,11 - 0,17","name":"number","nameLocation":"0,11 - 0,17","parameters":[]}]},"argNames":[],"returnTypes":{"type":"AstTypeList","types":[{"type":"AstTypeGroup","location":"0,22 - 0,36","inner":{"type":"AstTypeUnion","location":"0,23 - 0,35","types":[{"type":"AstTypeReference","location":"0,23 - 0,29","name":"string","nameLocation":"0,23 - 0,29","parameters":[]},{"type":"AstTypeReference","location":"0,32 - 0,35","name":"nil","nameLocation":"0,32 - 0,35","parameters":[]}]}}]}}},{"type":"AstTypeGroup","location":"0,40 - 0,56","inner":{"type":"AstTypeFunction","location":"0,41 - 0,55","attributes":[],"generics":[],"genericPacks":[],"argTypes":{"type":"AstTypeList","types":[{"type":"AstTypeReference","location":"0,42 - 0,48","name":"string","nameLocation":"0,42 - 0,48","parameters":[]}]},"argNames":[],"returnTypes":{"type":"AstTypeList","types":[]}}}]},"exported":false})";

View file

@ -1,11 +1,14 @@
// This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details
#include "Luau/Autocomplete.h"
#include "Luau/AutocompleteTypes.h"
#include "Luau/BuiltinDefinitions.h"
#include "Luau/TypeInfer.h"
#include "Luau/Type.h"
#include "Luau/VisitType.h"
#include "Luau/StringUtils.h"
#include "ClassFixture.h"
#include "Fixture.h"
#include "ScopedFlags.h"
@ -17,6 +20,10 @@ LUAU_FASTFLAG(LuauTraceTypesInNonstrictMode2)
LUAU_FASTFLAG(LuauSetMetatableDoesNotTimeTravel)
LUAU_FASTINT(LuauTypeInferRecursionLimit)
LUAU_FASTFLAG(LuauExposeRequireByStringAutocomplete)
LUAU_FASTFLAG(LuauAutocompleteUnionCopyPreviousSeen)
LUAU_FASTFLAG(LuauUserTypeFunTypecheck)
using namespace Luau;
static std::optional<AutocompleteEntryMap> nullCallback(std::string tag, std::optional<const ClassType*> ptr, std::optional<std::string> contents)
@ -151,6 +158,10 @@ struct ACBuiltinsFixture : ACFixtureImpl<BuiltinsFixture>
{
};
struct ACClassFixture : ACFixtureImpl<ClassFixture>
{
};
TEST_SUITE_BEGIN("AutocompleteTest");
TEST_CASE_FIXTURE(ACFixture, "empty_program")
@ -3752,6 +3763,73 @@ TEST_CASE_FIXTURE(ACFixture, "string_contents_is_available_to_callback")
CHECK(isCorrect);
}
TEST_CASE_FIXTURE(ACBuiltinsFixture, "require_by_string")
{
ScopedFastFlag sff{FFlag::LuauExposeRequireByStringAutocomplete, true};
fileResolver.source["MainModule"] = R"(
local info = "MainModule serves as the root directory"
)";
fileResolver.source["MainModule/Folder"] = R"(
local info = "MainModule/Folder serves as a subdirectory"
)";
fileResolver.source["MainModule/Folder/Requirer"] = R"(
local res0 = require("@")
local res1 = require(".")
local res2 = require("./")
local res3 = require("./Sib")
local res4 = require("..")
local res5 = require("../")
local res6 = require("../Sib")
)";
fileResolver.source["MainModule/Folder/SiblingDependency"] = R"(
return {"result"}
)";
fileResolver.source["MainModule/ParentDependency"] = R"(
return {"result"}
)";
struct RequireCompletion
{
std::string label;
std::string insertText;
};
auto checkEntries = [](const AutocompleteEntryMap& entryMap, const std::vector<RequireCompletion>& completions)
{
CHECK(completions.size() == entryMap.size());
for (const auto& completion : completions)
{
CHECK(entryMap.count(completion.label));
CHECK(entryMap.at(completion.label).insertText == completion.insertText);
}
};
AutocompleteResult acResult;
acResult = autocomplete("MainModule/Folder/Requirer", Position{1, 31});
checkEntries(acResult.entryMap, {{"@defaultalias", "@defaultalias"}, {"./", "./"}, {"../", "../"}});
acResult = autocomplete("MainModule/Folder/Requirer", Position{3, 31});
checkEntries(acResult.entryMap, {{"@defaultalias", "@defaultalias"}, {"./", "./"}, {"../", "../"}});
acResult = autocomplete("MainModule/Folder/Requirer", Position{4, 32});
checkEntries(acResult.entryMap, {{"..", "."}, {"Requirer", "./Requirer"}, {"SiblingDependency", "./SiblingDependency"}});
acResult = autocomplete("MainModule/Folder/Requirer", Position{5, 35});
checkEntries(acResult.entryMap, {{"..", "."}, {"Requirer", "./Requirer"}, {"SiblingDependency", "./SiblingDependency"}});
acResult = autocomplete("MainModule/Folder/Requirer", Position{7, 32});
checkEntries(acResult.entryMap, {{"@defaultalias", "@defaultalias"}, {"./", "./"}, {"../", "../"}});
acResult = autocomplete("MainModule/Folder/Requirer", Position{8, 33});
checkEntries(acResult.entryMap, {{"..", "../.."}, {"Folder", "../Folder"}, {"ParentDependency", "../ParentDependency"}});
acResult = autocomplete("MainModule/Folder/Requirer", Position{9, 36});
checkEntries(acResult.entryMap, {{"..", "../.."}, {"Folder", "../Folder"}, {"ParentDependency", "../ParentDependency"}});
}
TEST_CASE_FIXTURE(ACFixture, "autocomplete_response_perf1" * doctest::timeout(0.5))
{
if (FFlag::LuauSolverV2)
@ -4346,4 +4424,76 @@ local x = 1 + result.
CHECK(ac.entryMap.count("x"));
}
TEST_CASE_FIXTURE(ACClassFixture, "ac_dont_overflow_on_recursive_union")
{
ScopedFastFlag _{FFlag::LuauAutocompleteUnionCopyPreviousSeen, true};
check(R"(
local table1: {ChildClass} = {}
local table2 = {}
for index, value in table2[1] do
table.insert(table1, value)
value.@1
end
)");
auto ac = autocomplete('1');
// RIDE-11517: This should *really* be the members of `ChildClass`, but
// would previously stack overflow.
CHECK(ac.entryMap.empty());
}
TEST_CASE_FIXTURE(ACBuiltinsFixture, "type_function_has_types_definitions")
{
// Needs new global initialization in the Fixture, but can't place the flag inside the base Fixture
if (!FFlag::LuauUserTypeFunTypecheck)
return;
ScopedFastFlag newSolver{FFlag::LuauSolverV2, true};
check(R"(
type function foo()
types.@1
end
)");
auto ac = autocomplete('1');
CHECK_EQ(ac.entryMap.count("singleton"), 1);
}
TEST_CASE_FIXTURE(ACBuiltinsFixture, "type_function_private_scope")
{
// Needs new global initialization in the Fixture, but can't place the flag inside the base Fixture
if (!FFlag::LuauUserTypeFunTypecheck)
return;
ScopedFastFlag newSolver{FFlag::LuauSolverV2, true};
// Global scope polution by the embedder has no effect
addGlobalBinding(frontend.globals, "thisAlsoShouldNotBeThere", Binding{builtinTypes->anyType});
addGlobalBinding(frontend.globalsForAutocomplete, "thisAlsoShouldNotBeThere", Binding{builtinTypes->anyType});
check(R"(
local function thisShouldNotBeThere() end
type function thisShouldBeThere() end
type function foo()
this@1
end
this@2
)");
auto ac = autocomplete('1');
CHECK_EQ(ac.entryMap.count("thisShouldNotBeThere"), 0);
CHECK_EQ(ac.entryMap.count("thisAlsoShouldNotBeThere"), 0);
CHECK_EQ(ac.entryMap.count("thisShouldBeThere"), 1);
ac = autocomplete('2');
CHECK_EQ(ac.entryMap.count("thisShouldNotBeThere"), 1);
CHECK_EQ(ac.entryMap.count("thisAlsoShouldNotBeThere"), 1);
CHECK_EQ(ac.entryMap.count("thisShouldBeThere"), 0);
}
TEST_SUITE_END();

View file

@ -9,7 +9,8 @@ using std::nullopt;
namespace Luau
{
ClassFixture::ClassFixture()
ClassFixture::ClassFixture(bool prepareAutocomplete)
: BuiltinsFixture(prepareAutocomplete)
{
GlobalTypes& globals = frontend.globals;
TypeArena& arena = globals.globalTypes;

View file

@ -8,7 +8,7 @@ namespace Luau
struct ClassFixture : BuiltinsFixture
{
ClassFixture();
explicit ClassFixture(bool prepareAutocomplete = false);
TypeId vector2Type;
TypeId vector2InstanceType;

View file

@ -477,9 +477,8 @@ void setupUserdataHelpers(lua_State* L)
{
// create metatable with all the metamethods
luaL_newmetatable(L, "vec2");
luaL_getmetatable(L, "vec2");
lua_pushvalue(L, -1);
lua_setuserdatametatable(L, kTagVec2, -1);
lua_setuserdatametatable(L, kTagVec2);
lua_pushcfunction(L, lua_vec2_index, nullptr);
lua_setfield(L, -2, "__index");
@ -2255,12 +2254,12 @@ TEST_CASE("UserdataApi")
// tagged user data with fast metatable access
luaL_newmetatable(L, "udata3");
luaL_getmetatable(L, "udata3");
lua_setuserdatametatable(L, 50, -1);
lua_pushvalue(L, -1);
lua_setuserdatametatable(L, 50);
luaL_newmetatable(L, "udata4");
luaL_getmetatable(L, "udata4");
lua_setuserdatametatable(L, 51, -1);
lua_pushvalue(L, -1);
lua_setuserdatametatable(L, 51);
void* ud7 = lua_newuserdatatagged(L, 16, 50);
lua_getuserdatametatable(L, 50);

View file

@ -34,6 +34,7 @@ void ConstraintGeneratorFixture::generateConstraints(const std::string& code)
builtinTypes,
NotNull(&ice),
frontend.globals.globalScope,
frontend.globals.globalTypeFunctionScope,
/*prepareModuleScope*/ nullptr,
&logger,
NotNull{dfg.get()},

View file

@ -5,6 +5,7 @@
#include "Luau/BuiltinDefinitions.h"
#include "Luau/Common.h"
#include "Luau/Constraint.h"
#include "Luau/FileResolver.h"
#include "Luau/ModuleResolver.h"
#include "Luau/NotNull.h"
#include "Luau/Parser.h"
@ -34,6 +35,110 @@ extern std::optional<unsigned> randomSeed; // tests/main.cpp
namespace Luau
{
static std::string getNodeName(const TestRequireNode* node)
{
std::string name;
size_t lastSlash = node->moduleName.find_last_of('/');
if (lastSlash != std::string::npos)
name = node->moduleName.substr(lastSlash + 1);
else
name = node->moduleName;
return name;
}
std::string TestRequireNode::getLabel() const
{
return getNodeName(this);
}
std::string TestRequireNode::getPathComponent() const
{
return getNodeName(this);
}
static std::vector<std::string_view> splitStringBySlashes(std::string_view str)
{
std::vector<std::string_view> components;
size_t pos = 0;
size_t nextPos = str.find_first_of('/', pos);
if (nextPos == std::string::npos)
{
components.push_back(str);
return components;
}
while (nextPos != std::string::npos)
{
components.push_back(str.substr(pos, nextPos - pos));
pos = nextPos + 1;
nextPos = str.find_first_of('/', pos);
}
components.push_back(str.substr(pos));
return components;
}
std::unique_ptr<RequireNode> TestRequireNode::resolvePathToNode(const std::string& path) const
{
std::vector<std::string_view> components = splitStringBySlashes(path);
LUAU_ASSERT((components.empty() || components[0] == "." || components[0] == "..") && "Path must begin with ./ or ../ in test");
std::vector<std::string_view> normalizedComponents = splitStringBySlashes(moduleName);
normalizedComponents.pop_back();
LUAU_ASSERT(!normalizedComponents.empty() && "Must have a root module");
for (std::string_view component : components)
{
if (component == "..")
{
if (normalizedComponents.empty())
LUAU_ASSERT(!"Cannot go above root module in test");
else
normalizedComponents.pop_back();
}
else if (!component.empty() && component != ".")
{
normalizedComponents.emplace_back(component);
}
}
std::string moduleName;
for (size_t i = 0; i < normalizedComponents.size(); i++)
{
if (i > 0)
moduleName += '/';
moduleName += normalizedComponents[i];
}
if (allSources->count(moduleName) == 0)
return nullptr;
return std::make_unique<TestRequireNode>(moduleName, allSources);
}
std::vector<std::unique_ptr<RequireNode>> TestRequireNode::getChildren() const
{
std::vector<std::unique_ptr<RequireNode>> result;
for (const auto& entry : *allSources)
{
if (std::string_view(entry.first).substr(0, moduleName.size()) == moduleName && entry.first.size() > moduleName.size() &&
entry.first[moduleName.size()] == '/' && entry.first.find('/', moduleName.size() + 1) == std::string::npos)
{
result.push_back(std::make_unique<TestRequireNode>(entry.first, allSources));
}
}
return result;
}
std::vector<RequireAlias> TestRequireNode::getAvailableAliases() const
{
return {{"defaultalias"}};
}
std::unique_ptr<RequireNode> TestRequireSuggester::getNode(const ModuleName& name) const
{
return std::make_unique<TestRequireNode>(name, &resolver->source);
}
std::optional<ModuleInfo> TestFileResolver::resolveModuleInfo(const ModuleName& currentModuleName, const AstExpr& pathExpr)
{
if (auto name = pathExprToModuleName(currentModuleName, pathExpr))
@ -218,6 +323,7 @@ AstStatBlock* Fixture::parse(const std::string& source, const ParseOptions& pars
NotNull{&moduleResolver},
NotNull{&fileResolver},
frontend.globals.globalScope,
frontend.globals.globalTypeFunctionScope,
/*prepareModuleScope*/ nullptr,
frontend.options,
{},

View file

@ -37,10 +37,45 @@ namespace Luau
struct TypeChecker;
struct TestRequireNode : RequireNode
{
TestRequireNode(ModuleName moduleName, std::unordered_map<ModuleName, std::string>* allSources)
: moduleName(std::move(moduleName))
, allSources(allSources)
{
}
std::string getLabel() const override;
std::string getPathComponent() const override;
std::unique_ptr<RequireNode> resolvePathToNode(const std::string& path) const override;
std::vector<std::unique_ptr<RequireNode>> getChildren() const override;
std::vector<RequireAlias> getAvailableAliases() const override;
ModuleName moduleName;
std::unordered_map<ModuleName, std::string>* allSources;
};
struct TestFileResolver;
struct TestRequireSuggester : RequireSuggester
{
TestRequireSuggester(TestFileResolver* resolver)
: resolver(resolver)
{
}
std::unique_ptr<RequireNode> getNode(const ModuleName& name) const override;
TestFileResolver* resolver;
};
struct TestFileResolver
: FileResolver
, ModuleResolver
{
TestFileResolver()
: FileResolver(std::make_shared<TestRequireSuggester>(this))
{
}
std::optional<ModuleInfo> resolveModuleInfo(const ModuleName& currentModuleName, const AstExpr& pathExpr) override;
const ModulePtr getModule(const ModuleName& moduleName) const override;

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(LuauModuleHoldsAstRoot)
LUAU_FASTFLAG(LuauImproveTypePathsInErrors)
namespace
{
@ -920,7 +921,17 @@ TEST_CASE_FIXTURE(FrontendFixture, "it_should_be_safe_to_stringify_errors_when_f
// When this test fails, it is because the TypeIds needed by the error have been deallocated.
// It is thus basically impossible to predict what will happen when this assert is evaluated.
// It could segfault, or you could see weird type names like the empty string or <VALUELESS BY EXCEPTION>
if (FFlag::LuauSolverV2)
if (FFlag::LuauSolverV2 && FFlag::LuauImproveTypePathsInErrors)
{
REQUIRE_EQ(
"Type\n\t"
"'{ count: string }'"
"\ncould not be converted into\n\t"
"'{ Count: number }'",
toString(result.errors[0])
);
}
else if (FFlag::LuauSolverV2)
REQUIRE_EQ(
R"(Type
'{ count: string }'
@ -1542,6 +1553,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}};
@ -1571,8 +1599,6 @@ return {x = a, y = b, z = c}
TEST_CASE_FIXTURE(FrontendFixture, "test_traverse_dependents")
{
ScopedFastFlag dependencyTracking{FFlag::LuauBetterReverseDependencyTracking, true};
fileResolver.source["game/Gui/Modules/A"] = "return {hello=5, world=true}";
fileResolver.source["game/Gui/Modules/B"] = R"(
return require(game:GetService('Gui').Modules.A)
@ -1605,8 +1631,6 @@ TEST_CASE_FIXTURE(FrontendFixture, "test_traverse_dependents")
TEST_CASE_FIXTURE(FrontendFixture, "test_traverse_dependents_early_exit")
{
ScopedFastFlag dependencyTracking{FFlag::LuauBetterReverseDependencyTracking, true};
fileResolver.source["game/Gui/Modules/A"] = "return {hello=5, world=true}";
fileResolver.source["game/Gui/Modules/B"] = R"(
return require(game:GetService('Gui').Modules.A)
@ -1634,8 +1658,6 @@ TEST_CASE_FIXTURE(FrontendFixture, "test_traverse_dependents_early_exit")
TEST_CASE_FIXTURE(FrontendFixture, "test_dependents_stored_on_node_as_graph_updates")
{
ScopedFastFlag dependencyTracking{FFlag::LuauBetterReverseDependencyTracking, true};
auto updateSource = [&](const std::string& name, const std::string& source)
{
fileResolver.source[name] = source;
@ -1750,7 +1772,6 @@ TEST_CASE_FIXTURE(FrontendFixture, "test_dependents_stored_on_node_as_graph_upda
TEST_CASE_FIXTURE(FrontendFixture, "test_invalid_dependency_tracking_per_module_resolver")
{
ScopedFastFlag dependencyTracking{FFlag::LuauBetterReverseDependencyTracking, true};
ScopedFastFlag newSolver{FFlag::LuauSolverV2, false};
fileResolver.source["game/Gui/Modules/A"] = "return {hello=5, world=true}";

View file

@ -8,8 +8,6 @@
using namespace Luau;
LUAU_FASTFLAG(LexerFixInterpStringStart)
TEST_SUITE_BEGIN("LexerTests");
TEST_CASE("broken_string_works")
@ -156,7 +154,7 @@ TEST_CASE("string_interpolation_basic")
Lexeme interpEnd = lexer.next();
CHECK_EQ(interpEnd.type, Lexeme::InterpStringEnd);
// The InterpStringEnd should start with }, not `.
CHECK_EQ(interpEnd.location.begin.column, FFlag::LexerFixInterpStringStart ? 11 : 12);
CHECK_EQ(interpEnd.location.begin.column, 11);
}
TEST_CASE("string_interpolation_full")
@ -177,7 +175,7 @@ TEST_CASE("string_interpolation_full")
Lexeme interpMid = lexer.next();
CHECK_EQ(interpMid.type, Lexeme::InterpStringMid);
CHECK_EQ(interpMid.toString(), "} {");
CHECK_EQ(interpMid.location.begin.column, FFlag::LexerFixInterpStringStart ? 11 : 12);
CHECK_EQ(interpMid.location.begin.column, 11);
Lexeme quote2 = lexer.next();
CHECK_EQ(quote2.type, Lexeme::QuotedString);
@ -186,7 +184,7 @@ TEST_CASE("string_interpolation_full")
Lexeme interpEnd = lexer.next();
CHECK_EQ(interpEnd.type, Lexeme::InterpStringEnd);
CHECK_EQ(interpEnd.toString(), "} end`");
CHECK_EQ(interpEnd.location.begin.column, FFlag::LexerFixInterpStringStart ? 19 : 20);
CHECK_EQ(interpEnd.location.begin.column, 19);
}
TEST_CASE("string_interpolation_double_brace")

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

@ -17,6 +17,8 @@
LUAU_FASTFLAG(LuauNewNonStrictWarnOnUnknownGlobals)
LUAU_FASTFLAG(LuauNonStrictVisitorImprovements)
LUAU_FASTFLAG(LuauNonStrictFuncDefErrorFix)
LUAU_FASTFLAG(LuauNormalizedBufferIsNotUnknown)
using namespace Luau;
@ -359,6 +361,23 @@ end
NONSTRICT_REQUIRE_FUNC_DEFINITION_ERR(Position(1, 11), "x", result);
}
TEST_CASE_FIXTURE(NonStrictTypeCheckerFixture, "function_def_sequencing_errors_2")
{
ScopedFastFlag luauNonStrictFuncDefErrorFix{FFlag::LuauNonStrictFuncDefErrorFix, true};
ScopedFastFlag luauNonStrictVisitorImprovements{FFlag::LuauNonStrictVisitorImprovements, true};
CheckResult result = checkNonStrict(R"(
local t = {function(x)
abs(x)
lower(x)
end}
)");
LUAU_REQUIRE_ERROR_COUNT(3, result);
NONSTRICT_REQUIRE_CHECKED_ERR(Position(2, 8), "abs", result);
NONSTRICT_REQUIRE_CHECKED_ERR(Position(3, 10), "lower", result);
CHECK(toString(result.errors[2]) == "Argument x with type 'unknown' is used in a way that will run time error");
}
TEST_CASE_FIXTURE(NonStrictTypeCheckerFixture, "local_fn_produces_error")
{
CheckResult result = checkNonStrict(R"(
@ -649,4 +668,17 @@ TEST_CASE_FIXTURE(Fixture, "unknown_globals_in_non_strict")
LUAU_REQUIRE_ERROR_COUNT(2, result);
}
TEST_CASE_FIXTURE(BuiltinsFixture, "buffer_is_not_unknown")
{
ScopedFastFlag luauNormalizedBufferIsNotUnknown{FFlag::LuauNormalizedBufferIsNotUnknown, true};
CheckResult result = check(Mode::Nonstrict, R"(
local function wrap(b: buffer, i: number, v: number)
buffer.writeu32(b, i * 4, v)
end
)");
LUAU_REQUIRE_NO_ERRORS(result);
}
TEST_SUITE_END();

View file

@ -12,7 +12,7 @@
LUAU_FASTFLAG(LuauSolverV2)
LUAU_FASTINT(LuauTypeInferRecursionLimit)
LUAU_FASTFLAG(LuauFixNormalizedIntersectionOfNegatedClass)
LUAU_FASTFLAG(LuauNormalizeNegationFix)
using namespace Luau;
namespace
@ -851,7 +851,6 @@ TEST_CASE_FIXTURE(NormalizeFixture, "crazy_metatable")
TEST_CASE_FIXTURE(NormalizeFixture, "negations_of_classes")
{
ScopedFastFlag _{FFlag::LuauFixNormalizedIntersectionOfNegatedClass, true};
createSomeClasses(&frontend);
CHECK("(Parent & ~Child) | Unrelated" == toString(normal("(Parent & Not<Child>) | Unrelated")));
CHECK("((class & ~Child) | boolean | buffer | function | number | string | table | thread)?" == toString(normal("Not<Child>")));
@ -1029,6 +1028,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

@ -18,12 +18,12 @@ LUAU_FASTINT(LuauParseErrorLimit)
LUAU_FASTFLAG(LuauSolverV2)
LUAU_FASTFLAG(LuauAllowComplexTypesInGenericParams)
LUAU_FASTFLAG(LuauErrorRecoveryForTableTypes)
LUAU_FASTFLAG(LuauErrorRecoveryForClassNames)
LUAU_FASTFLAG(LuauFixFunctionNameStartPosition)
LUAU_FASTFLAG(LuauExtendStatEndPosWithSemicolon)
LUAU_FASTFLAG(LuauPreserveUnionIntersectionNodeForLeadingTokenSingleType)
LUAU_FASTFLAG(LuauAstTypeGroup2)
LUAU_FASTFLAG(LuauAstTypeGroup3)
LUAU_FASTFLAG(LuauFixDoBlockEndLocation)
LUAU_FASTFLAG(LuauParseOptionalAsNode)
namespace
{
@ -372,7 +372,7 @@ TEST_CASE_FIXTURE(Fixture, "return_type_is_an_intersection_type_if_led_with_one_
AstTypeIntersection* returnAnnotation = annotation->returnTypes.types.data[0]->as<AstTypeIntersection>();
REQUIRE(returnAnnotation != nullptr);
if (FFlag::LuauAstTypeGroup2)
if (FFlag::LuauAstTypeGroup3)
CHECK(returnAnnotation->types.data[0]->as<AstTypeGroup>());
else
CHECK(returnAnnotation->types.data[0]->as<AstTypeReference>());
@ -2127,8 +2127,6 @@ TEST_CASE_FIXTURE(Fixture, "variadic_definition_parsing")
TEST_CASE_FIXTURE(Fixture, "missing_declaration_prop")
{
ScopedFastFlag luauErrorRecoveryForClassNames{FFlag::LuauErrorRecoveryForClassNames, true};
matchParseError(
R"(
declare class Foo
@ -2451,7 +2449,7 @@ TEST_CASE_FIXTURE(Fixture, "leading_union_intersection_with_single_type_preserve
TEST_CASE_FIXTURE(Fixture, "parse_simple_ast_type_group")
{
ScopedFastFlag _{FFlag::LuauAstTypeGroup2, true};
ScopedFastFlag _{FFlag::LuauAstTypeGroup3, true};
AstStatBlock* stat = parse(R"(
type Foo = (string)
@ -2469,7 +2467,7 @@ TEST_CASE_FIXTURE(Fixture, "parse_simple_ast_type_group")
TEST_CASE_FIXTURE(Fixture, "parse_nested_ast_type_group")
{
ScopedFastFlag _{FFlag::LuauAstTypeGroup2, true};
ScopedFastFlag _{FFlag::LuauAstTypeGroup3, true};
AstStatBlock* stat = parse(R"(
type Foo = ((string))
@ -2490,7 +2488,7 @@ TEST_CASE_FIXTURE(Fixture, "parse_nested_ast_type_group")
TEST_CASE_FIXTURE(Fixture, "parse_return_type_ast_type_group")
{
ScopedFastFlag _{FFlag::LuauAstTypeGroup2, true};
ScopedFastFlag _{FFlag::LuauAstTypeGroup3, true};
AstStatBlock* stat = parse(R"(
type Foo = () -> (string)
@ -3813,7 +3811,7 @@ TEST_CASE_FIXTURE(Fixture, "grouped_function_type")
auto unionTy = paramTy.type->as<AstTypeUnion>();
LUAU_ASSERT(unionTy);
CHECK_EQ(unionTy->types.size, 2);
if (FFlag::LuauAstTypeGroup2)
if (FFlag::LuauAstTypeGroup3)
{
auto groupTy = unionTy->types.data[0]->as<AstTypeGroup>(); // (() -> ())
REQUIRE(groupTy);
@ -3821,7 +3819,10 @@ TEST_CASE_FIXTURE(Fixture, "grouped_function_type")
}
else
CHECK(unionTy->types.data[0]->is<AstTypeFunction>()); // () -> ()
CHECK(unionTy->types.data[1]->is<AstTypeReference>()); // nil
if (FFlag::LuauParseOptionalAsNode)
CHECK(unionTy->types.data[1]->is<AstTypeOptional>()); // ?
else
CHECK(unionTy->types.data[1]->is<AstTypeReference>()); // nil
}
TEST_CASE_FIXTURE(Fixture, "complex_union_in_generic_ty")
@ -3926,5 +3927,16 @@ TEST_CASE_FIXTURE(Fixture, "stat_end_includes_semicolon_position")
CHECK_EQ(Position{3, 22}, stat3->location.end);
}
TEST_CASE_FIXTURE(Fixture, "parsing_type_suffix_for_return_type_with_variadic")
{
ParseResult result = tryParse(R"(
function foo(): (string, ...number) | boolean
end
)");
// TODO(CLI-140667): this should produce a ParseError in future when we fix the invalid syntax
CHECK(result.errors.size() == 0);
}
TEST_SUITE_END();

View file

@ -6,8 +6,6 @@
#include "doctest.h"
LUAU_FASTFLAG(LuauExtendedSimpleRequire)
using namespace Luau;
namespace
@ -182,8 +180,6 @@ TEST_CASE_FIXTURE(RequireTracerFixture, "follow_string_indexexpr")
TEST_CASE_FIXTURE(RequireTracerFixture, "follow_group")
{
ScopedFastFlag luauExtendedSimpleRequire{FFlag::LuauExtendedSimpleRequire, true};
AstStatBlock* block = parse(R"(
local R = (((game).Test))
require(R)
@ -200,8 +196,6 @@ TEST_CASE_FIXTURE(RequireTracerFixture, "follow_group")
TEST_CASE_FIXTURE(RequireTracerFixture, "follow_type_annotation")
{
ScopedFastFlag luauExtendedSimpleRequire{FFlag::LuauExtendedSimpleRequire, true};
AstStatBlock* block = parse(R"(
local R = game.Test :: (typeof(game.Redirect))
require(R)
@ -218,8 +212,6 @@ TEST_CASE_FIXTURE(RequireTracerFixture, "follow_type_annotation")
TEST_CASE_FIXTURE(RequireTracerFixture, "follow_type_annotation_2")
{
ScopedFastFlag luauExtendedSimpleRequire{FFlag::LuauExtendedSimpleRequire, true};
AstStatBlock* block = parse(R"(
local R = game.Test :: (typeof(game.Redirect))
local N = R.Nested

View file

@ -15,7 +15,8 @@
#include <initializer_list>
LUAU_FASTFLAG(LuauSolverV2);
LUAU_FASTFLAG(LuauSolverV2)
LUAU_FASTFLAG(LuauNormalizedBufferIsNotUnknown)
using namespace Luau;
@ -961,6 +962,20 @@ TEST_IS_NOT_SUBTYPE(childClass, negate(rootClass));
TEST_IS_NOT_SUBTYPE(childClass, meet(builtinTypes->classType, negate(rootClass)));
TEST_IS_SUBTYPE(anotherChildClass, meet(builtinTypes->classType, negate(childClass)));
// Negated primitives against unknown
TEST_IS_NOT_SUBTYPE(builtinTypes->unknownType, negate(builtinTypes->booleanType));
TEST_IS_NOT_SUBTYPE(builtinTypes->unknownType, negate(builtinTypes->numberType));
TEST_IS_NOT_SUBTYPE(builtinTypes->unknownType, negate(builtinTypes->stringType));
TEST_IS_NOT_SUBTYPE(builtinTypes->unknownType, negate(builtinTypes->threadType));
TEST_CASE_FIXTURE(SubtypeFixture, "unknown <!: ~buffer")
{
// TODO: replace with TEST_IS_NOT_SUBTYPE on flag removal
ScopedFastFlag luauNormalizedBufferIsNotUnknown{FFlag::LuauNormalizedBufferIsNotUnknown, true};
CHECK_IS_NOT_SUBTYPE(builtinTypes->unknownType, negate(builtinTypes->bufferType));
}
TEST_CASE_FIXTURE(SubtypeFixture, "Root <: class")
{
CHECK_IS_SUBTYPE(rootClass, builtinTypes->classType);

View file

@ -5,14 +5,16 @@
#include "Fixture.h"
#include "Luau/TypeChecker2.h"
#include "ScopedFlags.h"
#include "doctest.h"
using namespace Luau;
LUAU_FASTFLAG(LuauRecursiveTypeParameterRestriction);
LUAU_FASTFLAG(LuauSolverV2);
LUAU_FASTFLAG(LuauAttributeSyntax);
LUAU_FASTFLAG(LuauRecursiveTypeParameterRestriction)
LUAU_FASTFLAG(LuauSolverV2)
LUAU_FASTFLAG(LuauAttributeSyntax)
LUAU_FASTFLAG(LuauImproveTypePathsInErrors)
TEST_SUITE_BEGIN("ToString");
@ -871,9 +873,28 @@ TEST_CASE_FIXTURE(Fixture, "tostring_error_mismatch")
)");
std::string expected;
if (FFlag::LuauSolverV2)
if (FFlag::LuauSolverV2 && FFlag::LuauImproveTypePathsInErrors)
expected =
"Type pack '{ a: number, b: string, c: { d: string } }' could not be converted into '{ a: number, b: string, c: { d: number } }'; \n"
"this is because in the 1st entry in the type pack, accessing `c.d` results in `string` in the former type and `number` in the latter "
"type, and `string` is not exactly `number`";
else if (FFlag::LuauSolverV2)
expected =
R"(Type pack '{ a: number, b: string, c: { d: string } }' could not be converted into '{ a: number, b: string, c: { d: number } }'; at [0][read "c"][read "d"], string is not exactly number)";
else if (FFlag::LuauImproveTypePathsInErrors)
expected = R"(Type
'{ a: number, b: string, c: { d: string } }'
could not be converted into
'{| a: number, b: string, c: {| d: number |} |}'
caused by:
Property 'c' is not compatible.
Type
'{ d: string }'
could not be converted into
'{| d: number |}'
caused by:
Property 'd' is not compatible.
Type 'string' could not be converted into 'number' in an invariant context)";
else
expected = R"(Type
'{ a: number, b: string, c: { d: string } }'

View file

@ -14,8 +14,7 @@ using namespace Luau;
LUAU_FASTFLAG(LuauStoreCSTData)
LUAU_FASTFLAG(LuauExtendStatEndPosWithSemicolon)
LUAU_FASTFLAG(LuauAstTypeGroup2);
LUAU_FASTFLAG(LexerFixInterpStringStart)
LUAU_FASTFLAG(LuauAstTypeGroup3);
TEST_SUITE_BEGIN("TranspilerTests");
@ -304,6 +303,95 @@ TEST_CASE("function")
CHECK_EQ(two, transpile(two).code);
}
TEST_CASE("function_spaces_around_tokens")
{
ScopedFastFlag _{FFlag::LuauStoreCSTData, true};
const std::string two = R"( function p(o, m, ...) end )";
CHECK_EQ(two, transpile(two).code);
const std::string three = R"( function p( o, m, ...) end )";
CHECK_EQ(three, transpile(three).code);
const std::string four = R"( function p(o , m, ...) end )";
CHECK_EQ(four, transpile(four).code);
const std::string five = R"( function p(o, m, ...) end )";
CHECK_EQ(five, transpile(five).code);
const std::string six = R"( function p(o, m , ...) end )";
CHECK_EQ(six, transpile(six).code);
const std::string seven = R"( function p(o, m, ...) end )";
CHECK_EQ(seven, transpile(seven).code);
const std::string eight = R"( function p(o, m, ... ) end )";
CHECK_EQ(eight, transpile(eight).code);
const std::string nine = R"( function p(o, m, ...) end )";
CHECK_EQ(nine, transpile(nine).code);
}
TEST_CASE("function_with_types_spaces_around_tokens")
{
ScopedFastFlag _{FFlag::LuauStoreCSTData, true};
std::string code = R"( function p<X, Y, Z...>(o: string, m: number, ...: any): string end )";
CHECK_EQ(code, transpile(code, {}, true).code);
code = R"( function p <X, Y, Z...>(o: string, m: number, ...: any): string end )";
CHECK_EQ(code, transpile(code, {}, true).code);
code = R"( function p<X , Y, Z...>(o: string, m: number, ...: any): string end )";
CHECK_EQ(code, transpile(code, {}, true).code);
code = R"( function p<X, Y, Z...>(o: string, m: number, ...: any): string end )";
CHECK_EQ(code, transpile(code, {}, true).code);
code = R"( function p<X, Y, Z...>(o: string, m: number, ...: any): string end )";
CHECK_EQ(code, transpile(code, {}, true).code);
code = R"( function p<X, Y, Z ...>(o: string, m: number, ...: any): string end )";
CHECK_EQ(code, transpile(code, {}, true).code);
code = R"( function p<X, Y, Z... >(o: string, m: number, ...: any): string end )";
CHECK_EQ(code, transpile(code, {}, true).code);
code = R"( function p<X, Y, Z...> (o: string, m: number, ...: any): string end )";
CHECK_EQ(code, transpile(code, {}, true).code);
// TODO(CLI-139347): re-enable test once colon positions are supported
// code = R"( function p<X, Y, Z...>(o : string, m: number, ...: any): string end )";
// CHECK_EQ(code, transpile(code, {}, true).code);
code = R"( function p<X, Y, Z...>(o: string, m: number, ...: any): string end )";
CHECK_EQ(code, transpile(code, {}, true).code);
code = R"( function p<X, Y, Z...>(o: string , m: number, ...: any): string end )";
CHECK_EQ(code, transpile(code, {}, true).code);
code = R"( function p<X, Y, Z...>(o: string, m: number, ...: any): string end )";
CHECK_EQ(code, transpile(code, {}, true).code);
code = R"( function p<X, Y, Z...>(o: string, m: number, ...: any): string end )";
CHECK_EQ(code, transpile(code, {}, true).code);
// TODO(CLI-139347): re-enable test once colon positions are supported
// code = R"( function p<X, Y, Z...>(o: string, m: number, ... : any): string end )";
// CHECK_EQ(code, transpile(code, {}, true).code);
code = R"( function p<X, Y, Z...>(o: string, m: number, ...: any): string end )";
CHECK_EQ(code, transpile(code, {}, true).code);
code = R"( function p<X, Y, Z...>(o: string, m: number, ...: any ): string end )";
CHECK_EQ(code, transpile(code, {}, true).code);
// TODO(CLI-139347): re-enable test once return type positions are supported
// code = R"( function p<X, Y, Z...>(o: string, m: number, ...: any) :string end )";
// CHECK_EQ(code, transpile(code, {}, true).code);
code = R"( function p<X, Y, Z...>(o: string, m: number, ...: any): string end )";
CHECK_EQ(code, transpile(code, {}, true).code);
}
TEST_CASE("returns_spaces_around_tokens")
{
ScopedFastFlag _{FFlag::LuauStoreCSTData, true};
@ -968,7 +1056,17 @@ TEST_CASE_FIXTURE(Fixture, "type_lists_should_be_emitted_correctly")
end
)";
std::string expected = R"(
std::string expected = FFlag::LuauStoreCSTData ? R"(
local a:(a:string,b:number,...string)->(string,...number)=function(a:string,b:number,...:string): (string,...number)
end
local b:(...string)->(...number)=function(...:string): ...number
end
local c:()->()=function(): ()
end
)"
: R"(
local a:(string,number,...string)->(string,...number)=function(a:string,b:number,...:string): (string,...number)
end
@ -1157,6 +1255,49 @@ local b: Packed<(number, string)>
CHECK_EQ(code, transpile(code, {}, true).code);
}
TEST_CASE_FIXTURE(Fixture, "type_packs_spaces_around_tokens")
{
ScopedFastFlag _{FFlag::LuauStoreCSTData, true};
std::string code = R"( type _ = Packed< T...> )";
CHECK_EQ(code, transpile(code, {}, true).code);
code = R"( type _ = Packed<T ...> )";
CHECK_EQ(code, transpile(code, {}, true).code);
code = R"( type _ = Packed< ...T> )";
CHECK_EQ(code, transpile(code, {}, true).code);
code = R"( type _ = Packed<... T> )";
CHECK_EQ(code, transpile(code, {}, true).code);
code = R"( type _ = Packed< ()> )";
CHECK_EQ(code, transpile(code, {}, true).code);
code = R"( type _ = Packed< (string, number)> )";
CHECK_EQ(code, transpile(code, {}, true).code);
code = R"( type _ = Packed<( string, number)> )";
CHECK_EQ(code, transpile(code, {}, true).code);
code = R"( type _ = Packed<(string , number)> )";
CHECK_EQ(code, transpile(code, {}, true).code);
code = R"( type _ = Packed<(string, number)> )";
CHECK_EQ(code, transpile(code, {}, true).code);
code = R"( type _ = Packed<(string, number )> )";
CHECK_EQ(code, transpile(code, {}, true).code);
code = R"( type _ = Packed<(string, number) > )";
CHECK_EQ(code, transpile(code, {}, true).code);
code = R"( type _ = Packed<( )> )";
CHECK_EQ(code, transpile(code, {}, true).code);
code = R"( type _ = Packed<() > )";
CHECK_EQ(code, transpile(code, {}, true).code);
}
TEST_CASE_FIXTURE(Fixture, "transpile_union_type_nested")
{
std::string code = "local a: ((number)->(string))|((string)->(string))";
@ -1175,7 +1316,7 @@ TEST_CASE_FIXTURE(Fixture, "transpile_union_type_nested_3")
{
std::string code = "local a: nil | (string & number)";
if (FFlag::LuauAstTypeGroup2)
if (FFlag::LuauAstTypeGroup3)
CHECK_EQ("local a: (string & number)?", transpile(code, {}, true).code);
else
CHECK_EQ("local a: ( string & number)?", transpile(code, {}, true).code);
@ -1488,7 +1629,6 @@ TEST_CASE_FIXTURE(Fixture, "transpile_string_interp")
{
ScopedFastFlag fflags[] = {
{FFlag::LuauStoreCSTData, true},
{FFlag::LexerFixInterpStringStart, true},
};
std::string code = R"( local _ = `hello {name}` )";
@ -1499,7 +1639,6 @@ TEST_CASE_FIXTURE(Fixture, "transpile_string_interp_multiline")
{
ScopedFastFlag fflags[] = {
{FFlag::LuauStoreCSTData, true},
{FFlag::LexerFixInterpStringStart, true},
};
std::string code = R"( local _ = `hello {
name
@ -1512,7 +1651,6 @@ TEST_CASE_FIXTURE(Fixture, "transpile_string_interp_on_new_line")
{
ScopedFastFlag fflags[] = {
{FFlag::LuauStoreCSTData, true},
{FFlag::LexerFixInterpStringStart, true},
};
std::string code = R"(
error(
@ -1536,7 +1674,6 @@ TEST_CASE_FIXTURE(Fixture, "transpile_string_literal_escape")
{
ScopedFastFlag fflags[] = {
{FFlag::LuauStoreCSTData, true},
{FFlag::LexerFixInterpStringStart, true},
};
std::string code = R"( local _ = ` bracket = \{, backtick = \` = {'ok'} ` )";
@ -1550,6 +1687,22 @@ TEST_CASE_FIXTURE(Fixture, "transpile_type_functions")
CHECK_EQ(code, transpile(code, {}, true).code);
}
TEST_CASE_FIXTURE(Fixture, "transpile_type_functions_spaces_around_tokens")
{
ScopedFastFlag _{FFlag::LuauStoreCSTData, true};
std::string code = R"( type function foo() end )";
CHECK_EQ(code, transpile(code, {}, true).code);
code = R"( type function foo() end )";
CHECK_EQ(code, transpile(code, {}, true).code);
code = R"( type function foo () end )";
CHECK_EQ(code, transpile(code, {}, true).code);
code = R"( export type function foo() end )";
CHECK_EQ(code, transpile(code, {}, true).code);
}
TEST_CASE_FIXTURE(Fixture, "transpile_typeof_spaces_around_tokens")
{
ScopedFastFlag _{FFlag::LuauStoreCSTData, true};
@ -1736,7 +1889,7 @@ TEST_CASE("transpile_types_preserve_parentheses_style")
{
ScopedFastFlag flags[] = {
{FFlag::LuauStoreCSTData, true},
{FFlag::LuauAstTypeGroup2, true},
{FFlag::LuauAstTypeGroup3, true},
};
std::string code = R"( type Foo = number )";
@ -1752,4 +1905,122 @@ TEST_CASE("transpile_types_preserve_parentheses_style")
CHECK_EQ(code, transpile(code, {}, true).code);
}
TEST_CASE("fuzzer_transpile_with_zero_location")
{
const std::string example = R"(
if _ then
elseif _ then
elseif l0 then
else
local function l0<t0>(...):(t0<t0...>,(any)|(<t0>((any)|(<t0>(""[[[[[[[[[[[[[[[[[[[[[[[[!*t")->()))->()))
end
end
)";
Luau::ParseOptions parseOptions;
parseOptions.captureComments = true;
auto allocator = std::make_unique<Luau::Allocator>();
auto names = std::make_unique<Luau::AstNameTable>(*allocator);
ParseResult parseResult = Parser::parse(example.data(), example.size(), *names, *allocator, parseOptions);
transpileWithTypes(*parseResult.root);
}
TEST_CASE("transpile_type_function_unnamed_arguments")
{
ScopedFastFlag _{FFlag::LuauStoreCSTData, true};
std::string code = R"( type Foo = () -> () )";
CHECK_EQ(R"( type Foo = () ->() )", transpile(code, {}, true).code);
code = R"( type Foo = () -> () )";
CHECK_EQ(R"( type Foo = () ->() )", transpile(code, {}, true).code);
code = R"( type Foo = (string) -> () )";
CHECK_EQ(R"( type Foo = (string) ->() )", transpile(code, {}, true).code);
code = R"( type Foo = (string, number) -> () )";
CHECK_EQ(R"( type Foo = (string, number) ->() )", transpile(code, {}, true).code);
code = R"( type Foo = ( string, number) -> () )";
CHECK_EQ(R"( type Foo = ( string, number) ->() )", transpile(code, {}, true).code);
code = R"( type Foo = (string , number) -> () )";
CHECK_EQ(R"( type Foo = (string , number) ->() )", transpile(code, {}, true).code);
code = R"( type Foo = (string, number) -> () )";
CHECK_EQ(R"( type Foo = (string, number) ->() )", transpile(code, {}, true).code);
code = R"( type Foo = (string, number ) -> () )";
CHECK_EQ(R"( type Foo = (string, number ) ->() )", transpile(code, {}, true).code);
code = R"( type Foo = (string, number) -> () )";
CHECK_EQ(R"( type Foo = (string, number) ->() )", transpile(code, {}, true).code);
code = R"( type Foo = (string, number) -> () )";
CHECK_EQ(R"( type Foo = (string, number) ->() )", transpile(code, {}, true).code);
}
TEST_CASE("transpile_type_function_named_arguments")
{
ScopedFastFlag _{FFlag::LuauStoreCSTData, true};
std::string code = R"( type Foo = (x: string) -> () )";
CHECK_EQ(R"( type Foo = (x: string) ->() )", transpile(code, {}, true).code);
code = R"( type Foo = (x: string, y: number) -> () )";
CHECK_EQ(R"( type Foo = (x: string, y: number) ->() )", transpile(code, {}, true).code);
code = R"( type Foo = ( x: string, y: number) -> () )";
CHECK_EQ(R"( type Foo = ( x: string, y: number) ->() )", transpile(code, {}, true).code);
code = R"( type Foo = (x : string, y: number) -> () )";
CHECK_EQ(R"( type Foo = (x : string, y: number) ->() )", transpile(code, {}, true).code);
code = R"( type Foo = (x: string, y: number) -> () )";
CHECK_EQ(R"( type Foo = (x: string, y: number) ->() )", transpile(code, {}, true).code);
code = R"( type Foo = (x: string, y: number) -> () )";
CHECK_EQ(R"( type Foo = (x: string, y: number) ->() )", transpile(code, {}, true).code);
code = R"( type Foo = (number, info: string) -> () )";
CHECK_EQ(R"( type Foo = (number, info: string) ->() )", transpile(code, {}, true).code);
code = R"( type Foo = (first: string, second: string, ...string) -> () )";
CHECK_EQ(R"( type Foo = (first: string, second: string, ...string) ->() )", transpile(code, {}, true).code);
}
TEST_CASE("transpile_type_function_generics")
{
ScopedFastFlag _{FFlag::LuauStoreCSTData, true};
std::string code = R"( type Foo = <X, Y, Z...>() -> () )";
CHECK_EQ(R"( type Foo = <X, Y, Z...>() ->() )", transpile(code, {}, true).code);
code = R"( type Foo = <X, Y, Z...>() -> () )";
CHECK_EQ(R"( type Foo = <X, Y, Z...>() ->() )", transpile(code, {}, true).code);
code = R"( type Foo = < X, Y, Z...>() -> () )";
CHECK_EQ(R"( type Foo = < X, Y, Z...>() ->() )", transpile(code, {}, true).code);
code = R"( type Foo = <X , Y, Z...>() -> () )";
CHECK_EQ(R"( type Foo = <X , Y, Z...>() ->() )", transpile(code, {}, true).code);
code = R"( type Foo = <X, Y, Z...>() -> () )";
CHECK_EQ(R"( type Foo = <X, Y, Z...>() ->() )", transpile(code, {}, true).code);
code = R"( type Foo = <X, Y , Z...>() -> () )";
CHECK_EQ(R"( type Foo = <X, Y , Z...>() ->() )", transpile(code, {}, true).code);
code = R"( type Foo = <X, Y, Z...>() -> () )";
CHECK_EQ(R"( type Foo = <X, Y, Z...>() ->() )", transpile(code, {}, true).code);
code = R"( type Foo = <X, Y, Z ...>() -> () )";
CHECK_EQ(R"( type Foo = <X, Y, Z ...>() ->() )", transpile(code, {}, true).code);
code = R"( type Foo = <X, Y, Z... >() -> () )";
CHECK_EQ(R"( type Foo = <X, Y, Z... >() ->() )", transpile(code, {}, true).code);
code = R"( type Foo = <X, Y, Z...> () -> () )";
CHECK_EQ(R"( type Foo = <X, Y, Z...> () ->() )", transpile(code, {}, true).code);
}
TEST_SUITE_END();

View file

@ -13,8 +13,11 @@
using namespace Luau;
LUAU_FASTFLAG(LuauSolverV2)
LUAU_FASTFLAG(LuauIndexTypeFunctionImprovements)
LUAU_DYNAMIC_FASTINT(LuauTypeFamilyApplicationCartesianProductLimit)
LUAU_FASTFLAG(LuauIndexTypeFunctionFunctionMetamethods)
LUAU_FASTFLAG(LuauMetatableTypeFunctions)
LUAU_FASTFLAG(LuauIndexAnyIsAny)
struct TypeFunctionFixture : Fixture
{
@ -904,6 +907,21 @@ end
LUAU_REQUIRE_NO_ERRORS(result);
}
TEST_CASE_FIXTURE(BuiltinsFixture, "index_of_any_is_any")
{
if (!FFlag::LuauSolverV2)
return;
ScopedFastFlag sff{FFlag::LuauIndexAnyIsAny, true};
CheckResult result = check(R"(
type T = index<any, "a">
)");
LUAU_REQUIRE_NO_ERRORS(result);
CHECK(toString(requireTypeAlias("T")) == "any");
}
TEST_CASE_FIXTURE(BuiltinsFixture, "index_type_function_works")
{
if (!FFlag::LuauSolverV2)
@ -965,6 +983,31 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "index_type_function_works_w_array")
LUAU_REQUIRE_NO_ERRORS(result);
}
TEST_CASE_FIXTURE(BuiltinsFixture, "cyclic_metatable_should_not_crash_index")
{
if (!FFlag::LuauSolverV2)
return;
ScopedFastFlag sff{FFlag::LuauIndexTypeFunctionImprovements, true};
// t :: t1 where t1 = {metatable {__index: t1, __tostring: (t1) -> string}}
CheckResult result = check(R"(
local mt = {}
local t = setmetatable({}, mt)
mt.__index = t
function mt:__tostring()
return t.p
end
type IndexFromT = index<typeof(t), "p">
)");
LUAU_REQUIRE_ERROR_COUNT(2, result);
CHECK_EQ("Type 't' does not have key 'p'", toString(result.errors[0]));
CHECK_EQ("Property '\"p\"' does not exist on type 't'", toString(result.errors[1]));
}
TEST_CASE_FIXTURE(BuiltinsFixture, "index_type_function_works_w_generic_types")
{
if (!FFlag::LuauSolverV2)
@ -1003,6 +1046,61 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "index_type_function_errors_w_bad_indexer")
CHECK(toString(result.errors[1]) == "Property 'boolean' does not exist on type 'MyObject'");
}
TEST_CASE_FIXTURE(BuiltinsFixture, "index_type_function_works_on_function_metamethods")
{
if (!FFlag::LuauSolverV2)
return;
ScopedFastFlag sff[]
{
{FFlag::LuauIndexTypeFunctionFunctionMetamethods, true},
{FFlag::LuauIndexTypeFunctionImprovements, true},
};
CheckResult result = check(R"(
type Foo = {x: string}
local t = {}
setmetatable(t, {
__index = function(x: string): Foo
return {x = x}
end
})
type Bar = index<typeof(t), "bar">
)");
LUAU_REQUIRE_NO_ERRORS(result);
CHECK_EQ(toString(requireTypeAlias("Bar"), {true}), "{ x: string }");
}
TEST_CASE_FIXTURE(BuiltinsFixture, "index_type_function_works_on_function_metamethods2")
{
if (!FFlag::LuauSolverV2)
return;
ScopedFastFlag sff[]
{
{FFlag::LuauIndexTypeFunctionFunctionMetamethods, true},
{FFlag::LuauIndexTypeFunctionImprovements, true},
};
CheckResult result = check(R"(
type Foo = {x: string}
local t = {}
setmetatable(t, {
__index = function(x: string): Foo
return {x = x}
end
})
type Bar = index<typeof(t), number>
)");
LUAU_REQUIRE_ERROR_COUNT(1, result);
}
TEST_CASE_FIXTURE(BuiltinsFixture, "index_type_function_errors_w_var_indexer")
{
if (!FFlag::LuauSolverV2)

View file

@ -7,13 +7,12 @@
using namespace Luau;
LUAU_FASTFLAG(LuauTypeFunFixHydratedClasses)
LUAU_FASTFLAG(LuauSolverV2)
LUAU_FASTFLAG(DebugLuauEqSatSimplification)
LUAU_FASTFLAG(LuauTypeFunSingletonEquality)
LUAU_FASTFLAG(LuauUserTypeFunTypeofReturnsType)
LUAU_FASTFLAG(LuauTypeFunReadWriteParents)
LUAU_FASTFLAG(LuauTypeFunPrintFix)
LUAU_FASTFLAG(LuauImproveTypePathsInErrors)
LUAU_FASTFLAG(LuauUserTypeFunTypecheck)
TEST_SUITE_BEGIN("UserDefinedTypeFunctionTests");
@ -613,9 +612,12 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "udtf_function_methods_work")
ty:setreturns(nil, types.boolean) -- (string, number) -> (...boolean)
if ty:is("function") then
-- creating a copy of `ty` parameters
local arr = {}
for index, val in ty:parameters().head do
table.insert(arr, val)
local arr: {type} = {}
local args = ty:parameters().head
if args then
for index, val in args do
table.insert(arr, val)
end
end
return types.newfunction({head = arr}, ty:returns()) -- (string, number) -> (...boolean)
end
@ -648,7 +650,6 @@ TEST_CASE_FIXTURE(ClassFixture, "udtf_class_serialization_works")
TEST_CASE_FIXTURE(ClassFixture, "udtf_class_serialization_works2")
{
ScopedFastFlag newSolver{FFlag::LuauSolverV2, true};
ScopedFastFlag luauTypeFunFixHydratedClasses{FFlag::LuauTypeFunFixHydratedClasses, true};
CheckResult result = check(R"(
type function serialize_class(arg)
@ -719,7 +720,7 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "udtf_check_mutability")
readresult = types.boolean,
writeresult = types.boolean,
}
local ty = types.newtable(props, indexer, nil) -- {[number]: boolean}
local ty = types.newtable(nil, indexer, nil) -- {[number]: boolean}
ty:setproperty(types.singleton("string"), types.number) -- {string: number, [number]: boolean}
local metatbl = types.newtable(nil, nil, ty) -- { { }, @metatable { [number]: boolean, string: number } }
-- mutate the table
@ -894,6 +895,7 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "udtf_type_overrides_eq_metamethod")
if p1 == p2 and t1 == t2 then
return types.number
end
return types.unknown
end
local function ok(idx: hello<>): number return idx end
)");
@ -988,8 +990,37 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "udtf_calling_each_other_3")
end
)");
LUAU_REQUIRE_ERROR_COUNT(4, result);
CHECK(toString(result.errors[0]) == R"('third' type function errored at runtime: [string "first"]:4: attempt to call a nil value)");
if (FFlag::LuauUserTypeFunTypecheck)
{
LUAU_REQUIRE_ERROR_COUNT(5, result);
CHECK(toString(result.errors[0]) == R"(Unknown global 'fourth')");
CHECK(toString(result.errors[1]) == R"('third' type function errored at runtime: [string "first"]:4: attempt to call a nil value)");
}
else
{
LUAU_REQUIRE_ERROR_COUNT(4, result);
CHECK(toString(result.errors[0]) == R"('third' type function errored at runtime: [string "first"]:4: attempt to call a nil value)");
}
}
TEST_CASE_FIXTURE(BuiltinsFixture, "udtf_calling_each_other_unordered")
{
ScopedFastFlag newSolver{FFlag::LuauSolverV2, true};
CheckResult result = check(R"(
type function bar()
return types.singleton(foo())
end
type function foo()
return "hi"
end
local function ok(idx: bar<>): nil return idx end
)");
LUAU_REQUIRE_ERROR_COUNT(1, result);
TypePackMismatch* tpm = get<TypePackMismatch>(result.errors[0]);
REQUIRE(tpm);
CHECK(toString(tpm->givenTp) == "\"hi\"");
}
TEST_CASE_FIXTURE(BuiltinsFixture, "udtf_no_shared_state")
@ -1013,10 +1044,21 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "udtf_no_shared_state")
local function ok2(idx: bar<'y'>): nil return idx end
)");
// We are only checking first errors, others are mostly duplicates
LUAU_REQUIRE_ERROR_COUNT(8, result);
CHECK(toString(result.errors[0]) == R"('bar' type function errored at runtime: [string "foo"]:4: attempt to modify a readonly table)");
CHECK(toString(result.errors[1]) == R"(Type function instance bar<"x"> is uninhabited)");
if (FFlag::LuauUserTypeFunTypecheck)
{
// We are only checking first errors, others are mostly duplicates
LUAU_REQUIRE_ERROR_COUNT(9, result);
CHECK(toString(result.errors[0]) == R"(Unknown global 'glob')");
CHECK(toString(result.errors[1]) == R"('bar' type function errored at runtime: [string "foo"]:4: attempt to modify a readonly table)");
CHECK(toString(result.errors[2]) == R"(Type function instance bar<"x"> is uninhabited)");
}
else
{
// We are only checking first errors, others are mostly duplicates
LUAU_REQUIRE_ERROR_COUNT(8, result);
CHECK(toString(result.errors[0]) == R"('bar' type function errored at runtime: [string "foo"]:4: attempt to modify a readonly table)");
CHECK(toString(result.errors[1]) == R"(Type function instance bar<"x"> is uninhabited)");
}
}
TEST_CASE_FIXTURE(BuiltinsFixture, "udtf_math_reset")
@ -1075,10 +1117,23 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "udtf_calling_illegal_global")
local function ok(idx: illegal<number>): nil return idx end
)");
LUAU_REQUIRE_ERROR_COUNT(4, result); // There are 2 type function uninhabited error, 2 user defined type function error
UserDefinedTypeFunctionError* e = get<UserDefinedTypeFunctionError>(result.errors[0]);
REQUIRE(e);
CHECK(e->message == "'illegal' type function errored at runtime: [string \"illegal\"]:3: this function is not supported in type functions");
if (FFlag::LuauUserTypeFunTypecheck)
{
// We are only checking first errors, others are mostly duplicates
LUAU_REQUIRE_ERROR_COUNT(5, result);
CHECK(toString(result.errors[0]) == R"(Unknown global 'gcinfo')");
CHECK(
toString(result.errors[1]) ==
R"('illegal' type function errored at runtime: [string "illegal"]:3: this function is not supported in type functions)"
);
}
else
{
LUAU_REQUIRE_ERROR_COUNT(4, result); // There are 2 type function uninhabited error, 2 user defined type function error
UserDefinedTypeFunctionError* e = get<UserDefinedTypeFunctionError>(result.errors[0]);
REQUIRE(e);
CHECK(e->message == "'illegal' type function errored at runtime: [string \"illegal\"]:3: this function is not supported in type functions");
}
}
TEST_CASE_FIXTURE(BuiltinsFixture, "udtf_recursion_and_gc")
@ -1172,7 +1227,7 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "no_type_methods_on_types")
CheckResult result = check(R"(
type function test(x)
return if types.is(x, "number") then types.string else types.boolean
return if (types :: any).is(x, "number") then types.string else types.boolean
end
local function ok(tbl: test<number>): never return tbl end
)");
@ -1243,9 +1298,36 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "tag_field")
)");
LUAU_REQUIRE_ERROR_COUNT(3, result);
CHECK(toString(result.errors[0]) == R"(Type pack '"number"' could not be converted into 'never'; at [0], "number" is not a subtype of never)");
CHECK(toString(result.errors[1]) == R"(Type pack '"string"' could not be converted into 'never'; at [0], "string" is not a subtype of never)");
CHECK(toString(result.errors[2]) == R"(Type pack '"table"' could not be converted into 'never'; at [0], "table" is not a subtype of never)");
if (FFlag::LuauImproveTypePathsInErrors)
{
CHECK(
toString(result.errors[0]) ==
"Type pack '\"number\"' could not be converted into 'never'; \n"
R"(this is because the 1st entry in the type pack is `"number"` in the former type and `never` in the latter type, and `"number"` is not a subtype of `never`)"
);
CHECK(
toString(result.errors[1]) ==
"Type pack '\"string\"' could not be converted into 'never'; \n"
R"(this is because the 1st entry in the type pack is `"string"` in the former type and `never` in the latter type, and `"string"` is not a subtype of `never`)"
);
CHECK(
toString(result.errors[2]) ==
"Type pack '\"table\"' could not be converted into 'never'; \n"
R"(this is because the 1st entry in the type pack is `"table"` in the former type and `never` in the latter type, and `"table"` is not a subtype of `never`)"
);
}
else
{
CHECK(
toString(result.errors[0]) == R"(Type pack '"number"' could not be converted into 'never'; at [0], "number" is not a subtype of never)"
);
CHECK(
toString(result.errors[1]) == R"(Type pack '"string"' could not be converted into 'never'; at [0], "string" is not a subtype of never)"
);
CHECK(toString(result.errors[2]) == R"(Type pack '"table"' could not be converted into 'never'; at [0], "table" is not a subtype of never)");
}
}
TEST_CASE_FIXTURE(BuiltinsFixture, "metatable_serialization")
@ -1293,8 +1375,12 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "implicit_export")
ScopedFastFlag newSolver{FFlag::LuauSolverV2, true};
fileResolver.source["game/A"] = R"(
type function concat(a, b)
return types.singleton(a:value() .. b:value())
type function concat(a: type, b: type)
local as = a:value()
local bs = b:value()
assert(typeof(as) == "string")
assert(typeof(bs) == "string")
return types.singleton(as .. bs)
end
export type Concat<T, U> = concat<T, U>
local a: concat<'first', 'second'>
@ -1342,8 +1428,12 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "explicit_export")
ScopedFastFlag newSolver{FFlag::LuauSolverV2, true};
fileResolver.source["game/A"] = R"(
export type function concat(a, b)
return types.singleton(a:value() .. b:value())
export type function concat(a: type, b: type)
local as = a:value()
local bs = b:value()
assert(typeof(as) == "string")
assert(typeof(bs) == "string")
return types.singleton(as .. bs)
end
local a: concat<'first', 'second'>
return {}
@ -1892,7 +1982,6 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "udtf_eqsat_opaque")
TEST_CASE_FIXTURE(BuiltinsFixture, "udtf_singleton_equality_bool")
{
ScopedFastFlag newSolver{FFlag::LuauSolverV2, true};
ScopedFastFlag luauTypeFunSingletonEquality{FFlag::LuauTypeFunSingletonEquality, true};
CheckResult result = check(R"(
type function compare(arg)
@ -1909,7 +1998,6 @@ local function ok(idx: compare<true>): false return idx end
TEST_CASE_FIXTURE(BuiltinsFixture, "udtf_singleton_equality_string")
{
ScopedFastFlag newSolver{FFlag::LuauSolverV2, true};
ScopedFastFlag luauTypeFunSingletonEquality{FFlag::LuauTypeFunSingletonEquality, true};
CheckResult result = check(R"(
type function compare(arg)
@ -1926,7 +2014,6 @@ local function ok(idx: compare<"a">): false return idx end
TEST_CASE_FIXTURE(BuiltinsFixture, "typeof_type_userdata_returns_type")
{
ScopedFastFlag solverV2{FFlag::LuauSolverV2, true};
ScopedFastFlag luauUserTypeFunTypeofReturnsType{FFlag::LuauUserTypeFunTypeofReturnsType, true};
CheckResult result = check(R"(
type function test(t)
@ -1982,4 +2069,38 @@ TEST_CASE_FIXTURE(ClassFixture, "udtf_class_parent_ops")
LUAU_REQUIRE_NO_ERRORS(result);
}
TEST_CASE_FIXTURE(BuiltinsFixture, "typecheck_success")
{
// Needs new global initialization in the Fixture, but can't place the flag inside the base Fixture
if (!FFlag::LuauUserTypeFunTypecheck)
return;
ScopedFastFlag newSolver{FFlag::LuauSolverV2, true};
CheckResult result = check(R"(
type function foo(x: type)
return types.singleton(x.tag)
end
)");
LUAU_REQUIRE_NO_ERRORS(result);
}
TEST_CASE_FIXTURE(BuiltinsFixture, "typecheck_failure")
{
// Needs new global initialization in the Fixture, but can't place the flag inside the base Fixture
if (!FFlag::LuauUserTypeFunTypecheck)
return;
ScopedFastFlag newSolver{FFlag::LuauSolverV2, true};
CheckResult result = check(R"(
type function foo()
return types.singleton({1})
end
)");
LUAU_REQUIRE_ERROR_COUNT(1, result);
}
TEST_SUITE_END();

View file

@ -11,6 +11,7 @@ using namespace Luau;
LUAU_FASTFLAG(LuauSolverV2)
LUAU_FASTFLAG(LuauFixInfiniteRecursionInNormalization)
LUAU_FASTFLAG(LuauImproveTypePathsInErrors)
TEST_SUITE_BEGIN("TypeAliases");
@ -216,7 +217,11 @@ TEST_CASE_FIXTURE(Fixture, "generic_aliases")
)");
LUAU_REQUIRE_ERROR_COUNT(1, result);
const std::string expected = R"(Type '{ v: string }' could not be converted into 'T<number>'; at [read "v"], string is not exactly number)";
const std::string expected = (FFlag::LuauImproveTypePathsInErrors)
? "Type '{ v: string }' could not be converted into 'T<number>'; \n"
"this is because accessing `v` results in `string` in the former type and `number` in the latter type, and "
"`string` is not exactly `number`"
: R"(Type '{ v: string }' could not be converted into 'T<number>'; at [read "v"], string is not exactly number)";
CHECK(result.errors[0].location == Location{{4, 31}, {4, 44}});
CHECK_EQ(expected, toString(result.errors[0]));
}
@ -236,7 +241,11 @@ TEST_CASE_FIXTURE(Fixture, "dependent_generic_aliases")
LUAU_REQUIRE_ERROR_COUNT(1, result);
const std::string expected =
R"(Type '{ t: { v: string } }' could not be converted into 'U<number>'; at [read "t"][read "v"], string is not exactly number)";
(FFlag::LuauImproveTypePathsInErrors)
? "Type '{ t: { v: string } }' could not be converted into 'U<number>'; \n"
"this is because accessing `t.v` results in `string` in the former type and `number` in the latter type, and `string` is not exactly "
"`number`"
: R"(Type '{ t: { v: string } }' could not be converted into 'U<number>'; at [read "t"][read "v"], string is not exactly number)";
CHECK(result.errors[0].location == Location{{4, 31}, {4, 52}});
CHECK_EQ(expected, toString(result.errors[0]));

View file

@ -12,7 +12,7 @@ using namespace Luau;
LUAU_FASTFLAG(LuauSolverV2)
LUAU_FASTFLAG(LuauTableCloneClonesType3)
LUAU_FASTFLAG(LuauStringFormatErrorSuppression)
LUAU_FASTFLAG(LuauFreezeIgnorePersistent)
LUAU_FASTFLAG(LuauImproveTypePathsInErrors)
TEST_SUITE_BEGIN("BuiltinTests");
@ -146,7 +146,20 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "sort_with_bad_predicate")
)");
LUAU_REQUIRE_ERROR_COUNT(1, result);
const std::string expected = R"(Type
const std::string expected = (FFlag::LuauImproveTypePathsInErrors) ? "Type\n\t"
"'(number, number) -> boolean'"
"\ncould not be converted into\n\t"
"'((string, string) -> boolean)?'"
"\ncaused by:\n"
" None of the union options are compatible. For example:\n"
"Type\n\t"
"'(number, number) -> boolean'"
"\ncould not be converted into\n\t"
"'(string, string) -> boolean'"
"\ncaused by:\n"
" Argument #1 type is not compatible.\n"
"Type 'string' could not be converted into 'number'"
: R"(Type
'(number, number) -> boolean'
could not be converted into
'((string, string) -> boolean)?'
@ -985,7 +998,14 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "tonumber_returns_optional_number_type")
)");
LUAU_REQUIRE_ERROR_COUNT(1, result);
if (FFlag::LuauSolverV2)
if (FFlag::LuauSolverV2 && FFlag::LuauImproveTypePathsInErrors)
CHECK_EQ(
"Type 'number?' could not be converted into 'number'; \n"
"this is because the 2nd component of the union is `nil`, which is not a subtype of `number`",
toString(result.errors[0])
);
else if (FFlag::LuauSolverV2)
CHECK_EQ(
"Type 'number?' could not be converted into 'number'; type number?[1] (nil) is not a subtype of number (number)",
toString(result.errors[0])
@ -1256,8 +1276,6 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "table_freeze_errors_on_non_tables")
TEST_CASE_FIXTURE(BuiltinsFixture, "table_freeze_persistent_skip")
{
ScopedFastFlag luauFreezeIgnorePersistent{FFlag::LuauFreezeIgnorePersistent, true};
CheckResult result = check(R"(
table.freeze(table)
)");
@ -1267,8 +1285,6 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "table_freeze_persistent_skip")
TEST_CASE_FIXTURE(BuiltinsFixture, "table_clone_persistent_skip")
{
ScopedFastFlag luauFreezeIgnorePersistent{FFlag::LuauFreezeIgnorePersistent, true};
CheckResult result = check(R"(
table.clone(table)
)");

View file

@ -13,7 +13,8 @@
using namespace Luau;
using std::nullopt;
LUAU_FASTFLAG(LuauSolverV2);
LUAU_FASTFLAG(LuauSolverV2)
LUAU_FASTFLAG(LuauImproveTypePathsInErrors)
TEST_SUITE_BEGIN("TypeInferClasses");
@ -545,7 +546,13 @@ local b: B = a
LUAU_REQUIRE_ERRORS(result);
if (FFlag::LuauSolverV2)
if (FFlag::LuauSolverV2 && FFlag::LuauImproveTypePathsInErrors)
CHECK(
"Type 'A' could not be converted into 'B'; \n"
"this is because accessing `x` results in `ChildClass` in the former type and `BaseClass` in the latter type, and `ChildClass` is not "
"exactly `BaseClass`" == toString(result.errors.at(0))
);
else if (FFlag::LuauSolverV2)
CHECK(toString(result.errors.at(0)) == "Type 'A' could not be converted into 'B'; at [read \"x\"], ChildClass is not exactly BaseClass");
else
{

View file

@ -9,9 +9,8 @@
using namespace Luau;
LUAU_FASTFLAG(LuauClipNestedAndRecursiveUnion)
LUAU_FASTINT(LuauTypeInferRecursionLimit)
LUAU_FASTFLAG(LuauPreventReentrantTypeFunctionReduction)
LUAU_FASTFLAG(LuauDontForgetToReduceUnionFunc)
TEST_SUITE_BEGIN("DefinitionTests");
@ -544,8 +543,6 @@ TEST_CASE_FIXTURE(Fixture, "definition_file_has_source_module_name_set")
TEST_CASE_FIXTURE(Fixture, "recursive_redefinition_reduces_rightfully")
{
ScopedFastFlag _{FFlag::LuauClipNestedAndRecursiveUnion, true};
LUAU_REQUIRE_NO_ERRORS(check(R"(
local t: {[string]: string} = {}
@ -557,9 +554,40 @@ TEST_CASE_FIXTURE(Fixture, "recursive_redefinition_reduces_rightfully")
)"));
}
TEST_CASE_FIXTURE(BuiltinsFixture, "cli_142285_reduce_minted_union_func")
{
ScopedFastFlag sffs[] = {
{FFlag::LuauSolverV2, true},
{FFlag::LuauDontForgetToReduceUnionFunc, true}
};
CheckResult result = check(R"(
local function middle(a: number, b: number): number
return math.ceil((a + b) / 2 - 0.5)
end
local function find<T>(array: {T}, item: T): number?
local l, m, r = 1, middle(1, #array), #array
while l <= r do
if item <= array[m] then
if item == array[m] then return m end
m, r = middle(l, m-1), m-1
else
l, m = middle(m+1, r), m+1
end
end
return nil
end
)");
LUAU_REQUIRE_ERROR_COUNT(3, result);
// There are three errors in the above snippet, but they should all be where
// clause needed errors.
for (const auto& e: result.errors)
CHECK(get<WhereClauseNeeded>(e));
}
TEST_CASE_FIXTURE(Fixture, "vector3_overflow")
{
ScopedFastFlag _{FFlag::LuauPreventReentrantTypeFunctionReduction, true};
// We set this to zero to ensure that we either run to completion or stack overflow here.
ScopedFastInt sfi{FInt::LuauTypeInferRecursionLimit, 0};

View file

@ -22,8 +22,8 @@ LUAU_FASTFLAG(LuauInstantiateInSubtyping)
LUAU_FASTFLAG(LuauSolverV2)
LUAU_FASTINT(LuauTarjanChildLimit)
LUAU_FASTFLAG(DebugLuauEqSatSimplification)
LUAU_FASTFLAG(LuauSubtypingFixTailPack)
LUAU_FASTFLAG(LuauUngeneralizedTypesForRecursiveFunctions)
LUAU_FASTFLAG(LuauImproveTypePathsInErrors)
TEST_SUITE_BEGIN("TypeInferFunctions");
@ -1306,7 +1306,16 @@ f(function(a, b, c, ...) return a + b end)
LUAU_REQUIRE_ERRORS(result);
std::string expected;
if (FFlag::LuauInstantiateInSubtyping)
if (FFlag::LuauInstantiateInSubtyping && FFlag::LuauImproveTypePathsInErrors)
{
expected = "Type\n\t"
"'<a>(number, number, a) -> number'"
"\ncould not be converted into\n\t"
"'(number, number) -> number'"
"\ncaused by:\n"
" Argument count mismatch. Function expects 3 arguments, but only 2 are specified";
}
else if (FFlag::LuauInstantiateInSubtyping)
{
expected = R"(Type
'<a>(number, number, a) -> number'
@ -1315,6 +1324,15 @@ could not be converted into
caused by:
Argument count mismatch. Function expects 3 arguments, but only 2 are specified)";
}
else if (FFlag::LuauImproveTypePathsInErrors)
{
expected = "Type\n\t"
"'(number, number, *error-type*) -> number'"
"\ncould not be converted into\n\t"
"'(number, number) -> number'"
"\ncaused by:\n"
" Argument count mismatch. Function expects 3 arguments, but only 2 are specified";
}
else
{
expected = R"(Type
@ -1519,7 +1537,14 @@ local b: B = a
)");
LUAU_REQUIRE_ERROR_COUNT(1, result);
const std::string expected = R"(Type
const std::string expected = (FFlag::LuauImproveTypePathsInErrors)
? "Type\n\t"
"'(number, number) -> string'"
"\ncould not be converted into\n\t"
"'(number) -> string'"
"\ncaused by:\n"
" Argument count mismatch. Function expects 2 arguments, but only 1 is specified"
: R"(Type
'(number, number) -> string'
could not be converted into
'(number) -> string'
@ -1542,7 +1567,14 @@ local b: B = a
)");
LUAU_REQUIRE_ERROR_COUNT(1, result);
const std::string expected = R"(Type
const std::string expected = (FFlag::LuauImproveTypePathsInErrors) ? "Type\n\t"
"'(number, number) -> string'"
"\ncould not be converted into\n\t"
"'(number, string) -> string'"
"\ncaused by:\n"
" Argument #2 type is not compatible.\n"
"Type 'string' could not be converted into 'number'"
: R"(Type
'(number, number) -> string'
could not be converted into
'(number, string) -> string'
@ -1566,7 +1598,13 @@ local b: B = a
)");
LUAU_REQUIRE_ERROR_COUNT(1, result);
const std::string expected = R"(Type
const std::string expected = (FFlag::LuauImproveTypePathsInErrors) ? "Type\n\t"
"'(number, number) -> number'"
"\ncould not be converted into\n\t"
"'(number, number) -> (number, boolean)'"
"\ncaused by:\n"
" Function only returns 1 value, but 2 are required here"
: R"(Type
'(number, number) -> number'
could not be converted into
'(number, number) -> (number, boolean)'
@ -1589,7 +1627,14 @@ local b: B = a
)");
LUAU_REQUIRE_ERROR_COUNT(1, result);
const std::string expected = R"(Type
const std::string expected = (FFlag::LuauImproveTypePathsInErrors) ? "Type\n\t"
"'(number, number) -> string'"
"\ncould not be converted into\n\t"
"'(number, number) -> number'"
"\ncaused by:\n"
" Return type is not compatible.\n"
"Type 'string' could not be converted into 'number'"
: R"(Type
'(number, number) -> string'
could not be converted into
'(number, number) -> number'
@ -1613,7 +1658,14 @@ local b: B = a
)");
LUAU_REQUIRE_ERROR_COUNT(1, result);
const std::string expected = R"(Type
const std::string expected = (FFlag::LuauImproveTypePathsInErrors) ? "Type\n\t"
"'(number, number) -> (number, string)'"
"\ncould not be converted into\n\t"
"'(number, number) -> (number, boolean)'"
"\ncaused by:\n"
" Return #2 type is not compatible.\n"
"Type 'string' could not be converted into 'boolean'"
: R"(Type
'(number, number) -> (number, string)'
could not be converted into
'(number, number) -> (number, boolean)'
@ -1769,6 +1821,24 @@ end
R"(Type function instance add<a, number> depends on generic function parameters but does not appear in the function signature; this construct cannot be type-checked at this time)"
);
}
else if (FFlag::LuauImproveTypePathsInErrors)
{
LUAU_REQUIRE_ERROR_COUNT(2, result);
CHECK_EQ(toString(result.errors[0]), R"(Type
'(string) -> string'
could not be converted into
'((number) -> number)?'
caused by:
None of the union options are compatible. For example:
Type
'(string) -> string'
could not be converted into
'(number) -> number'
caused by:
Argument #1 type is not compatible.
Type 'number' could not be converted into 'string')");
CHECK_EQ(toString(result.errors[1]), R"(Type 'string' could not be converted into 'number')");
}
else
{
LUAU_REQUIRE_ERROR_COUNT(2, result);
@ -1812,15 +1882,31 @@ function t:b() return 2 end -- not OK
)");
LUAU_REQUIRE_ERROR_COUNT(1, result);
CHECK_EQ(
R"(Type
if (FFlag::LuauImproveTypePathsInErrors)
{
CHECK_EQ(
"Type\n\t"
"'(*error-type*) -> number'"
"\ncould not be converted into\n\t"
"'() -> number'\n"
"caused by:\n"
" Argument count mismatch. Function expects 1 argument, but none are specified",
toString(result.errors[0])
);
}
else
{
CHECK_EQ(
R"(Type
'(*error-type*) -> number'
could not be converted into
'() -> number'
caused by:
Argument count mismatch. Function expects 1 argument, but none are specified)",
toString(result.errors[0])
);
toString(result.errors[0])
);
}
}
TEST_CASE_FIXTURE(Fixture, "too_few_arguments_variadic")
@ -2078,7 +2164,13 @@ z = y -- Not OK, so the line is colorable
)");
LUAU_REQUIRE_ERROR_COUNT(1, result);
const std::string expected = R"(Type
const std::string expected =
(FFlag::LuauImproveTypePathsInErrors)
? "Type\n\t"
R"('(("blue" | "red") -> ("blue" | "red") -> ("blue" | "red") -> boolean) & (("blue" | "red") -> ("blue") -> ("blue") -> false) & (("blue" | "red") -> ("red") -> ("red") -> false) & (("blue") -> ("blue") -> ("blue" | "red") -> false) & (("red") -> ("red") -> ("blue" | "red") -> false)')"
"\ncould not be converted into\n\t"
R"('("blue" | "red") -> ("blue" | "red") -> ("blue" | "red") -> false'; none of the intersection parts are compatible)"
: R"(Type
'(("blue" | "red") -> ("blue" | "red") -> ("blue" | "red") -> boolean) & (("blue" | "red") -> ("blue") -> ("blue") -> false) & (("blue" | "red") -> ("red") -> ("red") -> false) & (("blue") -> ("blue") -> ("blue" | "red") -> false) & (("red") -> ("red") -> ("blue" | "red") -> false)'
could not be converted into
'("blue" | "red") -> ("blue" | "red") -> ("blue" | "red") -> false'; none of the intersection parts are compatible)";
@ -2412,7 +2504,14 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "num_is_solved_before_num_or_str")
)");
LUAU_REQUIRE_ERROR_COUNT(1, result);
if (FFlag::LuauSolverV2)
if (FFlag::LuauSolverV2 && FFlag::LuauImproveTypePathsInErrors)
CHECK(
"Type pack 'string' could not be converted into 'number'; \n"
"this is because the 1st entry in the type pack is `string` in the former type and `number` in the latter type, and `string` is not a "
"subtype of `number`" == toString(result.errors.at(0))
);
else if (FFlag::LuauSolverV2)
CHECK(toString(result.errors.at(0)) == "Type pack 'string' could not be converted into 'number'; at [0], string is not a subtype of number");
else
CHECK_EQ("Type 'string' could not be converted into 'number'", toString(result.errors[0]));
@ -2437,7 +2536,13 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "num_is_solved_after_num_or_str")
)");
LUAU_REQUIRE_ERROR_COUNT(1, result);
if (FFlag::LuauSolverV2)
if (FFlag::LuauSolverV2 && FFlag::LuauImproveTypePathsInErrors)
CHECK(
"Type pack 'string' could not be converted into 'number'; \n"
"this is because the 1st entry in the type pack is `string` in the former type and `number` in the latter type, and `string` is not a "
"subtype of `number`" == toString(result.errors.at(0))
);
else if (FFlag::LuauSolverV2)
CHECK(toString(result.errors.at(0)) == "Type pack 'string' could not be converted into 'number'; at [0], string is not a subtype of number");
else
CHECK_EQ("Type 'string' could not be converted into 'number'", toString(result.errors[0]));
@ -3033,8 +3138,6 @@ TEST_CASE_FIXTURE(Fixture, "hidden_variadics_should_not_break_subtyping")
TEST_CASE_FIXTURE(BuiltinsFixture, "coroutine_wrap_result_call")
{
ScopedFastFlag luauSubtypingFixTailPack{FFlag::LuauSubtypingFixTailPack, true};
CheckResult result = check(R"(
function foo(a, b)
coroutine.wrap(a)(b)

View file

@ -10,9 +10,10 @@
#include "ScopedFlags.h"
#include "doctest.h"
LUAU_FASTFLAG(LuauInstantiateInSubtyping);
LUAU_FASTFLAG(LuauSolverV2);
LUAU_FASTFLAG(LuauInstantiateInSubtyping)
LUAU_FASTFLAG(LuauSolverV2)
LUAU_FASTFLAG(LuauDeferBidirectionalInferenceForTableAssignment)
LUAU_FASTFLAG(LuauImproveTypePathsInErrors)
using namespace Luau;
@ -875,7 +876,13 @@ y.a.c = y
CHECK(mismatch);
CHECK_EQ(toString(mismatch->givenType), "{ a: { c: T<string>?, d: number }, b: number }");
CHECK_EQ(toString(mismatch->wantedType), "T<string>");
std::string reason = "at [read \"a\"][read \"d\"], number is not exactly string\n\tat [read \"b\"], number is not exactly string";
std::string reason =
(FFlag::LuauImproveTypePathsInErrors)
? "\nthis is because \n\t"
" * accessing `a.d` results in `number` in the former type and `string` in the latter type, and `number` is not exactly "
"`string`\n\t"
" * accessing `b` results in `number` in the former type and `string` in the latter type, and `number` is not exactly `string`"
: "at [read \"a\"][read \"d\"], number is not exactly string\n\tat [read \"b\"], number is not exactly string";
CHECK_EQ(mismatch->reason, reason);
}
else

View file

@ -9,7 +9,8 @@
using namespace Luau;
LUAU_FASTFLAG(LuauSolverV2);
LUAU_FASTFLAG(LuauSolverV2)
LUAU_FASTFLAG(LuauImproveTypePathsInErrors)
TEST_SUITE_BEGIN("IntersectionTypes");
@ -357,12 +358,21 @@ TEST_CASE_FIXTURE(Fixture, "table_intersection_write_sealed_indirect")
else
{
LUAU_REQUIRE_ERROR_COUNT(4, result);
const std::string expected = R"(Type
const std::string expected = (FFlag::LuauImproveTypePathsInErrors)
? "Type\n\t"
"'(string, number) -> string'"
"\ncould not be converted into\n\t"
"'(string) -> string'\n"
"caused by:\n"
" Argument count mismatch. Function expects 2 arguments, but only 1 is specified"
: R"(Type
'(string, number) -> string'
could not be converted into
'(string) -> string'
caused by:
Argument count mismatch. Function expects 2 arguments, but only 1 is specified)";
CHECK_EQ(expected, toString(result.errors[0]));
CHECK_EQ(toString(result.errors[1]), "Cannot add property 'z' to table 'X & Y'");
CHECK_EQ(toString(result.errors[2]), "Type 'number' could not be converted into 'string'");
@ -387,7 +397,14 @@ TEST_CASE_FIXTURE(Fixture, "table_write_sealed_indirect")
)");
LUAU_REQUIRE_ERROR_COUNT(4, result);
const std::string expected = R"(Type
const std::string expected = (FFlag::LuauImproveTypePathsInErrors)
? "Type\n\t"
"'(string, number) -> string'"
"\ncould not be converted into\n\t"
"'(string) -> string'\n"
"caused by:\n"
" Argument count mismatch. Function expects 2 arguments, but only 1 is specified"
: R"(Type
'(string, number) -> string'
could not be converted into
'(string) -> string'
@ -430,7 +447,20 @@ Type 'number' could not be converted into 'X')";
R"(Type 'number' could not be converted into 'X & Y & Z'; type number (number) is not a subtype of X & Y & Z[0] (X)
type number (number) is not a subtype of X & Y & Z[1] (Y)
type number (number) is not a subtype of X & Y & Z[2] (Z))";
if (FFlag::LuauSolverV2)
if (FFlag::LuauSolverV2 && FFlag::LuauImproveTypePathsInErrors)
{
const std::string expected = "Type "
"'number'"
" could not be converted into "
"'X & Y & Z'; \n"
"this is because \n\t"
" * the 1st component of the intersection is `X`, and `number` is not a subtype of `X`\n\t"
" * the 2nd component of the intersection is `Y`, and `number` is not a subtype of `Y`\n\t"
" * the 3rd component of the intersection is `Z`, and `number` is not a subtype of `Z`";
CHECK_EQ(expected, toString(result.errors[0]));
}
else if (FFlag::LuauSolverV2)
CHECK_EQ(dcrExprected, toString(result.errors[0]));
else
CHECK_EQ(expected, toString(result.errors[0]));
@ -450,7 +480,23 @@ end
)");
LUAU_REQUIRE_ERROR_COUNT(1, result);
if (FFlag::LuauSolverV2)
if (FFlag::LuauSolverV2 && FFlag::LuauImproveTypePathsInErrors)
{
const std::string expected = "Type pack "
"'X & Y & Z'"
" could not be converted into "
"'number'; \n"
"this is because \n\t"
" * in the 1st entry in the type pack has the 1st component of the intersection as `X` and the 1st entry in the "
"type pack is `number`, and `X` is not a subtype of `number`\n\t"
" * in the 1st entry in the type pack has the 2nd component of the intersection as `Y` and the 1st entry in the "
"type pack is `number`, and `Y` is not a subtype of `number`\n\t"
" * in the 1st entry in the type pack has the 3rd component of the intersection as `Z` and the 1st entry in the "
"type pack is `number`, and `Z` is not a subtype of `number`";
CHECK_EQ(expected, toString(result.errors[0]));
}
else if (FFlag::LuauSolverV2)
{
CHECK_EQ(
R"(Type pack 'X & Y & Z' could not be converted into 'number'; type X & Y & Z[0][0] (X) is not a subtype of number[0] (number)
@ -503,7 +549,19 @@ TEST_CASE_FIXTURE(Fixture, "intersect_bool_and_false")
)");
LUAU_REQUIRE_ERROR_COUNT(1, result);
if (FFlag::LuauSolverV2)
if (FFlag::LuauSolverV2 && FFlag::LuauImproveTypePathsInErrors)
{
const std::string expected = "Type "
"'boolean & false'"
" could not be converted into "
"'true'; \n"
"this is because \n\t"
" * the 1st component of the intersection is `boolean`, which is not a subtype of `true`\n\t"
" * the 2nd component of the intersection is `false`, which is not a subtype of `true`";
CHECK_EQ(expected, toString(result.errors[0]));
}
else if (FFlag::LuauSolverV2)
{
CHECK_EQ(
R"(Type 'boolean & false' could not be converted into 'true'; type boolean & false[0] (boolean) is not a subtype of true (true)
@ -527,8 +585,21 @@ TEST_CASE_FIXTURE(Fixture, "intersect_false_and_bool_and_false")
)");
LUAU_REQUIRE_ERROR_COUNT(1, result);
// TODO: odd stringification of `false & (boolean & false)`.)
if (FFlag::LuauSolverV2)
if (FFlag::LuauSolverV2 && FFlag::LuauImproveTypePathsInErrors)
{
const std::string expected = "Type "
"'boolean & false & false'"
" could not be converted into "
"'true'; \n"
"this is because \n\t"
" * the 1st component of the intersection is `false`, which is not a subtype of `true`\n\t"
" * the 2nd component of the intersection is `boolean`, which is not a subtype of `true`\n\t"
" * the 3rd component of the intersection is `false`, which is not a subtype of `true`";
CHECK_EQ(expected, toString(result.errors[0]));
}
else if (FFlag::LuauSolverV2)
CHECK_EQ(
R"(Type 'boolean & false & false' could not be converted into 'true'; type boolean & false & false[0] (false) is not a subtype of true (true)
type boolean & false & false[1] (boolean) is not a subtype of true (true)
@ -550,7 +621,39 @@ TEST_CASE_FIXTURE(Fixture, "intersect_saturate_overloaded_functions")
local z : (number) -> number = x -- Not OK
end
)");
if (FFlag::LuauSolverV2)
if (FFlag::LuauSolverV2 && FFlag::LuauImproveTypePathsInErrors)
{
const std::string expected1 =
"Type\n\t"
"'((number?) -> number?) & ((string?) -> string?)'"
"\ncould not be converted into\n\t"
"'(nil) -> nil'; \n"
"this is because \n\t"
" * in the 1st component of the intersection, the function returns the 1st entry in the type pack which has the 1st component of the "
"union as `number` and it returns the 1st entry in the type pack is `nil`, and `number` is not a subtype of `nil`\n\t"
" * in the 2nd component of the intersection, the function returns the 1st entry in the type pack which has the 1st component of the "
"union as `string` and it returns the 1st entry in the type pack is `nil`, and `string` is not a subtype of `nil`";
const std::string expected2 =
"Type\n\t"
"'((number?) -> number?) & ((string?) -> string?)'"
"\ncould not be converted into\n\t"
"'(number) -> number'; \n"
"this is because \n\t"
" * in the 1st component of the intersection, the function returns the 1st entry in the type pack which has the 2nd component of the "
"union as `nil` and it returns the 1st entry in the type pack is `number`, and `nil` is not a subtype of `number`\n\t"
" * in the 2nd component of the intersection, the function returns the 1st entry in the type pack which has the 1st component of the "
"union as `string` and it returns the 1st entry in the type pack is `number`, and `string` is not a subtype of `number`\n\t"
" * in the 2nd component of the intersection, the function returns the 1st entry in the type pack which has the 2nd component of the "
"union as `nil` and it returns the 1st entry in the type pack is `number`, and `nil` is not a subtype of `number`\n\t"
" * in the 2nd component of the intersection, the function takes the 1st entry in the type pack which is `string?` and it takes the 1st "
"entry in the type pack is `number`, and `string?` is not a supertype of `number`";
CHECK_EQ(expected1, toString(result.errors[0]));
CHECK_EQ(expected2, toString(result.errors[1]));
}
else if (FFlag::LuauSolverV2)
{
LUAU_REQUIRE_ERROR_COUNT(2, result);
const std::string expected1 = R"(Type
@ -568,6 +671,15 @@ could not be converted into
CHECK_EQ(expected1, toString(result.errors[0]));
CHECK_EQ(expected2, toString(result.errors[1]));
}
else if (FFlag::LuauImproveTypePathsInErrors)
{
LUAU_REQUIRE_ERROR_COUNT(1, result);
const std::string expected = R"(Type
'((number?) -> number?) & ((string?) -> string?)'
could not be converted into
'(number) -> number'; none of the intersection parts are compatible)";
CHECK_EQ(expected, toString(result.errors[0]));
}
else
{
LUAU_REQUIRE_ERROR_COUNT(1, result);
@ -592,11 +704,23 @@ TEST_CASE_FIXTURE(Fixture, "union_saturate_overloaded_functions")
)");
LUAU_REQUIRE_ERROR_COUNT(1, result);
const std::string expected = R"(Type
if (FFlag::LuauImproveTypePathsInErrors)
{
const std::string expected = "Type\n\t"
"'((number) -> number) & ((string) -> string)'"
"\ncould not be converted into\n\t"
"'(boolean | number) -> boolean | number'; none of the intersection parts are compatible";
CHECK_EQ(expected, toString(result.errors[0]));
}
else
{
const std::string expected = R"(Type
'((number) -> number) & ((string) -> string)'
could not be converted into
'(boolean | number) -> boolean | number'; none of the intersection parts are compatible)";
CHECK_EQ(expected, toString(result.errors[0]));
CHECK_EQ(expected, toString(result.errors[0]));
}
}
TEST_CASE_FIXTURE(Fixture, "intersection_of_tables")
@ -609,16 +733,42 @@ TEST_CASE_FIXTURE(Fixture, "intersection_of_tables")
)");
LUAU_REQUIRE_ERROR_COUNT(1, result);
const std::string expected =
(FFlag::LuauSolverV2)
? R"(Type '{ p: number?, q: number?, r: number? } & { p: number?, q: string? }' could not be converted into '{ p: nil }'; type { p: number?, q: number?, r: number? } & { p: number?, q: string? }[0][read "p"][0] (number) is not exactly { p: nil }[read "p"] (nil)
type { p: number?, q: number?, r: number? } & { p: number?, q: string? }[1][read "p"][0] (number) is not exactly { p: nil }[read "p"] (nil))"
:
if (FFlag::LuauSolverV2 && FFlag::LuauImproveTypePathsInErrors)
{
const std::string expected = "Type "
"'{ p: number?, q: number?, r: number? } & { p: number?, q: string? }'"
" could not be converted into "
"'{ p: nil }'; \n"
"this is because \n\t"
" * in the 1st component of the intersection, accessing `p` has the 1st component of the union as `number` and "
"accessing `p` results in `nil`, and `number` is not exactly `nil`\n\t"
" * in the 2nd component of the intersection, accessing `p` has the 1st component of the union as `number` and "
"accessing `p` results in `nil`, and `number` is not exactly `nil`";
CHECK_EQ(expected, toString(result.errors[0]));
}
else if (FFlag::LuauImproveTypePathsInErrors)
{
const std::string expected =
R"(Type
'{| p: number?, q: number?, r: number? |} & {| p: number?, q: string? |}'
could not be converted into
'{| p: nil |}'; none of the intersection parts are compatible)";
CHECK_EQ(expected, toString(result.errors[0]));
}
else
{
const std::string expected =
(FFlag::LuauSolverV2)
? R"(Type '{ p: number?, q: number?, r: number? } & { p: number?, q: string? }' could not be converted into '{ p: nil }'; type { p: number?, q: number?, r: number? } & { p: number?, q: string? }[0][read "p"][0] (number) is not exactly { p: nil }[read "p"] (nil)
type { p: number?, q: number?, r: number? } & { p: number?, q: string? }[1][read "p"][0] (number) is not exactly { p: nil }[read "p"] (nil))"
:
R"(Type
'{| p: number?, q: number?, r: number? |} & {| p: number?, q: string? |}'
could not be converted into
'{| p: nil |}'; none of the intersection parts are compatible)";
CHECK_EQ(expected, toString(result.errors[0]));
CHECK_EQ(expected, toString(result.errors[0]));
}
}
TEST_CASE_FIXTURE(Fixture, "intersection_of_tables_with_top_properties")
@ -630,7 +780,28 @@ TEST_CASE_FIXTURE(Fixture, "intersection_of_tables_with_top_properties")
end
)");
if (FFlag::LuauSolverV2)
if (FFlag::LuauSolverV2 && FFlag::LuauImproveTypePathsInErrors)
{
const std::string expected = "Type\n\t"
"'{ p: number?, q: any } & { p: unknown, q: string? }'"
"\ncould not be converted into\n\t"
"'{ p: string?, q: number? }'; \n"
"this is because \n\t"
" * in the 1st component of the intersection, accessing `p` has the 1st component of the union as `number` and "
"accessing `p` results in `string?`, and `number` is not exactly `string?`\n\t"
" * in the 1st component of the intersection, accessing `p` results in `number?` and accessing `p` has the 1st "
"component of the union as `string`, and `number?` is not exactly `string`\n\t"
" * in the 1st component of the intersection, accessing `q` results in `any` and accessing `q` results in "
"`number?`, and `any` is not exactly `number?`\n\t"
" * in the 2nd component of the intersection, accessing `p` results in `unknown` and accessing `p` results in "
"`string?`, and `unknown` is not exactly `string?`\n\t"
" * in the 2nd component of the intersection, accessing `q` has the 1st component of the union as `string` and "
"accessing `q` results in `number?`, and `string` is not exactly `number?`\n\t"
" * in the 2nd component of the intersection, accessing `q` results in `string?` and accessing `q` has the 1st "
"component of the union as `number`, and `string?` is not exactly `number`";
CHECK_EQ(expected, toString(result.errors[0]));
}
else if (FFlag::LuauSolverV2)
{
LUAU_REQUIRE_ERROR_COUNT(1, result);
CHECK_EQ(
@ -646,6 +817,15 @@ could not be converted into
toString(result.errors[0])
);
}
else if (FFlag::LuauImproveTypePathsInErrors)
{
LUAU_REQUIRE_ERROR_COUNT(1, result);
const std::string expected = R"(Type
'{| p: number?, q: any |} & {| p: unknown, q: string? |}'
could not be converted into
'{| p: string?, q: number? |}'; none of the intersection parts are compatible)";
CHECK_EQ(expected, toString(result.errors[0]));
}
else
{
LUAU_REQUIRE_ERROR_COUNT(1, result);
@ -678,7 +858,52 @@ TEST_CASE_FIXTURE(Fixture, "overloaded_functions_returning_intersections")
end
)");
if (FFlag::LuauSolverV2)
if (FFlag::LuauSolverV2 && FFlag::LuauImproveTypePathsInErrors)
{
const std::string expected1 =
"Type\n\t"
"'((number?) -> { p: number } & { q: number }) & ((string?) -> { p: number } & { r: number })'"
"\ncould not be converted into\n\t"
"'(nil) -> { p: number, q: number, r: number }'; \n"
"this is because \n\t"
" * in the 1st component of the intersection, the function returns the 1st entry in the type pack which has the 1st component of the "
"intersection as `{ p: number }` and it returns the 1st entry in the type pack is `{ p: number, q: number, r: number }`, and `{ p: "
"number }` is not a subtype of `{ p: number, q: number, r: number }`\n\t"
" * in the 1st component of the intersection, the function returns the 1st entry in the type pack which has the 2nd component of the "
"intersection as `{ q: number }` and it returns the 1st entry in the type pack is `{ p: number, q: number, r: number }`, and `{ q: "
"number }` is not a subtype of `{ p: number, q: number, r: number }`\n\t"
" * in the 2nd component of the intersection, the function returns the 1st entry in the type pack which has the 1st component of the "
"intersection as `{ p: number }` and it returns the 1st entry in the type pack is `{ p: number, q: number, r: number }`, and `{ p: "
"number }` is not a subtype of `{ p: number, q: number, r: number }`\n\t"
" * in the 2nd component of the intersection, the function returns the 1st entry in the type pack which has the 2nd component of the "
"intersection as `{ r: number }` and it returns the 1st entry in the type pack is `{ p: number, q: number, r: number }`, and `{ r: "
"number }` is not a subtype of `{ p: number, q: number, r: number }`";
const std::string expected2 =
"Type\n\t"
"'((number?) -> { p: number } & { q: number }) & ((string?) -> { p: number } & { r: number })'"
"\ncould not be converted into\n\t"
"'(number?) -> { p: number, q: number, r: number }'; \n"
"this is because \n\t"
" * in the 1st component of the intersection, the function returns the 1st entry in the type pack which has the 1st component of the "
"intersection as `{ p: number }` and it returns the 1st entry in the type pack is `{ p: number, q: number, r: number }`, and `{ p: "
"number }` is not a subtype of `{ p: number, q: number, r: number }`\n\t"
" * in the 1st component of the intersection, the function returns the 1st entry in the type pack which has the 2nd component of the "
"intersection as `{ q: number }` and it returns the 1st entry in the type pack is `{ p: number, q: number, r: number }`, and `{ q: "
"number }` is not a subtype of `{ p: number, q: number, r: number }`\n\t"
" * in the 2nd component of the intersection, the function returns the 1st entry in the type pack which has the 1st component of the "
"intersection as `{ p: number }` and it returns the 1st entry in the type pack is `{ p: number, q: number, r: number }`, and `{ p: "
"number }` is not a subtype of `{ p: number, q: number, r: number }`\n\t"
" * in the 2nd component of the intersection, the function returns the 1st entry in the type pack which has the 2nd component of the "
"intersection as `{ r: number }` and it returns the 1st entry in the type pack is `{ p: number, q: number, r: number }`, and `{ r: "
"number }` is not a subtype of `{ p: number, q: number, r: number }`\n\t"
" * in the 2nd component of the intersection, the function takes the 1st entry in the type pack which is `string?` and it takes the 1st "
"entry in the type pack has the 1st component of the union as `number`, and `string?` is not a supertype of `number`";
CHECK_EQ(expected1, toString(result.errors[0]));
CHECK_EQ(expected2, toString(result.errors[1]));
}
else if (FFlag::LuauSolverV2)
{
LUAU_REQUIRE_ERROR_COUNT(2, result);
CHECK_EQ(
@ -703,6 +928,17 @@ could not be converted into
toString(result.errors[1])
);
}
else if (FFlag::LuauImproveTypePathsInErrors)
{
LUAU_REQUIRE_ERROR_COUNT(1, result);
CHECK_EQ(
R"(Type
'((number?) -> {| p: number |} & {| q: number |}) & ((string?) -> {| p: number |} & {| r: number |})'
could not be converted into
'(number?) -> {| p: number, q: number, r: number |}'; none of the intersection parts are compatible)",
toString(result.errors[0])
);
}
else
{
LUAU_REQUIRE_ERROR_COUNT(1, result);
@ -730,6 +966,15 @@ TEST_CASE_FIXTURE(Fixture, "overloaded_functions_mentioning_generic")
{
LUAU_REQUIRE_ERROR_COUNT(0, result);
}
else if (FFlag::LuauImproveTypePathsInErrors)
{
LUAU_REQUIRE_ERROR_COUNT(1, result);
const std::string expected = R"(Type
'((number?) -> a | number) & ((string?) -> a | string)'
could not be converted into
'(number?) -> a'; none of the intersection parts are compatible)";
CHECK_EQ(expected, toString(result.errors[0]));
}
else
{
LUAU_REQUIRE_ERROR_COUNT(1, result);
@ -757,6 +1002,15 @@ TEST_CASE_FIXTURE(Fixture, "overloaded_functions_mentioning_generics")
{
LUAU_REQUIRE_NO_ERRORS(result);
}
else if (FFlag::LuauImproveTypePathsInErrors)
{
LUAU_REQUIRE_ERROR_COUNT(1, result);
const std::string expected = R"(Type
'((a?) -> a | b) & ((c?) -> b | c)'
could not be converted into
'(a?) -> (a & c) | b'; none of the intersection parts are compatible)";
CHECK_EQ(expected, toString(result.errors[0]));
}
else
{
LUAU_REQUIRE_ERROR_COUNT(1, result);
@ -778,7 +1032,35 @@ TEST_CASE_FIXTURE(Fixture, "overloaded_functions_mentioning_generic_packs")
end
end
)");
if (FFlag::LuauSolverV2)
if (FFlag::LuauSolverV2 && FFlag::LuauImproveTypePathsInErrors)
{
const std::string expected1 =
"Type\n\t"
"'((number?, a...) -> (number?, b...)) & ((string?, a...) -> (string?, b...))'"
"\ncould not be converted into\n\t"
"'(nil, a...) -> (nil, b...)'; \n"
"this is because \n\t"
" * in the 1st component of the intersection, the function returns the 1st entry in the type pack which has the 1st component of the "
"union as `number` and it returns the 1st entry in the type pack is `nil`, and `number` is not a subtype of `nil`\n\t"
" * in the 2nd component of the intersection, the function returns the 1st entry in the type pack which has the 1st component of the "
"union as `string` and it returns the 1st entry in the type pack is `nil`, and `string` is not a subtype of `nil`";
const std::string expected2 =
"Type\n\t"
"'((number?, a...) -> (number?, b...)) & ((string?, a...) -> (string?, b...))'"
"\ncould not be converted into\n\t"
"'(nil, b...) -> (nil, a...)'; \n"
"this is because \n\t"
" * in the 1st component of the intersection, the function returns the 1st entry in the type pack which has the 1st component of the "
"union as `number` and it returns the 1st entry in the type pack is `nil`, and `number` is not a subtype of `nil`\n\t"
" * in the 2nd component of the intersection, the function returns the 1st entry in the type pack which has the 1st component of the "
"union as `string` and it returns the 1st entry in the type pack is `nil`, and `string` is not a subtype of `nil`";
CHECK_EQ(expected1, toString(result.errors[0]));
CHECK_EQ(expected2, toString(result.errors[1]));
}
else if (FFlag::LuauSolverV2)
{
LUAU_REQUIRE_ERROR_COUNT(2, result);
CHECK_EQ(
@ -798,6 +1080,15 @@ could not be converted into
toString(result.errors[1])
);
}
else if (FFlag::LuauImproveTypePathsInErrors)
{
LUAU_REQUIRE_ERROR_COUNT(1, result);
const std::string expected = R"(Type
'((number?, a...) -> (number?, b...)) & ((string?, a...) -> (string?, b...))'
could not be converted into
'(nil, b...) -> (nil, a...)'; none of the intersection parts are compatible)";
CHECK_EQ(expected, toString(result.errors[0]));
}
else
{
LUAU_REQUIRE_ERROR_COUNT(1, result);
@ -824,11 +1115,23 @@ TEST_CASE_FIXTURE(Fixture, "overloadeded_functions_with_unknown_result")
)");
LUAU_REQUIRE_ERROR_COUNT(1, result);
const std::string expected = R"(Type
if (FFlag::LuauImproveTypePathsInErrors)
{
const std::string expected = "Type\n\t"
"'((nil) -> unknown) & ((number) -> number)'"
"\ncould not be converted into\n\t"
"'(number?) -> number?'; none of the intersection parts are compatible";
CHECK_EQ(expected, toString(result.errors[0]));
}
else
{
const std::string expected = R"(Type
'((nil) -> unknown) & ((number) -> number)'
could not be converted into
'(number?) -> number?'; none of the intersection parts are compatible)";
CHECK_EQ(expected, toString(result.errors[0]));
CHECK_EQ(expected, toString(result.errors[0]));
}
}
TEST_CASE_FIXTURE(Fixture, "overloadeded_functions_with_unknown_arguments")
@ -846,11 +1149,23 @@ TEST_CASE_FIXTURE(Fixture, "overloadeded_functions_with_unknown_arguments")
)");
LUAU_REQUIRE_ERROR_COUNT(1, result);
const std::string expected = R"(Type
if (FFlag::LuauImproveTypePathsInErrors)
{
const std::string expected = "Type\n\t"
"'((number) -> number?) & ((unknown) -> string?)'"
"\ncould not be converted into\n\t"
"'(number?) -> nil'; none of the intersection parts are compatible";
CHECK_EQ(expected, toString(result.errors[0]));
}
else
{
const std::string expected = R"(Type
'((number) -> number?) & ((unknown) -> string?)'
could not be converted into
'(number?) -> nil'; none of the intersection parts are compatible)";
CHECK_EQ(expected, toString(result.errors[0]));
CHECK_EQ(expected, toString(result.errors[0]));
}
}
TEST_CASE_FIXTURE(Fixture, "overloadeded_functions_with_never_result")
@ -864,7 +1179,36 @@ TEST_CASE_FIXTURE(Fixture, "overloadeded_functions_with_never_result")
end
)");
if (FFlag::LuauSolverV2)
if (FFlag::LuauSolverV2 && FFlag::LuauImproveTypePathsInErrors)
{
const std::string expected1 =
"Type\n\t"
"'((nil) -> never) & ((number) -> number)'"
"\ncould not be converted into\n\t"
"'(number?) -> number'; \n"
"this is because \n\t"
" * in the 1st component of the intersection, the function takes the 1st entry in the type pack which is `number` and it takes the 1st "
"entry in the type pack has the 2nd component of the union as `nil`, and `number` is not a supertype of `nil`\n\t"
" * in the 2nd component of the intersection, the function takes the 1st entry in the type pack which is `nil` and it takes the 1st "
"entry in the type pack has the 1st component of the union as `number`, and `nil` is not a supertype of `number`";
const std::string expected2 =
"Type\n\t"
"'((nil) -> never) & ((number) -> number)'"
"\ncould not be converted into\n\t"
"'(number?) -> never'; \n"
"this is because \n\t"
" * in the 1st component of the intersection, the function returns the 1st entry in the type pack which is `number` and it returns the "
"1st entry in the type pack is `never`, and `number` is not a subtype of `never`\n\t"
" * in the 1st component of the intersection, the function takes the 1st entry in the type pack which is `number` and it takes the 1st "
"entry in the type pack has the 2nd component of the union as `nil`, and `number` is not a supertype of `nil`\n\t"
" * in the 2nd component of the intersection, the function takes the 1st entry in the type pack which is `nil` and it takes the 1st "
"entry in the type pack has the 1st component of the union as `number`, and `nil` is not a supertype of `number`";
CHECK_EQ(expected1, toString(result.errors[0]));
CHECK_EQ(expected2, toString(result.errors[1]));
}
else if (FFlag::LuauSolverV2)
{
LUAU_REQUIRE_ERROR_COUNT(2, result);
CHECK_EQ(
@ -885,6 +1229,15 @@ could not be converted into
toString(result.errors[1])
);
}
else if (FFlag::LuauImproveTypePathsInErrors)
{
LUAU_REQUIRE_ERROR_COUNT(1, result);
const std::string expected = R"(Type
'((nil) -> never) & ((number) -> number)'
could not be converted into
'(number?) -> never'; none of the intersection parts are compatible)";
CHECK_EQ(expected, toString(result.errors[0]));
}
else
{
LUAU_REQUIRE_ERROR_COUNT(1, result);
@ -907,7 +1260,40 @@ TEST_CASE_FIXTURE(Fixture, "overloadeded_functions_with_never_arguments")
end
)");
if (FFlag::LuauSolverV2)
if (FFlag::LuauSolverV2 && FFlag::LuauImproveTypePathsInErrors)
{
const std::string expected1 =
"Type\n\t"
"'((never) -> string?) & ((number) -> number?)'"
"\ncould not be converted into\n\t"
"'(never) -> nil'; \n"
"this is because \n\t"
" * in the 1st component of the intersection, the function returns the 1st entry in the type pack which has the 1st component of the "
"union as `number` and it returns the 1st entry in the type pack is `nil`, and `number` is not a subtype of `nil`\n\t"
" * in the 2nd component of the intersection, the function returns the 1st entry in the type pack which has the 1st component of the "
"union as `string` and it returns the 1st entry in the type pack is `nil`, and `string` is not a subtype of `nil`";
const std::string expected2 =
"Type\n\t"
"'((never) -> string?) & ((number) -> number?)'"
"\ncould not be converted into\n\t"
"'(number?) -> nil'; \n"
"this is because \n\t"
" * in the 1st component of the intersection, the function returns the 1st entry in the type pack which has the 1st component of the "
"union as `number` and it returns the 1st entry in the type pack is `nil`, and `number` is not a subtype of `nil`\n\t"
" * in the 1st component of the intersection, the function takes the 1st entry in the type pack which is `number` and it takes the 1st "
"entry in the type pack has the 2nd component of the union as `nil`, and `number` is not a supertype of `nil`\n\t"
" * in the 2nd component of the intersection, the function returns the 1st entry in the type pack which has the 1st component of the "
"union as `string` and it returns the 1st entry in the type pack is `nil`, and `string` is not a subtype of `nil`\n\t"
" * in the 2nd component of the intersection, the function takes the 1st entry in the type pack which is `never` and it takes the 1st "
"entry in the type pack has the 1st component of the union as `number`, and `never` is not a supertype of `number`\n\t"
" * in the 2nd component of the intersection, the function takes the 1st entry in the type pack which is `never` and it takes the 1st "
"entry in the type pack has the 2nd component of the union as `nil`, and `never` is not a supertype of `nil`";
CHECK_EQ(expected1, toString(result.errors[0]));
CHECK_EQ(expected2, toString(result.errors[1]));
}
else if (FFlag::LuauSolverV2)
{
LUAU_REQUIRE_ERROR_COUNT(2, result);
const std::string expected1 = R"(Type
@ -926,6 +1312,15 @@ could not be converted into
CHECK_EQ(expected1, toString(result.errors[0]));
CHECK_EQ(expected2, toString(result.errors[1]));
}
else if (FFlag::LuauImproveTypePathsInErrors)
{
LUAU_REQUIRE_ERROR_COUNT(1, result);
const std::string expected = R"(Type
'((never) -> string?) & ((number) -> number?)'
could not be converted into
'(number?) -> nil'; none of the intersection parts are compatible)";
CHECK_EQ(expected, toString(result.errors[0]));
}
else
{
LUAU_REQUIRE_ERROR_COUNT(1, result);
@ -950,11 +1345,23 @@ TEST_CASE_FIXTURE(Fixture, "overloadeded_functions_with_overlapping_results_and_
)");
LUAU_REQUIRE_ERROR_COUNT(1, result);
const std::string expected = R"(Type
if (FFlag::LuauImproveTypePathsInErrors)
{
const std::string expected = "Type\n\t"
"'((number?) -> (...number)) & ((string?) -> number | string)'"
"\ncould not be converted into\n\t"
"'(number | string) -> (number, number?)'; none of the intersection parts are compatible";
CHECK(expected == toString(result.errors[0]));
}
else
{
const std::string expected = R"(Type
'((number?) -> (...number)) & ((string?) -> number | string)'
could not be converted into
'(number | string) -> (number, number?)'; none of the intersection parts are compatible)";
CHECK_EQ(expected, toString(result.errors[0]));
CHECK_EQ(expected, toString(result.errors[0]));
}
}
TEST_CASE_FIXTURE(Fixture, "overloadeded_functions_with_weird_typepacks_1")
@ -1022,6 +1429,15 @@ TEST_CASE_FIXTURE(Fixture, "overloadeded_functions_with_weird_typepacks_3")
{
LUAU_REQUIRE_NO_ERRORS(result);
}
else if (FFlag::LuauImproveTypePathsInErrors)
{
LUAU_REQUIRE_ERROR_COUNT(1, result);
const std::string expected = R"(Type
'(() -> (a...)) & (() -> (number?, a...))'
could not be converted into
'() -> number'; none of the intersection parts are compatible)";
CHECK_EQ(expected, toString(result.errors[0]));
}
else
{
LUAU_REQUIRE_ERROR_COUNT(1, result);
@ -1045,7 +1461,21 @@ TEST_CASE_FIXTURE(Fixture, "overloadeded_functions_with_weird_typepacks_4")
)");
LUAU_REQUIRE_ERROR_COUNT(1, result);
if (FFlag::LuauSolverV2)
if (FFlag::LuauSolverV2 && FFlag::LuauImproveTypePathsInErrors)
{
const std::string expected = "Type\n\t"
"'((a...) -> ()) & ((number, a...) -> number)'"
"\ncould not be converted into\n\t"
"'((a...) -> ()) & ((number, a...) -> number)'; \n"
"this is because \n\t"
" * in the 1st component of the intersection, the function returns is `()` in the former type and `number` in "
"the latter type, and `()` is not a subtype of `number`\n\t"
" * in the 2nd component of the intersection, the function takes a tail of `a...` and in the 1st component of "
"the intersection, the function takes a tail of `a...`, and `a...` is not a supertype of `a...`";
CHECK(expected == toString(result.errors[0]));
}
else if (FFlag::LuauSolverV2)
{
CHECK_EQ(
R"(Type
@ -1056,6 +1486,16 @@ could not be converted into
toString(result.errors[0])
);
}
else if (FFlag::LuauImproveTypePathsInErrors)
{
CHECK_EQ(
R"(Type
'((a...) -> ()) & ((number, a...) -> number)'
could not be converted into
'(number?) -> ()'; none of the intersection parts are compatible)",
toString(result.errors[0])
);
}
else
{
CHECK_EQ(

View file

@ -12,6 +12,7 @@
LUAU_FASTFLAG(LuauInstantiateInSubtyping)
LUAU_FASTFLAG(LuauSolverV2)
LUAU_FASTFLAG(LuauImproveTypePathsInErrors)
using namespace Luau;
@ -461,7 +462,14 @@ local b: B.T = a
CheckResult result = frontend.check("game/C");
LUAU_REQUIRE_ERROR_COUNT(1, result);
if (FFlag::LuauSolverV2)
if (FFlag::LuauSolverV2 && FFlag::LuauImproveTypePathsInErrors)
{
const std::string expected = "Type 'T' from 'game/A' could not be converted into 'T' from 'game/B'; \n"
"this is because accessing `x` results in `number` in the former type and `string` in the latter type, and "
"`number` is not exactly `string`";
CHECK(expected == toString(result.errors[0]));
}
else if (FFlag::LuauSolverV2)
{
CHECK(
toString(result.errors.at(0)) ==
@ -507,7 +515,14 @@ local b: B.T = a
CheckResult result = frontend.check("game/D");
LUAU_REQUIRE_ERROR_COUNT(1, result);
if (FFlag::LuauSolverV2)
if (FFlag::LuauSolverV2 && FFlag::LuauImproveTypePathsInErrors)
{
const std::string expected = "Type 'T' from 'game/B' could not be converted into 'T' from 'game/C'; \n"
"this is because accessing `x` results in `number` in the former type and `string` in the latter type, and "
"`number` is not exactly `string`";
CHECK(expected == toString(result.errors[0]));
}
else if (FFlag::LuauSolverV2)
{
CHECK(
toString(result.errors.at(0)) ==

View file

@ -79,4 +79,5 @@ end
)");
LUAU_REQUIRE_NO_ERRORS(result);
}
TEST_SUITE_END();

View file

@ -17,7 +17,6 @@
using namespace Luau;
LUAU_FASTFLAG(LuauSolverV2)
LUAU_FASTFLAG(LuauDoNotGeneralizeInTypeFunctions)
TEST_SUITE_BEGIN("TypeInferOperators");
@ -815,8 +814,6 @@ TEST_CASE_FIXTURE(Fixture, "strict_binary_op_where_lhs_unknown")
TEST_CASE_FIXTURE(BuiltinsFixture, "and_binexps_dont_unify")
{
ScopedFastFlag _{FFlag::LuauDoNotGeneralizeInTypeFunctions, true};
// `t` will be inferred to be of type `{ { test: unknown } }` which is
// reasonable, in that it's empty with no bounds on its members. Optimally
// we might emit an error here that the `print(...)` expression is

View file

@ -11,14 +11,15 @@
using namespace Luau;
LUAU_FASTFLAG(LuauSolverV2);
LUAU_FASTFLAG(DebugLuauEqSatSimplification);
LUAU_FASTFLAG(LuauStoreCSTData);
LUAU_FASTINT(LuauNormalizeCacheLimit);
LUAU_FASTINT(LuauTarjanChildLimit);
LUAU_FASTINT(LuauTypeInferIterationLimit);
LUAU_FASTINT(LuauTypeInferRecursionLimit);
LUAU_FASTINT(LuauTypeInferTypePackLoopLimit);
LUAU_FASTFLAG(LuauSolverV2)
LUAU_FASTFLAG(DebugLuauEqSatSimplification)
LUAU_FASTFLAG(LuauStoreCSTData)
LUAU_FASTINT(LuauNormalizeCacheLimit)
LUAU_FASTINT(LuauTarjanChildLimit)
LUAU_FASTINT(LuauTypeInferIterationLimit)
LUAU_FASTINT(LuauTypeInferRecursionLimit)
LUAU_FASTINT(LuauTypeInferTypePackLoopLimit)
LUAU_FASTFLAG(LuauImproveTypePathsInErrors)
TEST_SUITE_BEGIN("ProvisionalTests");
@ -873,7 +874,15 @@ TEST_CASE_FIXTURE(Fixture, "assign_table_with_refined_property_with_a_similar_ty
else
{
LUAU_REQUIRE_ERROR_COUNT(1, result);
const std::string expected = R"(Type
const std::string expected = (FFlag::LuauImproveTypePathsInErrors) ?
R"(Type
'{| x: number? |}'
could not be converted into
'{| x: number |}'
caused by:
Property 'x' is not compatible.
Type 'number?' could not be converted into 'number' in an invariant context)"
: R"(Type
'{| x: number? |}'
could not be converted into
'{| x: number |}'

Some files were not shown because too many files have changed in this diff Show more