Sync to upstream/release/666 (#1747)

Another week, another release. Happy spring! 🌷 

## New Type Solver

- Add typechecking and autocomplete support for user-defined type
functions!
- Improve the display of type paths, making type mismatch errors far
more human-readable.
- Enhance various aspects of the `index` type function: support function
type metamethods, fix crashes involving cyclic metatables, and forward
`any` types through the type function.
- Fix incorrect subtyping results involving the `buffer` type.
- Fix crashes related to typechecking anonymous functions in nonstrict
mode.

## AST

- Retain source information for type packs, functions, and type
functions.
- Introduce `AstTypeOptional` to differentiate `T?` from `T | nil` in
the AST.
- Prevent the transpiler from advancing before tokens when the AST has
errors.

## Autocomplete

- Introduce demand-based cloning and better module isolation for
fragment autocomplete, leading to a substantial speedup in performance.
- Guard against recursive unions in `autocompleteProps`.

## Miscellaneous

- #1720 (thank you!)

## Internal Contributors

Co-authored-by: Andy Friesen <afriesen@roblox.com>
Co-authored-by: Ariel Weiss <aaronweiss@roblox.com>
Co-authored-by: Hunter Goldstein <hgoldstein@roblox.com>
Co-authored-by: Talha Pathan <tpathan@roblox.com>
Co-authored-by: Vighnesh Vijay <vvijay@roblox.com>
Co-authored-by: Vyacheslav Egorov <vegorov@roblox.com>
This commit is contained in:
Varun Saini 2025-03-21 14:43:00 -07:00 committed by GitHub
parent e0b55a9cb1
commit 5f42e63a73
Signed by: DevComp
GPG key ID: B5690EEEBB952194
82 changed files with 3871 additions and 868 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); void assignPropDocumentationSymbols(TableType::Props& props, const std::string& baseName);
std::string getBuiltinDefinitionSource(); 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, TypeId ty, const std::string& packageName);
void addGlobalBinding(GlobalTypes& globals, const std::string& name, Binding binding); void addGlobalBinding(GlobalTypes& globals, const std::string& name, Binding binding);

View file

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

View file

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

View file

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

View file

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

View file

@ -35,7 +35,7 @@ struct Scope
explicit Scope(TypePackId returnType); // root scope explicit Scope(TypePackId returnType); // root scope
explicit Scope(const ScopePtr& parent, int subLevel = 0); // child scope. Parent must not be nullptr. 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. // All the children of this scope.
std::vector<NotNull<Scope>> children; std::vector<NotNull<Scope>> children;
@ -59,6 +59,8 @@ struct Scope
std::optional<TypeId> lookup(Symbol sym) const; std::optional<TypeId> lookup(Symbol sym) const;
std::optional<TypeId> lookupUnrefinedType(DefId def) const; std::optional<TypeId> lookupUnrefinedType(DefId def) const;
std::optional<TypeId> lookupRValueRefinementType(DefId def) const;
std::optional<TypeId> lookup(DefId def) const; std::optional<TypeId> lookup(DefId def) const;
std::optional<std::pair<TypeId, Scope*>> lookupEx(DefId def); std::optional<std::pair<TypeId, Scope*>> lookupEx(DefId def);
std::optional<std::pair<Binding*, Scope*>> lookupEx(Symbol sym); 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) // 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<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; RefinementMap refinements;

View file

@ -622,7 +622,6 @@ struct UserDefinedFunctionData
AstStatTypeFunction* definition = nullptr; AstStatTypeFunction* definition = nullptr;
DenseHashMap<Name, std::pair<AstStatTypeFunction*, size_t>> environment{""}; 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/TypeOrPack.h"
#include "Luau/TypeUtils.h" #include "Luau/TypeUtils.h"
LUAU_FASTFLAG(LuauImproveTypePathsInErrors)
namespace Luau namespace Luau
{ {
@ -38,18 +40,29 @@ struct Reasonings
std::string toString() std::string toString()
{ {
if (FFlag::LuauImproveTypePathsInErrors && reasons.empty())
return "";
// DenseHashSet ordering is entirely undefined, so we want to // DenseHashSet ordering is entirely undefined, so we want to
// sort the reasons here to achieve a stable error // sort the reasons here to achieve a stable error
// stringification. // stringification.
std::sort(reasons.begin(), reasons.end()); std::sort(reasons.begin(), reasons.end());
std::string allReasons; std::string allReasons = FFlag::LuauImproveTypePathsInErrors ? "\nthis is because " : "";
bool first = true; bool first = true;
for (const std::string& reason : reasons) for (const std::string& reason : reasons)
{ {
if (first) if (FFlag::LuauImproveTypePathsInErrors)
first = false; {
if (reasons.size() > 1)
allReasons += "\n\t * ";
}
else else
allReasons += "\n\t"; {
if (first)
first = false;
else
allReasons += "\n\t";
}
allReasons += reason; 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 // Evaluation of type functions should only be performed in the absence of parse errors in the source module
bool allowEvaluation = true; bool allowEvaluation = true;
// Root scope in which the type function operates in, set up by ConstraintGenerator
ScopePtr rootScope;
// Output created by 'print' function // Output created by 'print' function
std::vector<std::string> messages; std::vector<std::string> messages;

View file

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

View file

@ -28,14 +28,8 @@ struct TypeFunctionRuntimeBuilderState
{ {
NotNull<TypeFunctionContext> ctx; 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 // 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{}; std::vector<std::string> errors{};
TypeFunctionRuntimeBuilderState(NotNull<TypeFunctionContext> ctx) TypeFunctionRuntimeBuilderState(NotNull<TypeFunctionContext> ctx)

View file

@ -42,9 +42,19 @@ struct Property
/// element. /// element.
struct Index struct Index
{ {
enum class Variant
{
Pack,
Union,
Intersection
};
/// The 0-based index to use for the lookup. /// The 0-based index to use for the lookup.
size_t index; 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; bool operator==(const Index& other) const;
}; };
@ -205,6 +215,9 @@ using Path = TypePath::Path;
/// terribly clear to end users of the Luau type system. /// terribly clear to end users of the Luau type system.
std::string toString(const TypePath::Path& path, bool prefixDot = false); 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(TypeId root, const Path& path, NotNull<BuiltinTypes> builtinTypes);
std::optional<TypeOrPack> traverse(TypePackId root, const Path& path, NotNull<BuiltinTypes> builtinTypes); std::optional<TypeOrPack> traverse(TypePackId root, const Path& path, NotNull<BuiltinTypes> builtinTypes);

View file

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

View file

@ -30,6 +30,7 @@ LUAU_FASTFLAG(LuauExposeRequireByStringAutocomplete)
LUAU_FASTFLAGVARIABLE(LuauAutocompleteRefactorsForIncrementalAutocomplete) LUAU_FASTFLAGVARIABLE(LuauAutocompleteRefactorsForIncrementalAutocomplete)
LUAU_FASTFLAGVARIABLE(LuauAutocompleteUsesModuleForTypeCompatibility) LUAU_FASTFLAGVARIABLE(LuauAutocompleteUsesModuleForTypeCompatibility)
LUAU_FASTFLAGVARIABLE(LuauAutocompleteUnionCopyPreviousSeen)
static const std::unordered_set<std::string> kStatementStartingKeywords = static const std::unordered_set<std::string> kStatementStartingKeywords =
{"while", "if", "local", "repeat", "function", "do", "for", "return", "break", "continue", "type", "export"}; {"while", "if", "local", "repeat", "function", "do", "for", "return", "break", "continue", "type", "export"};
@ -484,6 +485,21 @@ static void autocompleteProps(
AutocompleteEntryMap inner; AutocompleteEntryMap inner;
std::unordered_set<TypeId> innerSeen; 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)) if (isNil(*iter))
{ {
++iter; ++iter;

View file

@ -32,8 +32,8 @@ LUAU_FASTFLAG(LuauSolverV2)
LUAU_FASTFLAGVARIABLE(LuauStringFormatErrorSuppression) LUAU_FASTFLAGVARIABLE(LuauStringFormatErrorSuppression)
LUAU_FASTFLAGVARIABLE(LuauTableCloneClonesType3) LUAU_FASTFLAGVARIABLE(LuauTableCloneClonesType3)
LUAU_FASTFLAG(LuauTrackInteriorFreeTypesOnScope) LUAU_FASTFLAG(LuauTrackInteriorFreeTypesOnScope)
LUAU_FASTFLAGVARIABLE(LuauFreezeIgnorePersistent)
LUAU_FASTFLAGVARIABLE(LuauFollowTableFreeze) LUAU_FASTFLAGVARIABLE(LuauFollowTableFreeze)
LUAU_FASTFLAGVARIABLE(LuauUserTypeFunTypecheck)
namespace Luau 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) void registerBuiltinGlobals(Frontend& frontend, GlobalTypes& globals, bool typeCheckForAutocomplete)
{ {
LUAU_ASSERT(!globals.globalTypes.types.isFrozen()); LUAU_ASSERT(!globals.globalTypes.types.isFrozen());
@ -399,14 +415,21 @@ void registerBuiltinGlobals(Frontend& frontend, GlobalTypes& globals, bool typeC
// clang-format on // clang-format on
} }
for (const auto& pair : globals.globalScope->bindings) if (FFlag::LuauUserTypeFunTypecheck)
{ {
persist(pair.second.typeId); finalizeGlobalBindings(globals.globalScope);
}
if (TableType* ttv = getMutable<TableType>(pair.second.typeId)) else
{
for (const auto& pair : globals.globalScope->bindings)
{ {
if (!ttv->name) persist(pair.second.typeId);
ttv->name = "typeof(" + toString(pair.first) + ")";
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"); TypeId requireTy = getGlobalBinding(globals, "require");
attachTag(requireTy, kRequireTagName); attachTag(requireTy, kRequireTagName);
attachMagicFunction(requireTy, std::make_shared<MagicRequire>()); 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) 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; return false;
CloneState cloneState{context.solver->builtinTypes}; 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)) 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. // Clone the input type, this will become our final result type after we mutate it.
CloneState cloneState{context.solver->builtinTypes}; 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); auto tableTy = getMutable<TableType>(resultType);
// `clone` should not break this. // `clone` should not break this.
LUAU_ASSERT(tableTy); LUAU_ASSERT(tableTy);

View file

@ -9,13 +9,12 @@
#include "Luau/VisitType.h" #include "Luau/VisitType.h"
LUAU_FASTFLAG(LuauSolverV2) 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. // 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_FASTINTVARIABLE(LuauTypeCloneIterationLimit, 100'000)
LUAU_FASTFLAGVARIABLE(LuauClonedTableAndFunctionTypesMustHaveScopes) LUAU_FASTFLAGVARIABLE(LuauClonedTableAndFunctionTypesMustHaveScopes)
LUAU_FASTFLAGVARIABLE(LuauDoNotClonePersistentBindings) LUAU_FASTFLAGVARIABLE(LuauDoNotClonePersistentBindings)
LUAU_FASTFLAG(LuauIncrementalAutocompleteDemandBasedCloning)
namespace Luau namespace Luau
{ {
@ -134,7 +133,7 @@ protected:
ty = follow(ty, FollowOption::DisableLazyTypeThunks); ty = follow(ty, FollowOption::DisableLazyTypeThunks);
if (auto it = types->find(ty); it != types->end()) if (auto it = types->find(ty); it != types->end())
return it->second; return it->second;
else if (ty->persistent && (!FFlag::LuauFreezeIgnorePersistent || ty != forceTy)) else if (ty->persistent && ty != forceTy)
return ty; return ty;
return std::nullopt; return std::nullopt;
} }
@ -144,7 +143,7 @@ protected:
tp = follow(tp); tp = follow(tp);
if (auto it = packs->find(tp); it != packs->end()) if (auto it = packs->find(tp); it != packs->end())
return it->second; return it->second;
else if (tp->persistent && (!FFlag::LuauFreezeIgnorePersistent || tp != forceTp)) else if (tp->persistent && tp != forceTp)
return tp; return tp;
return std::nullopt; return std::nullopt;
} }
@ -170,7 +169,7 @@ public:
if (auto clone = find(ty)) if (auto clone = find(ty))
return *clone; return *clone;
else if (ty->persistent && (!FFlag::LuauFreezeIgnorePersistent || ty != forceTy)) else if (ty->persistent && ty != forceTy)
return ty; return ty;
TypeId target = arena->addType(ty->ty); TypeId target = arena->addType(ty->ty);
@ -196,7 +195,7 @@ public:
if (auto clone = find(tp)) if (auto clone = find(tp))
return *clone; return *clone;
else if (tp->persistent && (!FFlag::LuauFreezeIgnorePersistent || tp != forceTp)) else if (tp->persistent && tp != forceTp)
return tp; return tp;
TypePackId target = arena->addTypePack(tp->ty); TypePackId target = arena->addTypePack(tp->ty);
@ -398,7 +397,7 @@ private:
ty = shallowClone(ty); ty = shallowClone(ty);
} }
void cloneChildren(LazyType* t) virtual void cloneChildren(LazyType* t)
{ {
if (auto unwrapped = t->unwrapped.load()) if (auto unwrapped = t->unwrapped.load())
t->unwrapped.store(shallowClone(unwrapped)); t->unwrapped.store(shallowClone(unwrapped));
@ -505,7 +504,7 @@ public:
if (auto clone = find(ty)) if (auto clone = find(ty))
return *clone; return *clone;
else if (ty->persistent && (!FFlag::LuauFreezeIgnorePersistent || ty != forceTy)) else if (ty->persistent && ty != forceTy)
return ty; return ty;
TypeId target = arena->addType(ty->ty); TypeId target = arena->addType(ty->ty);
@ -539,7 +538,7 @@ public:
if (auto clone = find(tp)) if (auto clone = find(tp))
return *clone; return *clone;
else if (tp->persistent && (!FFlag::LuauFreezeIgnorePersistent || tp != forceTp)) else if (tp->persistent && tp != forceTp)
return tp; return tp;
TypePackId target = arena->addTypePack(tp->ty); TypePackId target = arena->addTypePack(tp->ty);
@ -553,6 +552,16 @@ public:
queue.emplace_back(target); queue.emplace_back(target);
return target; return target;
} }
void cloneChildren(LazyType* t) override
{
// Do not clone lazy types
if (!FFlag::LuauIncrementalAutocompleteDemandBasedCloning)
{
if (auto unwrapped = t->unwrapped.load())
t->unwrapped.store(shallowClone(unwrapped));
}
}
}; };
@ -560,7 +569,7 @@ public:
TypePackId shallowClone(TypePackId tp, TypeArena& dest, CloneState& cloneState, bool ignorePersistent) TypePackId shallowClone(TypePackId tp, TypeArena& dest, CloneState& cloneState, bool ignorePersistent)
{ {
if (tp->persistent && (!FFlag::LuauFreezeIgnorePersistent || !ignorePersistent)) if (tp->persistent && !ignorePersistent)
return tp; return tp;
TypeCloner cloner{ TypeCloner cloner{
@ -569,7 +578,7 @@ TypePackId shallowClone(TypePackId tp, TypeArena& dest, CloneState& cloneState,
NotNull{&cloneState.seenTypes}, NotNull{&cloneState.seenTypes},
NotNull{&cloneState.seenTypePacks}, NotNull{&cloneState.seenTypePacks},
nullptr, nullptr,
FFlag::LuauFreezeIgnorePersistent && ignorePersistent ? tp : nullptr ignorePersistent ? tp : nullptr
}; };
return cloner.shallowClone(tp); return cloner.shallowClone(tp);
@ -577,7 +586,7 @@ TypePackId shallowClone(TypePackId tp, TypeArena& dest, CloneState& cloneState,
TypeId shallowClone(TypeId typeId, TypeArena& dest, CloneState& cloneState, bool ignorePersistent) TypeId shallowClone(TypeId typeId, TypeArena& dest, CloneState& cloneState, bool ignorePersistent)
{ {
if (typeId->persistent && (!FFlag::LuauFreezeIgnorePersistent || !ignorePersistent)) if (typeId->persistent && !ignorePersistent)
return typeId; return typeId;
TypeCloner cloner{ TypeCloner cloner{
@ -585,7 +594,7 @@ TypeId shallowClone(TypeId typeId, TypeArena& dest, CloneState& cloneState, bool
cloneState.builtinTypes, cloneState.builtinTypes,
NotNull{&cloneState.seenTypes}, NotNull{&cloneState.seenTypes},
NotNull{&cloneState.seenTypePacks}, NotNull{&cloneState.seenTypePacks},
FFlag::LuauFreezeIgnorePersistent && ignorePersistent ? typeId : nullptr, ignorePersistent ? typeId : nullptr,
nullptr nullptr
}; };

View file

@ -44,6 +44,7 @@ LUAU_FASTFLAG(LuauFreeTypesMustHaveBounds)
LUAU_FASTFLAGVARIABLE(LuauInferLocalTypesInMultipleAssignments) LUAU_FASTFLAGVARIABLE(LuauInferLocalTypesInMultipleAssignments)
LUAU_FASTFLAGVARIABLE(LuauDoNotLeakNilInRefinement) LUAU_FASTFLAGVARIABLE(LuauDoNotLeakNilInRefinement)
LUAU_FASTFLAGVARIABLE(LuauExtraFollows) LUAU_FASTFLAGVARIABLE(LuauExtraFollows)
LUAU_FASTFLAG(LuauUserTypeFunTypecheck)
namespace Luau namespace Luau
{ {
@ -184,6 +185,7 @@ ConstraintGenerator::ConstraintGenerator(
NotNull<BuiltinTypes> builtinTypes, NotNull<BuiltinTypes> builtinTypes,
NotNull<InternalErrorReporter> ice, NotNull<InternalErrorReporter> ice,
const ScopePtr& globalScope, const ScopePtr& globalScope,
const ScopePtr& typeFunctionScope,
std::function<void(const ModuleName&, const ScopePtr&)> prepareModuleScope, std::function<void(const ModuleName&, const ScopePtr&)> prepareModuleScope,
DcrLogger* logger, DcrLogger* logger,
NotNull<DataFlowGraph> dfg, NotNull<DataFlowGraph> dfg,
@ -200,6 +202,7 @@ ConstraintGenerator::ConstraintGenerator(
, moduleResolver(moduleResolver) , moduleResolver(moduleResolver)
, ice(ice) , ice(ice)
, globalScope(globalScope) , globalScope(globalScope)
, typeFunctionScope(typeFunctionScope)
, prepareModuleScope(std::move(prepareModuleScope)) , prepareModuleScope(std::move(prepareModuleScope))
, requireCycles(std::move(requireCycles)) , requireCycles(std::move(requireCycles))
, logger(logger) , logger(logger)
@ -221,6 +224,14 @@ void ConstraintGenerator::visitModuleRoot(AstStatBlock* block)
rootScope->returnType = freshTypePack(scope); 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}); TypeId moduleFnTy = arena->addType(FunctionType{TypeLevel{}, rootScope, builtinTypes->anyTypePack, rootScope->returnType});
interiorTypes.emplace_back(); interiorTypes.emplace_back();
@ -699,6 +710,9 @@ void ConstraintGenerator::checkAliases(const ScopePtr& scope, AstStatBlock* bloc
std::unordered_map<Name, Location> aliasDefinitionLocations; std::unordered_map<Name, Location> aliasDefinitionLocations;
std::unordered_map<Name, Location> classDefinitionLocations; std::unordered_map<Name, Location> classDefinitionLocations;
bool hasTypeFunction = false;
ScopePtr typeFunctionEnvScope;
// In order to enable mutually-recursive type aliases, we need to // In order to enable mutually-recursive type aliases, we need to
// populate the type bindings before we actually check any of the // populate the type bindings before we actually check any of the
// alias statements. // alias statements.
@ -744,6 +758,9 @@ void ConstraintGenerator::checkAliases(const ScopePtr& scope, AstStatBlock* bloc
} }
else if (auto function = stat->as<AstStatTypeFunction>()) 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 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)) if (scope->exportedTypeBindings.count(function->name.value) || scope->privateTypeBindings.count(function->name.value))
{ {
@ -753,7 +770,8 @@ void ConstraintGenerator::checkAliases(const ScopePtr& scope, AstStatBlock* bloc
continue; 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 // Create TypeFunctionInstanceType
@ -820,11 +838,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 // Additional pass for user-defined type functions to fill in their environments completely
for (AstStat* stat : block->body) for (AstStat* stat : block->body)
{ {
if (auto function = stat->as<AstStatTypeFunction>()) 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 // Find the type function we have already created
TypeFunctionInstanceType* mainTypeFun = nullptr; TypeFunctionInstanceType* mainTypeFun = nullptr;
@ -843,51 +872,60 @@ void ConstraintGenerator::checkAliases(const ScopePtr& scope, AstStatBlock* bloc
UserDefinedFunctionData& userFuncData = mainTypeFun->userFuncData; UserDefinedFunctionData& userFuncData = mainTypeFun->userFuncData;
size_t level = 0; 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)) 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); 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)) for (auto& [name, tf] : curr->privateTypeBindings)
continue; addToEnvironment(userFuncData, typeFunctionEnvScope, name, tf.type, level);
if (auto ty = get<TypeFunctionInstanceType>(tf.type); ty && ty->userFuncData.definition) for (auto& [name, tf] : curr->exportedTypeBindings)
userFuncData.environment[name] = std::make_pair(ty->userFuncData.definition, level); addToEnvironment(userFuncData, typeFunctionEnvScope, name, tf.type, level);
level++;
} }
level++;
} }
} else
else if (mainTypeFun)
{
UserDefinedFunctionData& userFuncData = mainTypeFun->userFuncData;
for (Scope* curr = scope.get(); curr; curr = curr->parent.get())
{ {
for (auto& [name, tf] : curr->privateTypeBindings) for (Scope* curr = scope.get(); curr; curr = curr->parent.get())
{ {
if (userFuncData.environment_DEPRECATED.find(name)) for (auto& [name, tf] : curr->privateTypeBindings)
continue; {
if (userFuncData.environment.find(name))
continue;
if (auto ty = get<TypeFunctionInstanceType>(tf.type); ty && ty->userFuncData.definition) if (auto ty = get<TypeFunctionInstanceType>(tf.type); ty && ty->userFuncData.definition)
userFuncData.environment_DEPRECATED[name] = ty->userFuncData.definition; userFuncData.environment[name] = std::make_pair(ty->userFuncData.definition, level);
} }
for (auto& [name, tf] : curr->exportedTypeBindings) for (auto& [name, tf] : curr->exportedTypeBindings)
{ {
if (userFuncData.environment_DEPRECATED.find(name)) if (userFuncData.environment.find(name))
continue; continue;
if (auto ty = get<TypeFunctionInstanceType>(tf.type); ty && ty->userFuncData.definition) if (auto ty = get<TypeFunctionInstanceType>(tf.type); ty && ty->userFuncData.definition)
userFuncData.environment_DEPRECATED[name] = ty->userFuncData.definition; userFuncData.environment[name] = std::make_pair(ty->userFuncData.definition, level);
}
level++;
} }
} }
} }
@ -1689,6 +1727,64 @@ ControlFlow ConstraintGenerator::visit(const ScopePtr& scope, AstStatTypeAlias*
ControlFlow ConstraintGenerator::visit(const ScopePtr& scope, AstStatTypeFunction* function) 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; return ControlFlow::None;
} }
@ -3094,7 +3190,7 @@ ConstraintGenerator::FunctionSignature ConstraintGenerator::checkFunctionSignatu
signatureScope = childScope(fn, parent); signatureScope = childScope(fn, parent);
// We need to assign returnType before creating bodyScope so that the // 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); returnType = freshTypePack(signatureScope);
signatureScope->returnType = returnType; signatureScope->returnType = returnType;
@ -3520,6 +3616,10 @@ TypeId ConstraintGenerator::resolveType(const ScopePtr& scope, AstType* ty, bool
TypeId exprType = check(scope, tof->expr).ty; TypeId exprType = check(scope, tof->expr).ty;
result = exprType; result = exprType;
} }
else if (ty->is<AstTypeOptional>())
{
return builtinTypes->nilType;
}
else if (auto unionAnnotation = ty->as<AstTypeUnion>()) else if (auto unionAnnotation = ty->as<AstTypeUnion>())
{ {
if (FFlag::LuauPreserveUnionIntersectionNodeForLeadingTokenSingleType) if (FFlag::LuauPreserveUnionIntersectionNodeForLeadingTokenSingleType)
@ -3882,9 +3982,18 @@ struct GlobalPrepopulator : AstVisitor
void ConstraintGenerator::prepopulateGlobalScopeForFragmentTypecheck(const ScopePtr& globalScope, const ScopePtr& resumeScope, AstStatBlock* program) void ConstraintGenerator::prepopulateGlobalScopeForFragmentTypecheck(const ScopePtr& globalScope, const ScopePtr& resumeScope, AstStatBlock* program)
{ {
FragmentTypeCheckGlobalPrepopulator gp{NotNull{globalScope.get()}, NotNull{resumeScope.get()}, dfg, arena}; FragmentTypeCheckGlobalPrepopulator gp{NotNull{globalScope.get()}, NotNull{resumeScope.get()}, dfg, arena};
if (prepareModuleScope) if (prepareModuleScope)
prepareModuleScope(module->name, resumeScope); prepareModuleScope(module->name, resumeScope);
program->visit(&gp); 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) void ConstraintGenerator::prepopulateGlobalScope(const ScopePtr& globalScope, AstStatBlock* program)
@ -3895,6 +4004,13 @@ void ConstraintGenerator::prepopulateGlobalScope(const ScopePtr& globalScope, As
prepareModuleScope(module->name, globalScope); prepareModuleScope(module->name, globalScope);
program->visit(&gp); 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) bool ConstraintGenerator::recordPropertyAssignment(TypeId ty)

View file

@ -1168,6 +1168,8 @@ void DataFlowGraphBuilder::visitType(AstType* t)
return visitType(f); return visitType(f);
else if (auto tyof = t->as<AstTypeTypeof>()) else if (auto tyof = t->as<AstTypeTypeof>())
return visitType(tyof); return visitType(tyof);
else if (auto o = t->as<AstTypeOptional>())
return;
else if (auto u = t->as<AstTypeUnion>()) else if (auto u = t->as<AstTypeUnion>())
return visitType(u); return visitType(u);
else if (auto i = t->as<AstTypeIntersection>()) else if (auto i = t->as<AstTypeIntersection>())

View file

@ -317,4 +317,83 @@ std::string getBuiltinDefinitionSource()
return result; 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 } // namespace Luau

View file

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

View file

@ -39,6 +39,9 @@ LUAU_FASTFLAGVARIABLE(LuauPersistConstraintGenerationScopes)
LUAU_FASTFLAG(LuauModuleHoldsAstRoot) LUAU_FASTFLAG(LuauModuleHoldsAstRoot)
LUAU_FASTFLAGVARIABLE(LuauCloneTypeAliasBindings) LUAU_FASTFLAGVARIABLE(LuauCloneTypeAliasBindings)
LUAU_FASTFLAGVARIABLE(LuauCloneReturnTypePack) LUAU_FASTFLAGVARIABLE(LuauCloneReturnTypePack)
LUAU_FASTFLAGVARIABLE(LuauIncrementalAutocompleteDemandBasedCloning)
LUAU_FASTFLAG(LuauUserTypeFunTypecheck)
LUAU_FASTFLAGVARIABLE(LuauFragmentNoTypeFunEval)
namespace namespace
{ {
@ -84,8 +87,140 @@ void cloneModuleMap(
} }
} }
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 struct MixedModeIncrementalTCDefFinder : public AstVisitor
{ {
bool visit(AstExprLocal* local) override bool visit(AstExprLocal* local) override
{ {
referencedLocalDefs.emplace_back(local->local, local); referencedLocalDefs.emplace_back(local->local, local);
@ -337,6 +472,17 @@ 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;
}
}
}
} }
} }
} }
@ -622,7 +768,7 @@ static void reportFragmentString(IFragmentAutocompleteReporter* reporter, std::s
reporter->reportFragmentString(fragment); reporter->reportFragmentString(fragment);
} }
FragmentTypeCheckResult typecheckFragment_( FragmentTypeCheckResult typecheckFragmentHelper_DEPRECATED(
Frontend& frontend, Frontend& frontend,
AstStatBlock* root, AstStatBlock* root,
const ModulePtr& stale, const ModulePtr& stale,
@ -692,6 +838,7 @@ FragmentTypeCheckResult typecheckFragment_(
frontend.builtinTypes, frontend.builtinTypes,
iceHandler, iceHandler,
stale->getModuleScope(), stale->getModuleScope(),
frontend.globals.globalTypeFunctionScope,
nullptr, nullptr,
nullptr, nullptr,
NotNull{&dfg}, NotNull{&dfg},
@ -729,7 +876,7 @@ FragmentTypeCheckResult typecheckFragment_(
incrementalModule->scopes.emplace_back(root->location, freshChildOfNearestScope); incrementalModule->scopes.emplace_back(root->location, freshChildOfNearestScope);
mixedModeCompatibility(closestScope, freshChildOfNearestScope, stale, NotNull{&dfg}, root); mixedModeCompatibility(closestScope, freshChildOfNearestScope, stale, NotNull{&dfg}, root);
// closest Scope -> children = { ...., freshChildOfNearestScope} // 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()); closestScope->children.emplace_back(freshChildOfNearestScope.get());
cg.visitFragmentRoot(freshChildOfNearestScope, root); cg.visitFragmentRoot(freshChildOfNearestScope, root);
// Trim nearestChild from the closestScope // Trim nearestChild from the closestScope
@ -788,6 +935,149 @@ FragmentTypeCheckResult typecheckFragment_(
return {std::move(incrementalModule), std::move(freshChildOfNearestScope)}; 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( std::pair<FragmentTypeCheckStatus, FragmentTypeCheckResult> typecheckFragment(
Frontend& frontend, Frontend& frontend,
@ -850,7 +1140,11 @@ std::pair<FragmentTypeCheckStatus, FragmentTypeCheckResult> typecheckFragment(
FrontendOptions frontendOptions = opts.value_or(frontend.options); FrontendOptions frontendOptions = opts.value_or(frontend.options);
const ScopePtr& closestScope = findClosestScope(module, parseResult.nearestStatement); const ScopePtr& closestScope = findClosestScope(module, parseResult.nearestStatement);
FragmentTypeCheckResult result = FragmentTypeCheckResult result =
typecheckFragment_(frontend, parseResult.root, module, closestScope, cursorPos, std::move(parseResult.alloc), frontendOptions, reporter); FFlag::LuauIncrementalAutocompleteDemandBasedCloning
? typecheckFragment_(frontend, parseResult.root, module, closestScope, cursorPos, std::move(parseResult.alloc), frontendOptions, reporter)
: typecheckFragmentHelper_DEPRECATED(
frontend, parseResult.root, module, closestScope, cursorPos, std::move(parseResult.alloc), frontendOptions, reporter
);
result.ancestry = std::move(parseResult.ancestry); result.ancestry = std::move(parseResult.ancestry);
reportFragmentString(reporter, tryParse->fragmentToParse); reportFragmentString(reporter, tryParse->fragmentToParse);
return {FragmentTypeCheckStatus::Success, result}; return {FragmentTypeCheckStatus::Success, result};

View file

@ -1532,6 +1532,7 @@ ModulePtr check(
NotNull<ModuleResolver> moduleResolver, NotNull<ModuleResolver> moduleResolver,
NotNull<FileResolver> fileResolver, NotNull<FileResolver> fileResolver,
const ScopePtr& parentScope, const ScopePtr& parentScope,
const ScopePtr& typeFunctionScope,
std::function<void(const ModuleName&, const ScopePtr&)> prepareModuleScope, std::function<void(const ModuleName&, const ScopePtr&)> prepareModuleScope,
FrontendOptions options, FrontendOptions options,
TypeCheckLimits limits, TypeCheckLimits limits,
@ -1548,6 +1549,7 @@ ModulePtr check(
moduleResolver, moduleResolver,
fileResolver, fileResolver,
parentScope, parentScope,
typeFunctionScope,
std::move(prepareModuleScope), std::move(prepareModuleScope),
options, options,
limits, limits,
@ -1609,6 +1611,7 @@ ModulePtr check(
NotNull<ModuleResolver> moduleResolver, NotNull<ModuleResolver> moduleResolver,
NotNull<FileResolver> fileResolver, NotNull<FileResolver> fileResolver,
const ScopePtr& parentScope, const ScopePtr& parentScope,
const ScopePtr& typeFunctionScope,
std::function<void(const ModuleName&, const ScopePtr&)> prepareModuleScope, std::function<void(const ModuleName&, const ScopePtr&)> prepareModuleScope,
FrontendOptions options, FrontendOptions options,
TypeCheckLimits limits, TypeCheckLimits limits,
@ -1666,6 +1669,7 @@ ModulePtr check(
builtinTypes, builtinTypes,
iceHandler, iceHandler,
parentScope, parentScope,
typeFunctionScope,
std::move(prepareModuleScope), std::move(prepareModuleScope),
logger.get(), logger.get(),
NotNull{&dfg}, NotNull{&dfg},
@ -1848,6 +1852,7 @@ ModulePtr Frontend::check(
NotNull{forAutocomplete ? &moduleResolverForAutocomplete : &moduleResolver}, NotNull{forAutocomplete ? &moduleResolverForAutocomplete : &moduleResolver},
NotNull{fileResolver}, NotNull{fileResolver},
environmentScope ? *environmentScope : globals.globalScope, environmentScope ? *environmentScope : globals.globalScope,
globals.globalTypeFunctionScope,
prepareModuleScopeWrap, prepareModuleScopeWrap,
options, options,
typeCheckLimits, typeCheckLimits,

View file

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

View file

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

View file

@ -22,6 +22,7 @@
LUAU_FASTFLAG(LuauFreeTypesMustHaveBounds) LUAU_FASTFLAG(LuauFreeTypesMustHaveBounds)
LUAU_FASTFLAGVARIABLE(LuauNonStrictVisitorImprovements) LUAU_FASTFLAGVARIABLE(LuauNonStrictVisitorImprovements)
LUAU_FASTFLAGVARIABLE(LuauNewNonStrictWarnOnUnknownGlobals) LUAU_FASTFLAGVARIABLE(LuauNewNonStrictWarnOnUnknownGlobals)
LUAU_FASTFLAGVARIABLE(LuauNonStrictFuncDefErrorFix)
namespace Luau namespace Luau
{ {
@ -763,7 +764,17 @@ struct NonStrictTypeChecker
for (AstLocal* local : exprFn->args) for (AstLocal* local : exprFn->args)
{ {
if (std::optional<TypeId> ty = willRunTimeErrorFunctionDefinition(local, remainder)) 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)); remainder.remove(dfg->getDef(local));
} }
return remainder; return remainder;

View file

@ -22,7 +22,7 @@ LUAU_FASTINTVARIABLE(LuauNormalizeIntersectionLimit, 200)
LUAU_FASTFLAG(LuauSolverV2) LUAU_FASTFLAG(LuauSolverV2)
LUAU_FASTFLAGVARIABLE(LuauNormalizeNegationFix) LUAU_FASTFLAGVARIABLE(LuauNormalizeNegationFix)
LUAU_FASTFLAGVARIABLE(LuauFixInfiniteRecursionInNormalization) LUAU_FASTFLAGVARIABLE(LuauFixInfiniteRecursionInNormalization)
LUAU_FASTFLAGVARIABLE(LuauFixNormalizedIntersectionOfNegatedClass) LUAU_FASTFLAGVARIABLE(LuauNormalizedBufferIsNotUnknown)
namespace Luau namespace Luau
{ {
@ -304,7 +304,9 @@ bool NormalizedType::isUnknown() const
// Otherwise, we can still be unknown! // Otherwise, we can still be unknown!
bool hasAllPrimitives = isPrim(booleans, PrimitiveType::Boolean) && isPrim(nils, PrimitiveType::NilType) && isNumber(numbers) && 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 // Check is class
bool isTopClass = false; bool isTopClass = false;
@ -2289,7 +2291,7 @@ void Normalizer::intersectClassesWithClass(NormalizedClassType& heres, TypeId th
for (auto nIt = negations.begin(); nIt != negations.end();) 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 // Hitting this block means that the incoming class is a
// subclass of this type, _and_ one of its negations is a // subclass of this type, _and_ one of its negations is a

View file

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

View file

@ -84,6 +84,17 @@ std::optional<TypeId> Scope::lookupUnrefinedType(DefId def) const
return std::nullopt; 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 std::optional<TypeId> Scope::lookup(DefId def) const
{ {
for (const Scope* current = this; current; current = current->parent.get()) 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; 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`. // Updates the `this` scope with the assignments from the `childScope` including ones that doesn't exist in `this`.
void Scope::inheritAssignments(const ScopePtr& childScope) void Scope::inheritAssignments(const ScopePtr& childScope)
{ {

View file

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

View file

@ -13,8 +13,6 @@
#include "Luau/TypeUtils.h" #include "Luau/TypeUtils.h"
#include "Luau/Unifier2.h" #include "Luau/Unifier2.h"
LUAU_FASTFLAGVARIABLE(LuauDontInPlaceMutateTableType)
LUAU_FASTFLAGVARIABLE(LuauAllowNonSharedTableTypesInLiteral)
LUAU_FASTFLAGVARIABLE(LuauBidirectionalInferenceUpcast) LUAU_FASTFLAGVARIABLE(LuauBidirectionalInferenceUpcast)
namespace Luau namespace Luau
@ -277,19 +275,11 @@ TypeId matchLiteralType(
Property& prop = it->second; 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
// If we encounter a duplcate property, we may have already // that will definitely crash is trying to access a write
// set it to be read-only. If that's the case, the only thing // only property.
// that will definitely crash is trying to access a write LUAU_ASSERT(!prop.isWriteOnly());
// only property.
LUAU_ASSERT(!prop.isWriteOnly());
}
else
{
// Table literals always initially result in shared read-write types
LUAU_ASSERT(prop.isShared());
}
TypeId propTy = *prop.readTy; TypeId propTy = *prop.readTy;
auto it2 = expectedTableTy->props.find(keyStr); auto it2 = expectedTableTy->props.find(keyStr);
@ -322,10 +312,7 @@ TypeId matchLiteralType(
else else
tableTy->indexer = TableIndexer{expectedTableTy->indexer->indexType, matchedType}; tableTy->indexer = TableIndexer{expectedTableTy->indexer->indexType, matchedType};
if (FFlag::LuauDontInPlaceMutateTableType) keysToDelete.insert(item.key->as<AstExprConstantString>());
keysToDelete.insert(item.key->as<AstExprConstantString>());
else
tableTy->props.erase(keyStr);
} }
@ -434,14 +421,11 @@ TypeId matchLiteralType(
LUAU_ASSERT(!"Unexpected"); 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};
const AstArray<char>& s = key->value; tableTy->props.erase(keyStr);
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 // Keys that the expectedType says we should have, but that aren't

View file

@ -14,6 +14,7 @@ LUAU_FASTFLAG(LuauStoreCSTData)
LUAU_FASTFLAG(LuauExtendStatEndPosWithSemicolon) LUAU_FASTFLAG(LuauExtendStatEndPosWithSemicolon)
LUAU_FASTFLAG(LuauAstTypeGroup3) LUAU_FASTFLAG(LuauAstTypeGroup3)
LUAU_FASTFLAG(LuauFixDoBlockEndLocation) LUAU_FASTFLAG(LuauFixDoBlockEndLocation)
LUAU_FASTFLAG(LuauParseOptionalAsNode)
namespace namespace
{ {
@ -271,6 +272,43 @@ private:
const Position* commaPosition; 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 struct Printer_DEPRECATED
{ {
explicit Printer_DEPRECATED(Writer& writer) explicit Printer_DEPRECATED(Writer& writer)
@ -1216,6 +1254,15 @@ struct Printer_DEPRECATED
for (size_t i = 0; i < a->types.size; ++i) 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) if (i > 0)
{ {
writer.maybeSpace(a->types.data[i]->location.begin, 2); 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); advance(annotation.location.begin);
if (const AstTypePackVariadic* variadicTp = annotation.as<AstTypePackVariadic>()) if (const AstTypePackVariadic* variadicTp = annotation.as<AstTypePackVariadic>())
@ -1322,15 +1369,22 @@ struct Printer
visualizeTypeAnnotation(*variadicTp->variadicType); visualizeTypeAnnotation(*variadicTp->variadicType);
} }
else if (const AstTypePackGeneric* genericTp = annotation.as<AstTypePackGeneric>()) else if (AstTypePackGeneric* genericTp = annotation.as<AstTypePackGeneric>())
{ {
writer.symbol(genericTp->genericName.value); writer.symbol(genericTp->genericName.value);
if (const auto cstNode = lookupCstNode<CstTypePackGeneric>(genericTp))
advance(cstNode->ellipsisPosition);
writer.symbol("..."); writer.symbol("...");
} }
else if (const AstTypePackExplicit* explicitTp = annotation.as<AstTypePackExplicit>()) else if (AstTypePackExplicit* explicitTp = annotation.as<AstTypePackExplicit>())
{ {
LUAU_ASSERT(!forVarArg); 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 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); size_t typeCount = list.types.size + (list.tailType != nullptr ? 1 : 0);
if (typeCount == 0) if (typeCount == 0)
{ {
if (openParenthesesPosition)
advance(*openParenthesesPosition);
writer.symbol("("); writer.symbol("(");
if (closeParenthesesPosition)
advance(*closeParenthesesPosition);
writer.symbol(")"); writer.symbol(")");
} }
else if (typeCount == 1) else if (typeCount == 1)
{ {
bool shouldParenthesize = unconditionallyParenthesize && (list.types.size == 0 || !list.types.data[0]->is<AstTypeGroup>()); bool shouldParenthesize = unconditionallyParenthesize && (list.types.size == 0 || !list.types.data[0]->is<AstTypeGroup>());
if (FFlag::LuauAstTypeGroup3 ? shouldParenthesize : unconditionallyParenthesize) if (FFlag::LuauAstTypeGroup3 ? shouldParenthesize : unconditionallyParenthesize)
{
if (openParenthesesPosition)
advance(*openParenthesesPosition);
writer.symbol("("); writer.symbol("(");
}
ArgNameInserter(writer, argNames, argNamesColonPositions)();
// Only variadic tail // Only variadic tail
if (list.types.size == 0) if (list.types.size == 0)
@ -1363,33 +1435,50 @@ struct Printer
} }
if (FFlag::LuauAstTypeGroup3 ? shouldParenthesize : unconditionallyParenthesize) if (FFlag::LuauAstTypeGroup3 ? shouldParenthesize : unconditionallyParenthesize)
{
if (closeParenthesesPosition)
advance(*closeParenthesesPosition);
writer.symbol(")"); writer.symbol(")");
}
} }
else else
{ {
if (openParenthesesPosition)
advance(*openParenthesesPosition);
writer.symbol("("); 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) for (const auto& el : list.types)
{ {
if (first) comma();
first = false; argName();
else
writer.symbol(",");
visualizeTypeAnnotation(*el); visualizeTypeAnnotation(*el);
} }
if (list.tailType) if (list.tailType)
{ {
writer.symbol(","); comma();
visualizeTypePackAnnotation(*list.tailType, false); visualizeTypePackAnnotation(*list.tailType, false);
} }
if (closeParenthesesPosition)
advance(*closeParenthesesPosition);
writer.symbol(")"); 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) bool isIntegerish(double d)
{ {
if (d <= std::numeric_limits<int>::max() && d >= std::numeric_limits<int>::min()) if (d <= std::numeric_limits<int>::max() && d >= std::numeric_limits<int>::min())
@ -1406,7 +1495,7 @@ struct Printer
{ {
writer.symbol("("); writer.symbol("(");
visualize(*a->expr); visualize(*a->expr);
advance(Position{a->location.end.line, a->location.end.column - 1}); advanceBefore(a->location.end, 1);
writer.symbol(")"); writer.symbol(")");
} }
else if (expr.is<AstExprConstantNil>()) else if (expr.is<AstExprConstantNil>())
@ -1775,6 +1864,14 @@ struct Printer
writer.advance(newPos); 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) void visualize(AstStat& program)
{ {
advance(program.location.begin); advance(program.location.begin);
@ -1817,8 +1914,8 @@ struct Printer
visualizeBlock(*a->body); visualizeBlock(*a->body);
if (const auto cstNode = lookupCstNode<CstStatRepeat>(a)) if (const auto cstNode = lookupCstNode<CstStatRepeat>(a))
writer.advance(cstNode->untilPosition); writer.advance(cstNode->untilPosition);
else if (a->condition->location.begin.column > 5) else
writer.advance(Position{a->condition->location.begin.line, a->condition->location.begin.column - 6}); advanceBefore(a->condition->location.begin, 6);
writer.keyword("until"); writer.keyword("until");
visualize(*a->condition); visualize(*a->condition);
} }
@ -2121,7 +2218,20 @@ struct Printer
{ {
if (writeTypes) 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); writer.identifier(t->name.value);
visualizeFunctionBody(*t->body); visualizeFunctionBody(*t->body);
} }
@ -2152,16 +2262,22 @@ struct Printer
if (program.hasSemicolon) if (program.hasSemicolon)
{ {
if (FFlag::LuauStoreCSTData) if (FFlag::LuauStoreCSTData)
advance(Position{program.location.end.line, program.location.end.column - 1}); advanceBefore(program.location.end, 1);
writer.symbol(";"); writer.symbol(";");
} }
} }
void visualizeFunctionBody(AstExprFunction& func) 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) 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("<"); writer.symbol("<");
for (const auto& o : func.generics) for (const auto& o : func.generics)
{ {
@ -2176,13 +2292,19 @@ struct Printer
writer.advance(o->location.begin); writer.advance(o->location.begin);
writer.identifier(o->name.value); writer.identifier(o->name.value);
if (const auto* genericTypePackCstNode = lookupCstNode<CstGenericTypePack>(o))
advance(genericTypePackCstNode->ellipsisPosition);
writer.symbol("..."); writer.symbol("...");
} }
if (cstNode)
advance(cstNode->closeGenericsPosition);
writer.symbol(">"); writer.symbol(">");
} }
if (func.argLocation)
advance(func.argLocation->begin);
writer.symbol("("); writer.symbol("(");
CommaSeparatorInserter comma(writer); CommaSeparatorInserter comma(writer, cstNode ? cstNode->argsCommaPositions.begin() : nullptr);
for (size_t i = 0; i < func.args.size; ++i) 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(")"); writer.symbol(")");
if (writeTypes && func.returnAnnotation) if (writeTypes && func.returnAnnotation)
{ {
if (cstNode)
advance(cstNode->returnSpecifierPosition);
writer.symbol(":"); writer.symbol(":");
writer.space(); writer.space();
@ -2340,9 +2466,13 @@ struct Printer
} }
else if (const auto& a = typeAnnotation.as<AstTypeFunction>()) else if (const auto& a = typeAnnotation.as<AstTypeFunction>())
{ {
const auto cstNode = lookupCstNode<CstTypeFunction>(a);
if (a->generics.size > 0 || a->genericPacks.size > 0) 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("<"); writer.symbol("<");
for (const auto& o : a->generics) for (const auto& o : a->generics)
{ {
@ -2357,15 +2487,29 @@ struct Printer
writer.advance(o->location.begin); writer.advance(o->location.begin);
writer.identifier(o->name.value); writer.identifier(o->name.value);
if (const auto* genericTypePackCstNode = lookupCstNode<CstGenericTypePack>(o))
advance(genericTypePackCstNode->ellipsisPosition);
writer.symbol("..."); writer.symbol("...");
} }
if (cstNode)
advance(cstNode->closeGenericsPosition);
writer.symbol(">"); 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("->"); writer.symbol("->");
visualizeTypeList(a->returnTypes, true); visualizeTypeList(a->returnTypes, true);
} }
@ -2557,6 +2701,15 @@ struct Printer
for (size_t i = 0; i < a->types.size; ++i) 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) if (i > 0)
{ {
writer.maybeSpace(a->types.data[i]->location.begin, 2); writer.maybeSpace(a->types.data[i]->location.begin, 2);
@ -2599,7 +2752,7 @@ struct Printer
{ {
writer.symbol("("); writer.symbol("(");
visualizeTypeAnnotation(*a->type); visualizeTypeAnnotation(*a->type);
advance(Position{a->location.end.line, a->location.end.column - 1}); advanceBefore(a->location.end, 1);
writer.symbol(")"); writer.symbol(")");
} }
else if (const auto& a = typeAnnotation.as<AstTypeSingletonBool>()) else if (const auto& a = typeAnnotation.as<AstTypeSingletonBool>())

View file

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

View file

@ -26,10 +26,13 @@
#include "Luau/VisitType.h" #include "Luau/VisitType.h"
#include <algorithm> #include <algorithm>
#include <sstream>
LUAU_FASTFLAG(DebugLuauMagicTypes) LUAU_FASTFLAG(DebugLuauMagicTypes)
LUAU_FASTFLAG(LuauFreeTypesMustHaveBounds) LUAU_FASTFLAG(LuauFreeTypesMustHaveBounds)
LUAU_FASTFLAGVARIABLE(LuauImproveTypePathsInErrors)
LUAU_FASTFLAG(LuauUserTypeFunTypecheck)
namespace Luau namespace Luau
{ {
@ -1201,7 +1204,8 @@ void TypeChecker2::visit(AstStatTypeAlias* stat)
void TypeChecker2::visit(AstStatTypeFunction* 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) void TypeChecker2::visit(AstTypeList types)
@ -2701,20 +2705,61 @@ Reasonings TypeChecker2::explainReasonings_(TID subTy, TID superTy, Location loc
if (!subLeafTy && !superLeafTy && !subLeafTp && !superLeafTp) 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); 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 (FFlag::LuauImproveTypePathsInErrors)
if (reasoning.variance == SubtypingVariance::Invariant) {
relation = "exactly"; std::string relation = "a subtype of";
else if (reasoning.variance == SubtypingVariance::Contravariant) if (reasoning.variance == SubtypingVariance::Invariant)
relation = "a supertype of"; relation = "exactly";
else if (reasoning.variance == SubtypingVariance::Contravariant)
relation = "a supertype of";
std::string reason; std::string subLeafAsString = toString(subLeaf);
if (reasoning.subPath == reasoning.superPath) // if the string is empty, it must be an empty type pack
reason = "at " + toString(reasoning.subPath) + ", " + toString(subLeaf) + " is not " + relation + " " + toString(superLeaf); 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 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 we haven't already proved this isn't suppressing, we have to keep checking.
if (suppressed) if (suppressed)

View file

@ -18,6 +18,7 @@
#include "Luau/ToString.h" #include "Luau/ToString.h"
#include "Luau/TxnLog.h" #include "Luau/TxnLog.h"
#include "Luau/Type.h" #include "Luau/Type.h"
#include "Luau/TypeChecker2.h"
#include "Luau/TypeFunctionReductionGuesser.h" #include "Luau/TypeFunctionReductionGuesser.h"
#include "Luau/TypeFunctionRuntime.h" #include "Luau/TypeFunctionRuntime.h"
#include "Luau/TypeFunctionRuntimeBuilder.h" #include "Luau/TypeFunctionRuntimeBuilder.h"
@ -49,12 +50,13 @@ LUAU_FASTFLAGVARIABLE(DebugLuauLogTypeFamilies)
LUAU_FASTFLAG(DebugLuauEqSatSimplification) LUAU_FASTFLAG(DebugLuauEqSatSimplification)
LUAU_FASTFLAGVARIABLE(LuauMetatableTypeFunctions) LUAU_FASTFLAGVARIABLE(LuauMetatableTypeFunctions)
LUAU_FASTFLAGVARIABLE(LuauClipNestedAndRecursiveUnion) LUAU_FASTFLAGVARIABLE(LuauClipNestedAndRecursiveUnion)
LUAU_FASTFLAGVARIABLE(LuauDoNotGeneralizeInTypeFunctions) LUAU_FASTFLAGVARIABLE(LuauIndexTypeFunctionImprovements)
LUAU_FASTFLAGVARIABLE(LuauPreventReentrantTypeFunctionReduction) LUAU_FASTFLAGVARIABLE(LuauIndexTypeFunctionFunctionMetamethods)
LUAU_FASTFLAGVARIABLE(LuauIntersectNotNil) LUAU_FASTFLAGVARIABLE(LuauIntersectNotNil)
LUAU_FASTFLAGVARIABLE(LuauSkipNoRefineDuringRefinement) LUAU_FASTFLAGVARIABLE(LuauSkipNoRefineDuringRefinement)
LUAU_FASTFLAGVARIABLE(LuauDontForgetToReduceUnionFunc) LUAU_FASTFLAGVARIABLE(LuauDontForgetToReduceUnionFunc)
LUAU_FASTFLAGVARIABLE(LuauSearchForRefineableType) LUAU_FASTFLAGVARIABLE(LuauSearchForRefineableType)
LUAU_FASTFLAGVARIABLE(LuauIndexAnyIsAny)
namespace Luau namespace Luau
{ {
@ -451,48 +453,29 @@ static FunctionGraphReductionResult reduceFunctionsInternal(
TypeFunctionReducer reducer{std::move(queuedTys), std::move(queuedTps), std::move(shouldGuess), std::move(cyclics), location, ctx, force}; TypeFunctionReducer reducer{std::move(queuedTys), std::move(queuedTps), std::move(shouldGuess), std::move(cyclics), location, ctx, force};
int iterationCount = 0; 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, reducer.step();
// 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}; ++iterationCount;
while (!reducer.done()) if (iterationCount > DFInt::LuauTypeFamilyGraphReductionMaximumSteps)
{ {
reducer.step(); reducer.result.errors.emplace_back(location, CodeTooComplex{});
break;
++iterationCount;
if (iterationCount > DFInt::LuauTypeFamilyGraphReductionMaximumSteps)
{
reducer.result.errors.emplace_back(location, CodeTooComplex{});
break;
}
} }
return std::move(reducer.result);
} }
else
{
while (!reducer.done())
{
reducer.step();
++iterationCount; return std::move(reducer.result);
if (iterationCount > DFInt::LuauTypeFamilyGraphReductionMaximumSteps)
{
reducer.result.errors.emplace_back(location, CodeTooComplex{});
break;
}
}
return std::move(reducer.result);
}
} }
FunctionGraphReductionResult reduceTypeFunctions(TypeId entrypoint, Location location, TypeFunctionContext ctx, bool force) FunctionGraphReductionResult reduceTypeFunctions(TypeId entrypoint, Location location, TypeFunctionContext ctx, bool force)
@ -861,15 +844,6 @@ TypeFunctionReductionResult<TypeId> lenTypeFunction(
if (isPending(operandTy, ctx->solver)) if (isPending(operandTy, ctx->solver))
return {std::nullopt, Reduction::MaybeOk, {operandTy}, {}}; 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); std::shared_ptr<const NormalizedType> normTy = ctx->normalizer->normalize(operandTy);
NormalizationResult inhabited = ctx->normalizer->isInhabited(normTy.get()); NormalizationResult inhabited = ctx->normalizer->isInhabited(normTy.get());
@ -953,15 +927,6 @@ TypeFunctionReductionResult<TypeId> unmTypeFunction(
if (isPending(operandTy, ctx->solver)) if (isPending(operandTy, ctx->solver))
return {std::nullopt, Reduction::MaybeOk, {operandTy}, {}}; 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); 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. // if the operand failed to normalize, we can't reduce, but know nothing about inhabitance.
@ -1196,21 +1161,6 @@ TypeFunctionReductionResult<TypeId> numericBinopTypeFunction(
else if (isPending(rhsTy, ctx->solver)) else if (isPending(rhsTy, ctx->solver))
return {std::nullopt, Reduction::MaybeOk, {rhsTy}, {}}; 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`. // 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> normLhsTy = ctx->normalizer->normalize(lhsTy);
std::shared_ptr<const NormalizedType> normRhsTy = ctx->normalizer->normalize(rhsTy); std::shared_ptr<const NormalizedType> normRhsTy = ctx->normalizer->normalize(rhsTy);
@ -1433,21 +1383,6 @@ TypeFunctionReductionResult<TypeId> concatTypeFunction(
else if (isPending(rhsTy, ctx->solver)) else if (isPending(rhsTy, ctx->solver))
return {std::nullopt, Reduction::MaybeOk, {rhsTy}, {}}; 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> normLhsTy = ctx->normalizer->normalize(lhsTy);
std::shared_ptr<const NormalizedType> normRhsTy = ctx->normalizer->normalize(rhsTy); std::shared_ptr<const NormalizedType> normRhsTy = ctx->normalizer->normalize(rhsTy);
@ -1548,21 +1483,6 @@ TypeFunctionReductionResult<TypeId> andTypeFunction(
else if (isPending(rhsTy, ctx->solver)) else if (isPending(rhsTy, ctx->solver))
return {std::nullopt, Reduction::MaybeOk, {rhsTy}, {}}; 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. // 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 filteredLhs = simplifyIntersection(ctx->builtins, ctx->arena, lhsTy, ctx->builtins->falsyType);
SimplifyResult overallResult = simplifyUnion(ctx->builtins, ctx->arena, rhsTy, filteredLhs.result); SimplifyResult overallResult = simplifyUnion(ctx->builtins, ctx->arena, rhsTy, filteredLhs.result);
@ -1603,21 +1523,6 @@ TypeFunctionReductionResult<TypeId> orTypeFunction(
else if (isPending(rhsTy, ctx->solver)) else if (isPending(rhsTy, ctx->solver))
return {std::nullopt, Reduction::MaybeOk, {rhsTy}, {}}; 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. // 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 filteredLhs = simplifyIntersection(ctx->builtins, ctx->arena, lhsTy, ctx->builtins->truthyType);
SimplifyResult overallResult = simplifyUnion(ctx->builtins, ctx->arena, rhsTy, filteredLhs.result); SimplifyResult overallResult = simplifyUnion(ctx->builtins, ctx->arena, rhsTy, filteredLhs.result);
@ -1689,21 +1594,6 @@ static TypeFunctionReductionResult<TypeId> comparisonTypeFunction(
lhsTy = follow(lhsTy); lhsTy = follow(lhsTy);
rhsTy = follow(rhsTy); 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 // 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); std::shared_ptr<const NormalizedType> normLhsTy = ctx->normalizer->normalize(lhsTy);
@ -1827,21 +1717,6 @@ TypeFunctionReductionResult<TypeId> eqTypeFunction(
else if (isPending(rhsTy, ctx->solver)) else if (isPending(rhsTy, ctx->solver))
return {std::nullopt, Reduction::MaybeOk, {rhsTy}, {}}; 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> normLhsTy = ctx->normalizer->normalize(lhsTy);
std::shared_ptr<const NormalizedType> normRhsTy = ctx->normalizer->normalize(rhsTy); std::shared_ptr<const NormalizedType> normRhsTy = ctx->normalizer->normalize(rhsTy);
NormalizationResult lhsInhabited = ctx->normalizer->isInhabited(normLhsTy.get()); NormalizationResult lhsInhabited = ctx->normalizer->isInhabited(normLhsTy.get());
@ -2000,20 +1875,6 @@ TypeFunctionReductionResult<TypeId> refineTypeFunction(
auto stepRefine = [&ctx](TypeId target, TypeId discriminant) -> std::pair<TypeId, std::vector<TypeId>> auto stepRefine = [&ctx](TypeId target, TypeId discriminant) -> std::pair<TypeId, std::vector<TypeId>>
{ {
std::vector<TypeId> toBlock; 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 // we need a more complex check for blocking on the discriminant in particular
FindRefinementBlockers frb; FindRefinementBlockers frb;
frb.traverse(discriminant); frb.traverse(discriminant);
@ -2131,15 +1992,6 @@ TypeFunctionReductionResult<TypeId> singletonTypeFunction(
if (isPending(type, ctx->solver)) if (isPending(type, ctx->solver))
return {std::nullopt, Reduction::MaybeOk, {type}, {}}; 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; TypeId followed = type;
// we want to follow through a negation here as well. // we want to follow through a negation here as well.
if (auto negation = get<NegationType>(followed)) if (auto negation = get<NegationType>(followed))
@ -2207,73 +2059,24 @@ TypeFunctionReductionResult<TypeId> unionTypeFunction(
if (typeParams.size() == 1) if (typeParams.size() == 1)
return {follow(typeParams[0]), Reduction::MaybeOk, {}, {}}; return {follow(typeParams[0]), Reduction::MaybeOk, {}, {}};
if (FFlag::LuauClipNestedAndRecursiveUnion)
CollectUnionTypeOptions collector{ctx};
collector.traverse(instance);
if (!collector.blockingTypes.empty())
{ {
std::vector<TypeId> blockingTypes{collector.blockingTypes.begin(), collector.blockingTypes.end()};
CollectUnionTypeOptions collector{ctx}; return {std::nullopt, Reduction::MaybeOk, std::move(blockingTypes), {}};
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, {}, {}};
} }
// 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; TypeId resultTy = ctx->builtins->neverType;
for (auto ty : types) for (auto ty : collector.options)
{ {
SimplifyResult result = simplifyUnion(ctx->builtins, ctx->arena, resultTy, ty); 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()) if (!result.blockedTypes.empty())
return {std::nullopt, Reduction::MaybeOk, {result.blockedTypes.begin(), result.blockedTypes.end()}, {}}; return {std::nullopt, Reduction::MaybeOk, {result.blockedTypes.begin(), result.blockedTypes.end()}, {}};
@ -2281,6 +2084,7 @@ TypeFunctionReductionResult<TypeId> unionTypeFunction(
} }
return {resultTy, Reduction::MaybeOk, {}, {}}; return {resultTy, Reduction::MaybeOk, {}, {}};
} }
@ -2621,7 +2425,12 @@ bool searchPropsAndIndexer(
if (auto propUnionTy = get<UnionType>(propTy)) if (auto propUnionTy = get<UnionType>(propTy))
{ {
for (TypeId option : propUnionTy->options) 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 else // property is a singular type or intersection type -> we can simply append
result.insert(propTy); result.insert(propTy);
@ -2641,7 +2450,12 @@ bool searchPropsAndIndexer(
if (auto idxResUnionTy = get<UnionType>(idxResultTy)) if (auto idxResUnionTy = get<UnionType>(idxResultTy))
{ {
for (TypeId option : idxResUnionTy->options) 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 else // indexResultType is a singular type or intersection type -> we can simply append
result.insert(idxResultTy); result.insert(idxResultTy);
@ -2656,7 +2470,7 @@ bool searchPropsAndIndexer(
/* Handles recursion / metamethods of tables/classes /* Handles recursion / metamethods of tables/classes
`isRaw` parameter indicates whether or not we should follow __index metamethods `isRaw` parameter indicates whether or not we should follow __index metamethods
returns false if property of `ty` could not be found */ 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); indexer = follow(indexer);
indexee = follow(indexee); indexee = follow(indexee);
@ -2686,13 +2500,113 @@ bool tblIndexInto(TypeId indexer, TypeId indexee, DenseHashSet<TypeId>& result,
ErrorVec dummy; ErrorVec dummy;
std::optional<TypeId> mmType = findMetatableEntry(ctx->builtins, dummy, indexee, "__index", Location{}); std::optional<TypeId> mmType = findMetatableEntry(ctx->builtins, dummy, indexee, "__index", Location{});
if (mmType) if (mmType)
return tblIndexInto(indexer, *mmType, result, ctx, isRaw); return tblIndexInto_DEPRECATED(indexer, *mmType, result, ctx, isRaw);
} }
} }
return false; 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, /* Vocabulary note: indexee refers to the type that contains the properties,
indexer refers to the type that is used to access indexee indexer refers to the type that is used to access indexee
Example: index<Person, "name"> => `Person` is the indexee and `"name"` is the indexer */ Example: index<Person, "name"> => `Person` is the indexee and `"name"` is the indexer */
@ -2710,6 +2624,13 @@ TypeFunctionReductionResult<TypeId> indexFunctionImpl(
if (!indexeeNormTy) if (!indexeeNormTy)
return {std::nullopt, Reduction::MaybeOk, {}, {}}; 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 we don't have either just tables or just classes, we've got nothing to index into
if (indexeeNormTy->hasTables() == indexeeNormTy->hasClasses()) if (indexeeNormTy->hasTables() == indexeeNormTy->hasClasses())
return {std::nullopt, Reduction::Erroneous, {}, {}}; return {std::nullopt, Reduction::Erroneous, {}, {}};
@ -2809,17 +2730,19 @@ TypeFunctionReductionResult<TypeId> indexFunctionImpl(
} }
} }
// Call `follow()` on each element to resolve all Bound types before returning if (!FFlag::LuauIndexTypeFunctionImprovements)
std::transform( {
properties.begin(), // Call `follow()` on each element to resolve all Bound types before returning
properties.end(), std::transform(
properties.begin(), properties.begin(),
[](TypeId ty) properties.end(),
{ properties.begin(),
return follow(ty); [](TypeId ty)
} {
return follow(ty);
}
); );
}
// If the type being reduced to is a single type, no need to union // If the type being reduced to is a single type, no need to union
if (properties.size() == 1) if (properties.size() == 1)
return {*properties.begin(), Reduction::MaybeOk, {}, {}}; return {*properties.begin(), Reduction::MaybeOk, {}, {}};

View file

@ -13,10 +13,7 @@
#include <set> #include <set>
#include <vector> #include <vector>
LUAU_FASTFLAGVARIABLE(LuauTypeFunFixHydratedClasses)
LUAU_DYNAMIC_FASTINT(LuauTypeFunctionSerdeIterationLimit) LUAU_DYNAMIC_FASTINT(LuauTypeFunctionSerdeIterationLimit)
LUAU_FASTFLAGVARIABLE(LuauTypeFunSingletonEquality)
LUAU_FASTFLAGVARIABLE(LuauUserTypeFunTypeofReturnsType)
LUAU_FASTFLAGVARIABLE(LuauTypeFunPrintFix) LUAU_FASTFLAGVARIABLE(LuauTypeFunPrintFix)
LUAU_FASTFLAGVARIABLE(LuauTypeFunReadWriteParents) LUAU_FASTFLAGVARIABLE(LuauTypeFunReadWriteParents)
@ -1617,11 +1614,8 @@ void registerTypeUserData(lua_State* L)
// Create and register metatable for type userdata // Create and register metatable for type userdata
luaL_newmetatable(L, "type"); 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 // Protect metatable from being changed
lua_pushstring(L, "The metatable is locked"); 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* lp = get<TypeFunctionBooleanSingleton>(&lhs);
const TypeFunctionBooleanSingleton* rp = get<TypeFunctionBooleanSingleton>(FFlag::LuauTypeFunSingletonEquality ? &rhs : &lhs); const TypeFunctionBooleanSingleton* rp = get<TypeFunctionBooleanSingleton>(&rhs);
if (lp && rp) if (lp && rp)
return lp->value == rp->value; return lp->value == rp->value;
} }
{ {
const TypeFunctionStringSingleton* lp = get<TypeFunctionStringSingleton>(&lhs); const TypeFunctionStringSingleton* lp = get<TypeFunctionStringSingleton>(&lhs);
const TypeFunctionStringSingleton* rp = get<TypeFunctionStringSingleton>(FFlag::LuauTypeFunSingletonEquality ? &rhs : &lhs); const TypeFunctionStringSingleton* rp = get<TypeFunctionStringSingleton>(&rhs);
if (lp && rp) if (lp && rp)
return lp->value == rp->value; return lp->value == rp->value;
} }
@ -1918,10 +1912,7 @@ bool areEqual(SeenSet& seen, const TypeFunctionClassType& lhs, const TypeFunctio
if (seenSetContains(seen, &lhs, &rhs)) if (seenSetContains(seen, &lhs, &rhs))
return true; return true;
if (FFlag::LuauTypeFunFixHydratedClasses) return lhs.classTy == rhs.classTy;
return lhs.classTy == rhs.classTy;
else
return lhs.name_DEPRECATED == rhs.name_DEPRECATED;
} }
bool areEqual(SeenSet& seen, const TypeFunctionType& lhs, const TypeFunctionType& rhs) 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 // used to control the recursion limit of any operations done by user-defined type functions
// currently, controls serialization, deserialization, and `type.copy` // currently, controls serialization, deserialization, and `type.copy`
LUAU_DYNAMIC_FASTINTVARIABLE(LuauTypeFunctionSerdeIterationLimit, 100'000); LUAU_DYNAMIC_FASTINTVARIABLE(LuauTypeFunctionSerdeIterationLimit, 100'000);
LUAU_FASTFLAG(LuauTypeFunFixHydratedClasses)
LUAU_FASTFLAG(LuauTypeFunReadWriteParents) LUAU_FASTFLAG(LuauTypeFunReadWriteParents)
namespace Luau namespace Luau
@ -209,19 +208,11 @@ private:
} }
else if (auto c = get<ClassType>(ty)) 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
// Since there aren't any new class types being created in type functions, we will deserialize by using a direct reference to the target = typeFunctionRuntime->typeArena.allocate(
// original class TypeFunctionClassType{{}, std::nullopt, std::nullopt, std::nullopt, std::nullopt, std::nullopt, ty}
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}
);
}
} }
else if (auto g = get<GenericType>(ty)) else if (auto g = get<GenericType>(ty))
{ {
@ -713,17 +704,7 @@ private:
} }
else if (auto c = get<TypeFunctionClassType>(ty)) else if (auto c = get<TypeFunctionClassType>(ty))
{ {
if (FFlag::LuauTypeFunFixHydratedClasses) target = c->classTy;
{
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");
}
} }
else if (auto g = get<TypeFunctionGenericType>(ty)) else if (auto g = get<TypeFunctionGenericType>(ty))
{ {

View file

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

View file

@ -14,7 +14,8 @@
#include <type_traits> #include <type_traits>
LUAU_FASTFLAG(LuauSolverV2); LUAU_FASTFLAG(LuauSolverV2);
LUAU_FASTFLAGVARIABLE(LuauDisableNewSolverAssertsInMixedMode); LUAU_FASTFLAGVARIABLE(LuauDisableNewSolverAssertsInMixedMode)
// Maximum number of steps to follow when traversing a path. May not always // 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 // equate to the number of components in a path, depending on the traversal
// logic. // logic.
@ -638,6 +639,247 @@ std::string toString(const TypePath::Path& path, bool prefixDot)
return result.str(); 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) static bool traverse(TraversalState& state, const Path& path)
{ {
auto step = [&state](auto&& c) auto step = [&state](auto&& c)

View file

@ -677,7 +677,7 @@ struct FreeTypeSearcher : TypeVisitor
DenseHashSet<const void*> seenPositive{nullptr}; DenseHashSet<const void*> seenPositive{nullptr};
DenseHashSet<const void*> seenNegative{nullptr}; DenseHashSet<const void*> seenNegative{nullptr};
bool seenWithPolarity(const void* ty) bool seenWithCurrentPolarity(const void* ty)
{ {
switch (polarity) switch (polarity)
{ {
@ -719,7 +719,7 @@ struct FreeTypeSearcher : TypeVisitor
bool visit(TypeId ty) override bool visit(TypeId ty) override
{ {
if (seenWithPolarity(ty)) if (seenWithCurrentPolarity(ty))
return false; return false;
LUAU_ASSERT(ty); LUAU_ASSERT(ty);
@ -728,7 +728,7 @@ struct FreeTypeSearcher : TypeVisitor
bool visit(TypeId ty, const FreeType& ft) override bool visit(TypeId ty, const FreeType& ft) override
{ {
if (seenWithPolarity(ty)) if (seenWithCurrentPolarity(ty))
return false; return false;
if (!subsumes(scope, ft.scope)) if (!subsumes(scope, ft.scope))
@ -753,7 +753,7 @@ struct FreeTypeSearcher : TypeVisitor
bool visit(TypeId ty, const TableType& tt) override bool visit(TypeId ty, const TableType& tt) override
{ {
if (seenWithPolarity(ty)) if (seenWithCurrentPolarity(ty))
return false; return false;
if ((tt.state == TableState::Free || tt.state == TableState::Unsealed) && subsumes(scope, tt.scope)) if ((tt.state == TableState::Free || tt.state == TableState::Unsealed) && subsumes(scope, tt.scope))
@ -799,7 +799,7 @@ struct FreeTypeSearcher : TypeVisitor
bool visit(TypeId ty, const FunctionType& ft) override bool visit(TypeId ty, const FunctionType& ft) override
{ {
if (seenWithPolarity(ty)) if (seenWithCurrentPolarity(ty))
return false; return false;
flip(); flip();
@ -818,7 +818,7 @@ struct FreeTypeSearcher : TypeVisitor
bool visit(TypePackId tp, const FreeTypePack& ftp) override bool visit(TypePackId tp, const FreeTypePack& ftp) override
{ {
if (seenWithPolarity(tp)) if (seenWithCurrentPolarity(tp))
return false; return false;
if (!subsumes(scope, ftp.scope)) if (!subsumes(scope, ftp.scope))

View file

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

View file

@ -105,6 +105,20 @@ public:
Position closeBracketPosition; 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 class CstExprTable : public CstNode
{ {
public: public:
@ -311,6 +325,17 @@ public:
Position equalsPosition; Position equalsPosition;
}; };
class CstStatTypeFunction : public CstNode
{
public:
LUAU_CST_RTTI(CstStatTypeFunction)
CstStatTypeFunction(Position typeKeywordPosition, Position functionKeywordPosition);
Position typeKeywordPosition;
Position functionKeywordPosition;
};
class CstTypeReference : public CstNode class CstTypeReference : public CstNode
{ {
public: public:
@ -359,6 +384,32 @@ public:
bool isArray = false; 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 class CstTypeTypeof : public CstNode
{ {
public: public:
@ -382,4 +433,26 @@ public:
unsigned int blockDepth; 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 } // namespace Luau

View file

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

View file

@ -1069,6 +1069,16 @@ void AstTypeTypeof::visit(AstVisitor* visitor)
expr->visit(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) AstTypeUnion::AstTypeUnion(const Location& location, const AstArray<AstType*>& types)
: AstType(ClassIndex(), location) : AstType(ClassIndex(), location)
, types(types) , types(types)

View file

@ -38,6 +38,10 @@ CstExprIndexExpr::CstExprIndexExpr(Position openBracketPosition, Position closeB
{ {
} }
CstExprFunction::CstExprFunction() : CstNode(CstClassIndex())
{
}
CstExprTable::CstExprTable(const AstArray<Item>& items) CstExprTable::CstExprTable(const AstArray<Item>& items)
: CstNode(CstClassIndex()) : CstNode(CstClassIndex())
, items(items) , items(items)
@ -160,6 +164,13 @@ CstStatTypeAlias::CstStatTypeAlias(
{ {
} }
CstStatTypeFunction::CstStatTypeFunction(Position typeKeywordPosition, Position functionKeywordPosition)
: CstNode(CstClassIndex())
, typeKeywordPosition(typeKeywordPosition)
, functionKeywordPosition(functionKeywordPosition)
{
}
CstTypeReference::CstTypeReference( CstTypeReference::CstTypeReference(
std::optional<Position> prefixPointPosition, std::optional<Position> prefixPointPosition,
Position openParametersPosition, 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) CstTypeTypeof::CstTypeTypeof(Position openPosition, Position closePosition)
: CstNode(CstClassIndex()) : CstNode(CstClassIndex())
, openPosition(openPosition) , openPosition(openPosition)
@ -197,4 +230,18 @@ CstTypeSingletonString::CstTypeSingletonString(AstArray<char> sourceString, CstE
LUAU_ASSERT(quoteStyle != CstExprConstantString::QuotedInterp); 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 } // namespace Luau

View file

@ -8,8 +8,6 @@
#include <limits.h> #include <limits.h>
LUAU_FASTFLAGVARIABLE(LexerFixInterpStringStart)
namespace Luau namespace Luau
{ {
@ -789,7 +787,7 @@ Lexeme Lexer::readNext()
return Lexeme(Location(start, 1), '}'); return Lexeme(Location(start, 1), '}');
} }
return readInterpolatedStringSection(FFlag::LexerFixInterpStringStart ? start : position(), Lexeme::InterpStringMid, Lexeme::InterpStringEnd); return readInterpolatedStringSection(start, Lexeme::InterpStringMid, Lexeme::InterpStringEnd);
} }
case '=': case '=':

View file

@ -27,6 +27,7 @@ LUAU_FASTFLAGVARIABLE(LuauPreserveUnionIntersectionNodeForLeadingTokenSingleType
LUAU_FASTFLAGVARIABLE(LuauAstTypeGroup3) LUAU_FASTFLAGVARIABLE(LuauAstTypeGroup3)
LUAU_FASTFLAGVARIABLE(ParserNoErrorLimit) LUAU_FASTFLAGVARIABLE(ParserNoErrorLimit)
LUAU_FASTFLAGVARIABLE(LuauFixDoBlockEndLocation) LUAU_FASTFLAGVARIABLE(LuauFixDoBlockEndLocation)
LUAU_FASTFLAGVARIABLE(LuauParseOptionalAsNode)
namespace Luau namespace Luau
{ {
@ -647,16 +648,16 @@ AstStat* Parser::parseFor()
else else
{ {
TempVector<Binding> names(scratchBinding); TempVector<Binding> names(scratchBinding);
TempVector<Position> varsCommaPosition(scratchPosition); AstArray<Position> varsCommaPosition;
names.push_back(varname); names.push_back(varname);
if (lexer.current().type == ',') if (lexer.current().type == ',')
{ {
if (FFlag::LuauStoreCSTData && options.storeCstData) if (FFlag::LuauStoreCSTData && options.storeCstData)
{ {
varsCommaPosition.push_back(lexer.current().location.begin); Position initialCommaPosition = lexer.current().location.begin;
nextLexeme(); nextLexeme();
parseBindingList(names, false, &varsCommaPosition); parseBindingList(names, false, &varsCommaPosition, initialCommaPosition);
} }
else else
{ {
@ -701,7 +702,7 @@ AstStat* Parser::parseFor()
AstStatForIn* node = AstStatForIn* node =
allocator.alloc<AstStatForIn>(Location(start, end), copy(vars), copy(values), body, hasIn, inLocation, hasDo, matchDo.location); allocator.alloc<AstStatForIn>(Location(start, end), copy(vars), copy(values), body, hasIn, inLocation, hasDo, matchDo.location);
if (options.storeCstData) if (options.storeCstData)
cstNodeMap[node] = allocator.alloc<CstStatForIn>(copy(varsCommaPosition), copy(valuesCommaPositions)); cstNodeMap[node] = allocator.alloc<CstStatForIn>(varsCommaPosition, copy(valuesCommaPositions));
return node; return node;
} }
else else
@ -957,7 +958,7 @@ AstStat* Parser::parseLocal(const AstArray<AstAttr*>& attributes)
matchRecoveryStopOnToken['=']++; matchRecoveryStopOnToken['=']++;
TempVector<Binding> names(scratchBinding); TempVector<Binding> names(scratchBinding);
TempVector<Position> varsCommaPositions(scratchPosition); AstArray<Position> varsCommaPositions;
if (FFlag::LuauStoreCSTData && options.storeCstData) if (FFlag::LuauStoreCSTData && options.storeCstData)
parseBindingList(names, false, &varsCommaPositions); parseBindingList(names, false, &varsCommaPositions);
else else
@ -990,7 +991,7 @@ AstStat* Parser::parseLocal(const AstArray<AstAttr*>& attributes)
{ {
AstStatLocal* node = allocator.alloc<AstStatLocal>(Location(start, end), copy(vars), copy(values), equalsSignLocation); AstStatLocal* node = allocator.alloc<AstStatLocal>(Location(start, end), copy(vars), copy(values), equalsSignLocation);
if (options.storeCstData) if (options.storeCstData)
cstNodeMap[node] = allocator.alloc<CstStatLocal>(copy(varsCommaPositions), copy(valuesCommaPositions)); cstNodeMap[node] = allocator.alloc<CstStatLocal>(varsCommaPositions, copy(valuesCommaPositions));
return node; return node;
} }
else else
@ -1033,7 +1034,7 @@ AstStat* Parser::parseTypeAlias(const Location& start, bool exported, Position t
{ {
// parsing a type function // parsing a type function
if (lexer.current().type == Lexeme::ReservedFunction) if (lexer.current().type == Lexeme::ReservedFunction)
return parseTypeFunction(start, exported); return parseTypeFunction(start, exported, typeKeywordPosition);
// parsing a type alias // parsing a type alias
@ -1046,7 +1047,7 @@ AstStat* Parser::parseTypeAlias(const Location& start, bool exported, Position t
name = Name(nameError, lexer.current().location); name = Name(nameError, lexer.current().location);
Position genericsOpenPosition{0, 0}; Position genericsOpenPosition{0, 0};
TempVector<Position> genericsCommaPositions(scratchPosition); AstArray<Position> genericsCommaPositions;
Position genericsClosePosition{0, 0}; Position genericsClosePosition{0, 0};
auto [generics, genericPacks] = FFlag::LuauStoreCSTData && options.storeCstData auto [generics, genericPacks] = FFlag::LuauStoreCSTData && options.storeCstData
? parseGenericTypeList( ? parseGenericTypeList(
@ -1065,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); allocator.alloc<AstStatTypeAlias>(Location(start, type->location), name->name, name->location, generics, genericPacks, type, exported);
if (options.storeCstData) if (options.storeCstData)
cstNodeMap[node] = allocator.alloc<CstStatTypeAlias>( cstNodeMap[node] = allocator.alloc<CstStatTypeAlias>(
typeKeywordPosition, genericsOpenPosition, copy(genericsCommaPositions), genericsClosePosition, equalsPosition typeKeywordPosition, genericsOpenPosition, genericsCommaPositions, genericsClosePosition, equalsPosition
); );
return node; return node;
} }
@ -1076,7 +1077,7 @@ AstStat* Parser::parseTypeAlias(const Location& start, bool exported, Position t
} }
// type function Name `(' arglist `)' `=' funcbody `end' // 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(); Lexeme matchFn = lexer.current();
nextLexeme(); nextLexeme();
@ -1097,7 +1098,18 @@ AstStat* Parser::parseTypeFunction(const Location& start, bool exported)
matchRecoveryStopOnToken[Lexeme::ReservedEnd]--; 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() AstDeclaredClassProp Parser::parseDeclaredClassMethod()
@ -1439,7 +1451,15 @@ std::pair<AstExprFunction*, AstLocal*> Parser::parseFunctionBody(
{ {
Location start = matchFunction.location; 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(); MatchLexeme matchParen = lexer.current();
expectAndConsume('(', "function"); expectAndConsume('(', "function");
@ -1464,7 +1484,8 @@ std::pair<AstExprFunction*, AstLocal*> Parser::parseFunctionBody(
AstTypePack* varargAnnotation = nullptr; AstTypePack* varargAnnotation = nullptr;
if (lexer.current().type != ')') 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; std::optional<Location> argLocation;
@ -1476,7 +1497,7 @@ std::pair<AstExprFunction*, AstLocal*> Parser::parseFunctionBody(
if (FFlag::LuauErrorRecoveryForTableTypes) if (FFlag::LuauErrorRecoveryForTableTypes)
matchRecoveryStopOnToken[')']--; matchRecoveryStopOnToken[')']--;
std::optional<AstTypeList> typelist = parseOptionalReturnType(); std::optional<AstTypeList> typelist = parseOptionalReturnType(cstNode ? &cstNode->returnSpecifierPosition : nullptr);
AstLocal* funLocal = nullptr; AstLocal* funLocal = nullptr;
@ -1503,8 +1524,9 @@ std::pair<AstExprFunction*, AstLocal*> Parser::parseFunctionBody(
bool hasEnd = expectMatchEndAndConsume(Lexeme::ReservedEnd, matchFunction); bool hasEnd = expectMatchEndAndConsume(Lexeme::ReservedEnd, matchFunction);
body->hasEnd = hasEnd; body->hasEnd = hasEnd;
return { if (FFlag::LuauStoreCSTData)
allocator.alloc<AstExprFunction>( {
AstExprFunction* node = allocator.alloc<AstExprFunction>(
Location(start, end), Location(start, end),
attributes, attributes,
generics, generics,
@ -1519,9 +1541,34 @@ std::pair<AstExprFunction*, AstLocal*> Parser::parseFunctionBody(
typelist, typelist,
varargAnnotation, varargAnnotation,
argLocation 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 // explist ::= {exp `,'} exp
@ -1559,8 +1606,13 @@ Parser::Binding Parser::parseBinding()
} }
// bindinglist ::= (binding | `...') [`,' bindinglist] // 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) while (true)
{ {
if (lexer.current().type == Lexeme::Dot3 && allowDot3) if (lexer.current().type == Lexeme::Dot3 && allowDot3)
@ -1575,6 +1627,9 @@ std::tuple<bool, Location, AstTypePack*> Parser::parseBindingList(TempVector<Bin
tailAnnotation = parseVariadicArgumentTypePack(); tailAnnotation = parseVariadicArgumentTypePack();
} }
if (FFlag::LuauStoreCSTData && commaPositions)
*commaPositions = copy(localCommaPositions);
return {true, varargLocation, tailAnnotation}; return {true, varargLocation, tailAnnotation};
} }
@ -1583,10 +1638,13 @@ std::tuple<bool, Location, AstTypePack*> Parser::parseBindingList(TempVector<Bin
if (lexer.current().type != ',') if (lexer.current().type != ',')
break; break;
if (FFlag::LuauStoreCSTData && commaPositions) if (FFlag::LuauStoreCSTData && commaPositions)
commaPositions->push_back(lexer.current().location.begin); localCommaPositions.push_back(lexer.current().location.begin);
nextLexeme(); nextLexeme();
} }
if (FFlag::LuauStoreCSTData && commaPositions)
*commaPositions = copy(localCommaPositions);
return {false, Location(), nullptr}; return {false, Location(), nullptr};
} }
@ -1602,7 +1660,12 @@ AstType* Parser::parseOptionalType()
} }
// TypeList ::= Type [`,' TypeList] | ...Type // 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) while (true)
{ {
@ -1614,22 +1677,33 @@ AstTypePack* Parser::parseTypeList(TempVector<AstType*>& result, TempVector<std:
// Fill in previous argument names with empty slots // Fill in previous argument names with empty slots
while (resultNames.size() < result.size()) while (resultNames.size() < result.size())
resultNames.push_back({}); 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}); resultNames.push_back(AstArgumentName{AstName(lexer.current().name), lexer.current().location});
nextLexeme(); nextLexeme();
if (FFlag::LuauStoreCSTData && nameColonPositions)
nameColonPositions->push_back(lexer.current().location.begin);
expectAndConsume(':'); expectAndConsume(':');
} }
else if (!resultNames.empty()) else if (!resultNames.empty())
{ {
// If we have a type with named arguments, provide elements for all types // If we have a type with named arguments, provide elements for all types
resultNames.push_back({}); resultNames.push_back({});
if (FFlag::LuauStoreCSTData && nameColonPositions)
nameColonPositions->push_back({});
} }
result.push_back(parseType()); result.push_back(parseType());
if (lexer.current().type != ',') if (lexer.current().type != ',')
break; break;
if (FFlag::LuauStoreCSTData && commaPositions)
commaPositions->push_back(lexer.current().location.begin);
nextLexeme(); nextLexeme();
if (lexer.current().type == ')') if (lexer.current().type == ')')
@ -1642,13 +1716,15 @@ AstTypePack* Parser::parseTypeList(TempVector<AstType*>& result, TempVector<std:
return nullptr; 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 == ':' || lexer.current().type == Lexeme::SkinnyArrow)
{ {
if (lexer.current().type == Lexeme::SkinnyArrow) if (lexer.current().type == Lexeme::SkinnyArrow)
report(lexer.current().location, "Function return type annotations are written after ':' instead of '->'"); report(lexer.current().location, "Function return type annotations are written after ':' instead of '->'");
if (FFlag::LuauStoreCSTData && returnSpecifierPosition)
*returnSpecifierPosition = lexer.current().location.begin;
nextLexeme(); nextLexeme();
unsigned int oldRecursionCount = recursionCounter; unsigned int oldRecursionCount = recursionCounter;
@ -2016,7 +2092,14 @@ AstTypeOrPack Parser::parseFunctionType(bool allowPack, const AstArray<AstAttr*>
Lexeme begin = lexer.current(); 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(); Lexeme parameterStart = lexer.current();
@ -2026,10 +2109,17 @@ AstTypeOrPack Parser::parseFunctionType(bool allowPack, const AstArray<AstAttr*>
TempVector<AstType*> params(scratchType); TempVector<AstType*> params(scratchType);
TempVector<std::optional<AstArgumentName>> names(scratchOptArgName); TempVector<std::optional<AstArgumentName>> names(scratchOptArgName);
TempVector<std::optional<Position>> nameColonPositions(scratchOptPosition);
TempVector<Position> argCommaPositions(scratchPosition);
AstTypePack* varargAnnotation = nullptr; AstTypePack* varargAnnotation = nullptr;
if (lexer.current().type != ')') 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; Location closeArgsLocation = lexer.current().location;
expectMatchAndConsume(')', parameterStart, true); expectMatchAndConsume(')', parameterStart, true);
@ -2047,7 +2137,20 @@ AstTypeOrPack Parser::parseFunctionType(bool allowPack, const AstArray<AstAttr*>
if (params.size() == 1 && !varargAnnotation && !forceFunctionType && !returnTypeIntroducer) if (params.size() == 1 && !varargAnnotation && !forceFunctionType && !returnTypeIntroducer)
{ {
if (allowPack) 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 else
{ {
if (FFlag::LuauAstTypeGroup3) if (FFlag::LuauAstTypeGroup3)
@ -2058,11 +2161,46 @@ AstTypeOrPack Parser::parseFunctionType(bool allowPack, const AstArray<AstAttr*>
} }
if (!forceFunctionType && !returnTypeIntroducer && allowPack) 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); 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( AstType* Parser::parseFunctionTypeTail(
@ -2124,7 +2262,9 @@ AstType* Parser::parseTypeSuffix(AstType* type, const Location& begin)
bool isUnion = false; bool isUnion = false;
bool isIntersection = false; bool isIntersection = false;
bool hasOptional = false; // Clip with FFlag::LuauParseOptionalAsNode
bool hasOptional_DEPRECATED = false;
unsigned int optionalCount = 0;
Location location = begin; Location location = begin;
@ -2148,11 +2288,19 @@ AstType* Parser::parseTypeSuffix(AstType* type, const Location& begin)
Location loc = lexer.current().location; Location loc = lexer.current().location;
nextLexeme(); nextLexeme();
if (!hasOptional) if (FFlag::LuauParseOptionalAsNode)
parts.push_back(allocator.alloc<AstTypeReference>(loc, std::nullopt, nameNil, std::nullopt, loc)); {
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; isUnion = true;
hasOptional = true; hasOptional_DEPRECATED = true;
} }
else if (c == '&') else if (c == '&')
{ {
@ -2172,8 +2320,16 @@ AstType* Parser::parseTypeSuffix(AstType* type, const Location& begin)
else else
break; break;
if (parts.size() > unsigned(FInt::LuauTypeLengthLimit) + hasOptional) if (FFlag::LuauParseOptionalAsNode)
ParseError::raise(parts.back()->location, "Exceeded allowed type length; simplify your type annotation to make the code compile"); {
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) if (FFlag::LuauPreserveUnionIntersectionNodeForLeadingTokenSingleType)
@ -2465,7 +2621,17 @@ AstTypePack* Parser::parseVariadicArgumentTypePack()
// This will not fail because of the lookahead guard. // This will not fail because of the lookahead guard.
expectAndConsume(Lexeme::Dot3, "generic type pack annotation"); 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 // Variadic: T
else else
@ -2493,7 +2659,17 @@ AstTypePack* Parser::parseTypePack()
// This will not fail because of the lookahead guard. // This will not fail because of the lookahead guard.
expectAndConsume(Lexeme::Dot3, "generic type pack annotation"); 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 // TODO: shouldParseTypePack can be removed and parseTypePack can be called unconditionally instead
@ -3348,12 +3524,13 @@ Parser::Name Parser::parseIndexName(const char* context, const Position& previou
std::pair<AstArray<AstGenericType*>, AstArray<AstGenericTypePack*>> Parser::parseGenericTypeList( std::pair<AstArray<AstGenericType*>, AstArray<AstGenericTypePack*>> Parser::parseGenericTypeList(
bool withDefaultValues, bool withDefaultValues,
Position* openPosition, Position* openPosition,
TempVector<Position>* commaPositions, AstArray<Position>* commaPositions,
Position* closePosition Position* closePosition
) )
{ {
TempVector<AstGenericType*> names{scratchGenericTypes}; TempVector<AstGenericType*> names{scratchGenericTypes};
TempVector<AstGenericTypePack*> namePacks{scratchGenericTypePacks}; TempVector<AstGenericTypePack*> namePacks{scratchGenericTypePacks};
TempVector<Position> localCommaPositions{scratchPosition};
if (lexer.current().type == '<') if (lexer.current().type == '<')
{ {
@ -3483,7 +3660,7 @@ std::pair<AstArray<AstGenericType*>, AstArray<AstGenericTypePack*>> Parser::pars
if (lexer.current().type == ',') if (lexer.current().type == ',')
{ {
if (FFlag::LuauStoreCSTData && commaPositions) if (FFlag::LuauStoreCSTData && commaPositions)
commaPositions->push_back(lexer.current().location.begin); localCommaPositions.push_back(lexer.current().location.begin);
nextLexeme(); nextLexeme();
if (lexer.current().type == '>') if (lexer.current().type == '>')
@ -3501,6 +3678,9 @@ std::pair<AstArray<AstGenericType*>, AstArray<AstGenericTypePack*>> Parser::pars
expectMatchAndConsume('>', begin); expectMatchAndConsume('>', begin);
} }
if (FFlag::LuauStoreCSTData && commaPositions)
*commaPositions = copy(localCommaPositions);
AstArray<AstGenericType*> generics = copy(names); AstArray<AstGenericType*> generics = copy(names);
AstArray<AstGenericTypePack*> genericPacks = copy(namePacks); AstArray<AstGenericTypePack*> genericPacks = copy(namePacks);
return {generics, genericPacks}; return {generics, genericPacks};

View file

@ -125,6 +125,10 @@ static LuauBytecodeType getType(
{ {
return getType(group->type, generics, typeAliases, resolveAliases, hostVectorType, userdataTypes, bytecode); 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; return LBC_TYPE_ANY;
} }

View file

@ -53,9 +53,9 @@ type A = (number, string) -> ...any
ModulePtr module = frontend.moduleResolver.getModule("game/Gui/Modules/A"); ModulePtr module = frontend.moduleResolver.getModule("game/Gui/Modules/A");
LUAU_ASSERT(module->ats.typeInfo.size() == 1); REQUIRE(module->ats.typeInfo.size() == 1);
LUAU_ASSERT(module->ats.typeInfo[0].code == Pattern::Alias); CHECK(module->ats.typeInfo[0].code == Pattern::Alias);
LUAU_ASSERT(module->ats.typeInfo[0].node == "type A = (number, string)->( ...any)"); CHECK(module->ats.typeInfo[0].node == "type A = (number, string)->( ...any)");
} }
TEST_CASE_FIXTURE(ATSFixture, "export_alias") 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"); ModulePtr module = frontend.moduleResolver.getModule("game/Gui/Modules/A");
LUAU_ASSERT(module->ats.typeInfo.size() == 1); REQUIRE(module->ats.typeInfo.size() == 1);
LUAU_ASSERT(module->ats.typeInfo[0].code == Pattern::Alias); CHECK(module->ats.typeInfo[0].code == Pattern::Alias);
if (FFlag::LuauStoreCSTData && FFlag::LuauAstTypeGroup3) 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) 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::LuauAstTypeGroup3) 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 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"); ModulePtr module = frontend.moduleResolver.getModule("game/Gui/Modules/A");
LUAU_ASSERT(module->ats.typeInfo.size() == 3); REQUIRE(module->ats.typeInfo.size() == 3);
LUAU_ASSERT(module->ats.typeInfo[1].code == Pattern::TypePk); CHECK(module->ats.typeInfo[1].code == Pattern::TypePk);
LUAU_ASSERT( CHECK(
module->ats.typeInfo[0].node == 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" "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"); 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") 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"); ModulePtr module = frontend.moduleResolver.getModule("game/Gui/Modules/A");
LUAU_ASSERT(module->ats.typeInfo.size() == 1); REQUIRE(module->ats.typeInfo.size() == 1);
LUAU_ASSERT(module->ats.typeInfo[0].code == Pattern::Alias); CHECK(module->ats.typeInfo[0].code == Pattern::Alias);
LUAU_ASSERT(module->ats.typeInfo[0].node == "type Pair<T> = {first: T, second: any}"); CHECK(module->ats.typeInfo[0].node == "type Pair<T> = {first: T, second: any}");
} }
TEST_CASE_FIXTURE(ATSFixture, "assign_uneq") TEST_CASE_FIXTURE(ATSFixture, "assign_uneq")
@ -190,7 +190,7 @@ local x, y, z = greetings("Dibri") -- mismatch
LUAU_REQUIRE_ERROR_COUNT(1, result1); LUAU_REQUIRE_ERROR_COUNT(1, result1);
ModulePtr module = frontend.moduleResolver.getModule("game/Gui/Modules/B"); 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") 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"); ModulePtr module = frontend.moduleResolver.getModule("game/Gui/Modules/A");
LUAU_ASSERT(module->ats.typeInfo.size() == 1); REQUIRE(module->ats.typeInfo.size() == 1);
LUAU_ASSERT(module->ats.typeInfo[0].code == Pattern::Alias); CHECK(module->ats.typeInfo[0].code == Pattern::Alias);
LUAU_ASSERT(module->ats.typeInfo[0].node == "type Pair<T> = (boolean, T)->( ...any)"); CHECK(module->ats.typeInfo[0].node == "type Pair<T> = (boolean, T)->( ...any)");
} }
TEST_CASE_FIXTURE(ATSFixture, "typeof_any_in_func") 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"); ModulePtr module = frontend.moduleResolver.getModule("game/Gui/Modules/A");
LUAU_ASSERT(module->ats.typeInfo.size() == 2); REQUIRE(module->ats.typeInfo.size() == 2);
LUAU_ASSERT(module->ats.typeInfo[0].code == Pattern::VarAnnot); CHECK(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"); 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") TEST_CASE_FIXTURE(ATSFixture, "generic_types")
@ -264,9 +264,9 @@ foo(addNumbers)
ModulePtr module = frontend.moduleResolver.getModule("game/Gui/Modules/A"); ModulePtr module = frontend.moduleResolver.getModule("game/Gui/Modules/A");
LUAU_ASSERT(module->ats.typeInfo.size() == 3); REQUIRE(module->ats.typeInfo.size() == 3);
LUAU_ASSERT(module->ats.typeInfo[1].code == Pattern::FuncApp); CHECK(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"); CHECK(module->ats.typeInfo[0].node == "local function foo<A>(a: (...A)->( any),...: A)\n return a(...)\nend");
} }
TEST_CASE_FIXTURE(ATSFixture, "no_annot") TEST_CASE_FIXTURE(ATSFixture, "no_annot")
@ -285,7 +285,7 @@ local character = script.Parent
ModulePtr module = frontend.moduleResolver.getModule("game/Gui/Modules/A"); 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") TEST_CASE_FIXTURE(ATSFixture, "if_any")
@ -314,9 +314,9 @@ end
ModulePtr module = frontend.moduleResolver.getModule("game/Gui/Modules/A"); ModulePtr module = frontend.moduleResolver.getModule("game/Gui/Modules/A");
LUAU_ASSERT(module->ats.typeInfo.size() == 1); REQUIRE(module->ats.typeInfo.size() == 1);
LUAU_ASSERT(module->ats.typeInfo[0].code == Pattern::FuncArg); CHECK(module->ats.typeInfo[0].code == Pattern::FuncArg);
LUAU_ASSERT( 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 = " 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" "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"); ModulePtr module = frontend.moduleResolver.getModule("game/Gui/Modules/A");
LUAU_ASSERT(module->ats.typeInfo.size() == 2); REQUIRE(module->ats.typeInfo.size() == 2);
LUAU_ASSERT(module->ats.typeInfo[0].code == Pattern::FuncRet); CHECK(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"); CHECK(module->ats.typeInfo[0].node == "local function f(): (number, ...any)\n return 1, 5\n end");
} }
TEST_CASE_FIXTURE(ATSFixture, "type_alias_intersection") 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"); ModulePtr module = frontend.moduleResolver.getModule("game/Gui/Modules/A");
LUAU_ASSERT(module->ats.typeInfo.size() == 3); REQUIRE(module->ats.typeInfo.size() == 3);
LUAU_ASSERT(module->ats.typeInfo[2].code == Pattern::VarAnnot); CHECK(module->ats.typeInfo[2].code == Pattern::VarAnnot);
LUAU_ASSERT(module->ats.typeInfo[2].node == "local vec2: Vector2 = {x = 1, y = 2}"); CHECK(module->ats.typeInfo[2].node == "local vec2: Vector2 = {x = 1, y = 2}");
} }
TEST_CASE_FIXTURE(ATSFixture, "var_func_arg") 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"); ModulePtr module = frontend.moduleResolver.getModule("game/Gui/Modules/A");
LUAU_ASSERT(module->ats.typeInfo.size() == 4); REQUIRE(module->ats.typeInfo.size() == 4);
LUAU_ASSERT(module->ats.typeInfo[0].code == Pattern::VarAny); CHECK(module->ats.typeInfo[0].code == Pattern::VarAny);
LUAU_ASSERT(module->ats.typeInfo[0].node == "local function f(...: any)\n end"); CHECK(module->ats.typeInfo[0].node == "local function f(...: any)\n end");
} }
TEST_CASE_FIXTURE(ATSFixture, "var_func_apps") 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"); ModulePtr module = frontend.moduleResolver.getModule("game/Gui/Modules/A");
LUAU_ASSERT(module->ats.typeInfo.size() == 3); REQUIRE(module->ats.typeInfo.size() == 3);
LUAU_ASSERT(module->ats.typeInfo[0].code == Pattern::VarAny); CHECK(module->ats.typeInfo[0].code == Pattern::VarAny);
LUAU_ASSERT(module->ats.typeInfo[0].node == "local function f(...: any)\n end"); 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"); CHECK_EQ(module->ats.typeInfo[0].node, "descendant.CollisionGroup = CAR_COLLISION_GROUP");
} }
else else
LUAU_ASSERT(module->ats.typeInfo.size() == 0); REQUIRE(module->ats.typeInfo.size() == 0);
} }
TEST_CASE_FIXTURE(ATSFixture, "unknown_symbol") TEST_CASE_FIXTURE(ATSFixture, "unknown_symbol")
@ -477,9 +477,9 @@ end
ModulePtr module = frontend.moduleResolver.getModule("game/Gui/Modules/A"); ModulePtr module = frontend.moduleResolver.getModule("game/Gui/Modules/A");
LUAU_ASSERT(module->ats.typeInfo.size() == 2); REQUIRE(module->ats.typeInfo.size() == 2);
LUAU_ASSERT(module->ats.typeInfo[0].code == Pattern::FuncArg); CHECK(module->ats.typeInfo[0].code == Pattern::FuncArg);
LUAU_ASSERT(module->ats.typeInfo[0].node == "local function manageRace(raceContainer: Model)\n RaceManager.new(raceContainer)\nend"); CHECK(module->ats.typeInfo[0].node == "local function manageRace(raceContainer: Model)\n RaceManager.new(raceContainer)\nend");
} }
TEST_CASE_FIXTURE(ATSFixture, "racing_3_short") TEST_CASE_FIXTURE(ATSFixture, "racing_3_short")
@ -518,9 +518,9 @@ initialize()
ModulePtr module = frontend.moduleResolver.getModule("game/Gui/Modules/A"); ModulePtr module = frontend.moduleResolver.getModule("game/Gui/Modules/A");
LUAU_ASSERT(module->ats.typeInfo.size() == 5); REQUIRE(module->ats.typeInfo.size() == 5);
LUAU_ASSERT(module->ats.typeInfo[0].code == Pattern::FuncArg); CHECK(module->ats.typeInfo[0].code == Pattern::FuncArg);
LUAU_ASSERT(module->ats.typeInfo[0].node == "local function manageRace(raceContainer: Model)\n RaceManager.new(raceContainer)\nend"); CHECK(module->ats.typeInfo[0].node == "local function manageRace(raceContainer: Model)\n RaceManager.new(raceContainer)\nend");
} }
TEST_CASE_FIXTURE(ATSFixture, "racing_collision_2") TEST_CASE_FIXTURE(ATSFixture, "racing_collision_2")
@ -596,10 +596,10 @@ initialize()
ModulePtr module = frontend.moduleResolver.getModule("game/Gui/Modules/A"); ModulePtr module = frontend.moduleResolver.getModule("game/Gui/Modules/A");
if (FFlag::LuauSkipNoRefineDuringRefinement) if (FFlag::LuauSkipNoRefineDuringRefinement)
CHECK_EQ(module->ats.typeInfo.size(), 12); REQUIRE_EQ(module->ats.typeInfo.size(), 12);
else else
LUAU_ASSERT(module->ats.typeInfo.size() == 11); REQUIRE(module->ats.typeInfo.size() == 11);
LUAU_ASSERT(module->ats.typeInfo[0].code == Pattern::FuncArg); CHECK(module->ats.typeInfo[0].code == Pattern::FuncArg);
if (FFlag::LuauStoreCSTData) if (FFlag::LuauStoreCSTData)
{ {
CHECK_EQ( CHECK_EQ(
@ -612,7 +612,7 @@ initialize()
} }
else else
{ {
LUAU_ASSERT( CHECK(
module->ats.typeInfo[0].node == module->ats.typeInfo[0].node ==
"local function onCharacterAdded(character: Model)\n\n character.DescendantAdded:Connect(function(descendant)\n if " "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 " "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"); ModulePtr module = frontend.moduleResolver.getModule("game/Gui/Modules/A");
LUAU_ASSERT(module->ats.typeInfo.size() == 7); REQUIRE(module->ats.typeInfo.size() == 7);
LUAU_ASSERT(module->ats.typeInfo[0].code == Pattern::FuncArg); CHECK(module->ats.typeInfo[0].code == Pattern::FuncArg);
LUAU_ASSERT( CHECK(
module->ats.typeInfo[0].node == module->ats.typeInfo[0].node ==
"local function setupKiosk(kiosk: Model)\n local spawnLocation = kiosk:FindFirstChild('SpawnLocation')\n assert(spawnLocation, " "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, " "`{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"); 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") TEST_CASE_FIXTURE(ATSFixture, "explicit_pack")
@ -739,9 +739,9 @@ type Bar = Foo<(number, any)>
ModulePtr module = frontend.moduleResolver.getModule("game/Gui/Modules/A"); ModulePtr module = frontend.moduleResolver.getModule("game/Gui/Modules/A");
LUAU_ASSERT(module->ats.typeInfo.size() == 1); REQUIRE(module->ats.typeInfo.size() == 1);
LUAU_ASSERT(module->ats.typeInfo[0].code == Pattern::Alias); CHECK(module->ats.typeInfo[0].code == Pattern::Alias);
LUAU_ASSERT(module->ats.typeInfo[0].node == "type Bar = Foo<(number, any)>"); CHECK(module->ats.typeInfo[0].node == "type Bar = Foo<(number, any)>");
} }
TEST_CASE_FIXTURE(ATSFixture, "local_val") 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"); ModulePtr module = frontend.moduleResolver.getModule("game/Gui/Modules/A");
LUAU_ASSERT(module->ats.typeInfo.size() == 1); REQUIRE(module->ats.typeInfo.size() == 1);
LUAU_ASSERT(module->ats.typeInfo[0].code == Pattern::Casts); CHECK(module->ats.typeInfo[0].code == Pattern::Casts);
LUAU_ASSERT(module->ats.typeInfo[0].node == "local a, b, c = 1 :: any"); CHECK(module->ats.typeInfo[0].node == "local a, b, c = 1 :: any");
} }
TEST_CASE_FIXTURE(ATSFixture, "var_any_local") 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"); ModulePtr module = frontend.moduleResolver.getModule("game/Gui/Modules/A");
LUAU_ASSERT(module->ats.typeInfo.size() == 3); REQUIRE(module->ats.typeInfo.size() == 3);
LUAU_ASSERT(module->ats.typeInfo[0].code == Pattern::VarAnnot); CHECK(module->ats.typeInfo[0].code == Pattern::VarAnnot);
LUAU_ASSERT(module->ats.typeInfo[0].node == "local x: any = 2, 3"); CHECK(module->ats.typeInfo[0].node == "local x: any = 2, 3");
} }
TEST_CASE_FIXTURE(ATSFixture, "table_uses_any") 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"); ModulePtr module = frontend.moduleResolver.getModule("game/Gui/Modules/A");
LUAU_ASSERT(module->ats.typeInfo.size() == 1); REQUIRE(module->ats.typeInfo.size() == 1);
LUAU_ASSERT(module->ats.typeInfo[0].code == Pattern::VarAnnot); CHECK(module->ats.typeInfo[0].code == Pattern::VarAnnot);
LUAU_ASSERT(module->ats.typeInfo[0].node == "local x: any = 0"); CHECK(module->ats.typeInfo[0].node == "local x: any = 0");
} }
TEST_CASE_FIXTURE(ATSFixture, "typeof_any") 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"); ModulePtr module = frontend.moduleResolver.getModule("game/Gui/Modules/A");
LUAU_ASSERT(module->ats.typeInfo.size() == 2); REQUIRE(module->ats.typeInfo.size() == 2);
LUAU_ASSERT(module->ats.typeInfo[1].code == Pattern::FuncArg); CHECK(module->ats.typeInfo[1].code == Pattern::FuncArg);
LUAU_ASSERT(module->ats.typeInfo[0].node == "function some1(x: typeof(x))\n end"); CHECK(module->ats.typeInfo[0].node == "function some1(x: typeof(x))\n end");
} }
TEST_CASE_FIXTURE(ATSFixture, "table_type_assigned") 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"); ModulePtr module = frontend.moduleResolver.getModule("game/Gui/Modules/A");
LUAU_ASSERT(module->ats.typeInfo.size() == 2); REQUIRE(module->ats.typeInfo.size() == 2);
LUAU_ASSERT(module->ats.typeInfo[1].code == Pattern::Assign); CHECK(module->ats.typeInfo[1].code == Pattern::Assign);
LUAU_ASSERT(module->ats.typeInfo[0].node == "local x: { x: any?} = {x = 1}"); CHECK(module->ats.typeInfo[0].node == "local x: { x: any?} = {x = 1}");
} }
TEST_CASE_FIXTURE(ATSFixture, "simple_func_wo_ret") 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"); ModulePtr module = frontend.moduleResolver.getModule("game/Gui/Modules/A");
LUAU_ASSERT(module->ats.typeInfo.size() == 1); REQUIRE(module->ats.typeInfo.size() == 1);
LUAU_ASSERT(module->ats.typeInfo[0].code == Pattern::FuncArg); CHECK(module->ats.typeInfo[0].code == Pattern::FuncArg);
LUAU_ASSERT(module->ats.typeInfo[0].node == "function some(x: any)\n end"); CHECK(module->ats.typeInfo[0].node == "function some(x: any)\n end");
} }
TEST_CASE_FIXTURE(ATSFixture, "simple_func_w_ret") 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"); ModulePtr module = frontend.moduleResolver.getModule("game/Gui/Modules/A");
LUAU_ASSERT(module->ats.typeInfo.size() == 1); REQUIRE(module->ats.typeInfo.size() == 1);
LUAU_ASSERT(module->ats.typeInfo[0].code == Pattern::FuncRet); CHECK(module->ats.typeInfo[0].code == Pattern::FuncRet);
LUAU_ASSERT(module->ats.typeInfo[0].node == "function other(y: number): any\n return 'gotcha!'\n end"); CHECK(module->ats.typeInfo[0].node == "function other(y: number): any\n return 'gotcha!'\n end");
} }
TEST_CASE_FIXTURE(ATSFixture, "nested_local") 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"); ModulePtr module = frontend.moduleResolver.getModule("game/Gui/Modules/A");
LUAU_ASSERT(module->ats.typeInfo.size() == 1); REQUIRE(module->ats.typeInfo.size() == 1);
LUAU_ASSERT(module->ats.typeInfo[0].code == Pattern::VarAnnot); CHECK(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"); 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") 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"); ModulePtr module = frontend.moduleResolver.getModule("game/Gui/Modules/A");
LUAU_ASSERT(module->ats.typeInfo.size() == 1); REQUIRE(module->ats.typeInfo.size() == 1);
LUAU_ASSERT(module->ats.typeInfo[0].code == Pattern::FuncArg); CHECK(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"); 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") 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"); ModulePtr module = frontend.moduleResolver.getModule("game/Gui/Modules/A");
LUAU_ASSERT(module->ats.typeInfo.size() == 2); REQUIRE(module->ats.typeInfo.size() == 2);
LUAU_ASSERT(module->ats.typeInfo[0].code == Pattern::Alias); CHECK(module->ats.typeInfo[0].code == Pattern::Alias);
LUAU_ASSERT(module->ats.typeInfo[0].node == "type Clear = any"); CHECK(module->ats.typeInfo[0].node == "type Clear = any");
} }
TEST_CASE_FIXTURE(ATSFixture, "multi_module_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"); ModulePtr module = frontend.moduleResolver.getModule("game/B");
LUAU_ASSERT(module->ats.typeInfo.size() == 2); REQUIRE(module->ats.typeInfo.size() == 2);
LUAU_ASSERT(module->ats.typeInfo[0].code == Pattern::Alias); CHECK(module->ats.typeInfo[0].code == Pattern::Alias);
LUAU_ASSERT(module->ats.typeInfo[0].node == "type Clear = any"); CHECK(module->ats.typeInfo[0].node == "type Clear = any");
} }
TEST_CASE_FIXTURE(ATSFixture, "cast_on_cyclic_req") 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"); ModulePtr module = frontend.moduleResolver.getModule("game/B");
LUAU_ASSERT(module->ats.typeInfo.size() == 3); REQUIRE(module->ats.typeInfo.size() == 3);
LUAU_ASSERT(module->ats.typeInfo[1].code == Pattern::Alias); CHECK(module->ats.typeInfo[1].code == Pattern::Alias);
LUAU_ASSERT(module->ats.typeInfo[1].node == "type Clear = any"); CHECK(module->ats.typeInfo[1].node == "type Clear = any");
} }

View file

@ -7,6 +7,8 @@
#include "Luau/VisitType.h" #include "Luau/VisitType.h"
#include "Luau/StringUtils.h" #include "Luau/StringUtils.h"
#include "ClassFixture.h"
#include "Fixture.h" #include "Fixture.h"
#include "ScopedFlags.h" #include "ScopedFlags.h"
@ -19,6 +21,8 @@ LUAU_FASTFLAG(LuauSetMetatableDoesNotTimeTravel)
LUAU_FASTINT(LuauTypeInferRecursionLimit) LUAU_FASTINT(LuauTypeInferRecursionLimit)
LUAU_FASTFLAG(LuauExposeRequireByStringAutocomplete) LUAU_FASTFLAG(LuauExposeRequireByStringAutocomplete)
LUAU_FASTFLAG(LuauAutocompleteUnionCopyPreviousSeen)
LUAU_FASTFLAG(LuauUserTypeFunTypecheck)
using namespace Luau; using namespace Luau;
@ -154,6 +158,10 @@ struct ACBuiltinsFixture : ACFixtureImpl<BuiltinsFixture>
{ {
}; };
struct ACClassFixture : ACFixtureImpl<ClassFixture>
{
};
TEST_SUITE_BEGIN("AutocompleteTest"); TEST_SUITE_BEGIN("AutocompleteTest");
TEST_CASE_FIXTURE(ACFixture, "empty_program") TEST_CASE_FIXTURE(ACFixture, "empty_program")
@ -4416,4 +4424,76 @@ local x = 1 + result.
CHECK(ac.entryMap.count("x")); 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(); TEST_SUITE_END();

View file

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

View file

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

View file

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

View file

@ -323,6 +323,7 @@ AstStatBlock* Fixture::parse(const std::string& source, const ParseOptions& pars
NotNull{&moduleResolver}, NotNull{&moduleResolver},
NotNull{&fileResolver}, NotNull{&fileResolver},
frontend.globals.globalScope, frontend.globals.globalScope,
frontend.globals.globalTypeFunctionScope,
/*prepareModuleScope*/ nullptr, /*prepareModuleScope*/ nullptr,
frontend.options, frontend.options,
{}, {},

View file

@ -43,6 +43,8 @@ LUAU_FASTFLAG(LuauDisableNewSolverAssertsInMixedMode)
LUAU_FASTFLAG(LuauCloneTypeAliasBindings) LUAU_FASTFLAG(LuauCloneTypeAliasBindings)
LUAU_FASTFLAG(LuauDoNotClonePersistentBindings) LUAU_FASTFLAG(LuauDoNotClonePersistentBindings)
LUAU_FASTFLAG(LuauCloneReturnTypePack) LUAU_FASTFLAG(LuauCloneReturnTypePack)
LUAU_FASTFLAG(LuauIncrementalAutocompleteDemandBasedCloning)
LUAU_FASTFLAG(LuauUserTypeFunTypecheck)
static std::optional<AutocompleteEntryMap> nullCallback(std::string tag, std::optional<const ClassType*> ptr, std::optional<std::string> contents) static std::optional<AutocompleteEntryMap> nullCallback(std::string tag, std::optional<const ClassType*> ptr, std::optional<std::string> contents)
{ {
@ -83,6 +85,7 @@ struct FragmentAutocompleteFixtureImpl : BaseType
ScopedFastFlag luauCloneTypeAliasBindings{FFlag::LuauCloneTypeAliasBindings, true}; ScopedFastFlag luauCloneTypeAliasBindings{FFlag::LuauCloneTypeAliasBindings, true};
ScopedFastFlag luauDoNotClonePersistentBindings{FFlag::LuauDoNotClonePersistentBindings, true}; ScopedFastFlag luauDoNotClonePersistentBindings{FFlag::LuauDoNotClonePersistentBindings, true};
ScopedFastFlag luauCloneReturnTypePack{FFlag::LuauCloneReturnTypePack, true}; ScopedFastFlag luauCloneReturnTypePack{FFlag::LuauCloneReturnTypePack, true};
ScopedFastFlag luauIncrementalAutocompleteDemandBasedCloning{FFlag::LuauIncrementalAutocompleteDemandBasedCloning, true};
FragmentAutocompleteFixtureImpl() FragmentAutocompleteFixtureImpl()
: BaseType(true) : BaseType(true)
@ -157,12 +160,14 @@ struct FragmentAutocompleteFixtureImpl : BaseType
this->check(document, getOptions()); this->check(document, getOptions());
FragmentAutocompleteStatusResult result = autocompleteFragment(updated, cursorPos, fragmentEndPosition); FragmentAutocompleteStatusResult result = autocompleteFragment(updated, cursorPos, fragmentEndPosition);
CHECK(result.status != FragmentAutocompleteStatus::InternalIce);
assertions(result); assertions(result);
ScopedFastFlag _{FFlag::LuauSolverV2, false}; ScopedFastFlag _{FFlag::LuauSolverV2, false};
this->check(document, getOptions()); this->check(document, getOptions());
result = autocompleteFragment(updated, cursorPos, fragmentEndPosition); result = autocompleteFragment(updated, cursorPos, fragmentEndPosition);
CHECK(result.status != FragmentAutocompleteStatus::InternalIce);
assertions(result); assertions(result);
} }
@ -2341,4 +2346,40 @@ z = a.P.E
autocompleteFragmentInBothSolvers(source, dest, Position{8, 9}, [](FragmentAutocompleteStatusResult& _) {}); autocompleteFragmentInBothSolvers(source, dest, Position{8, 9}, [](FragmentAutocompleteStatusResult& _) {});
} }
TEST_CASE_FIXTURE(FragmentAutocompleteBuiltinsFixture, "user_defined_type_function_local")
{
ScopedFastFlag luauUserTypeFunTypecheck{FFlag::LuauUserTypeFunTypecheck, true};
const std::string source = R"(--!strict
type function foo(x: type): type
if x.tag == "singleton" then
local t = x:value()
return types.unionof(types.singleton(t), types.singleton(nil))
end
return types.number
end
)";
const std::string dest = R"(--!strict
type function foo(x: type): type
if x.tag == "singleton" then
local t = x:value()
x
return types.unionof(types.singleton(t), types.singleton(nil))
end
return types.number
end
)";
// Only checking in new solver as old solver doesn't handle type functions and constraint solver will ICE
ScopedFastFlag sff{FFlag::LuauSolverV2, true};
this->check(source, getOptions());
FragmentAutocompleteStatusResult result = autocompleteFragment(dest, Position{4, 9}, std::nullopt);
CHECK(result.status != FragmentAutocompleteStatus::InternalIce);
}
TEST_SUITE_END(); TEST_SUITE_END();

View file

@ -18,6 +18,7 @@ LUAU_FASTFLAG(DebugLuauFreezeArena)
LUAU_FASTFLAG(DebugLuauMagicTypes) LUAU_FASTFLAG(DebugLuauMagicTypes)
LUAU_FASTFLAG(LuauSelectivelyRetainDFGArena) LUAU_FASTFLAG(LuauSelectivelyRetainDFGArena)
LUAU_FASTFLAG(LuauModuleHoldsAstRoot) LUAU_FASTFLAG(LuauModuleHoldsAstRoot)
LUAU_FASTFLAG(LuauImproveTypePathsInErrors)
namespace 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. // 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 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> // 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( REQUIRE_EQ(
R"(Type R"(Type
'{ count: string }' '{ count: string }'

View file

@ -8,8 +8,6 @@
using namespace Luau; using namespace Luau;
LUAU_FASTFLAG(LexerFixInterpStringStart)
TEST_SUITE_BEGIN("LexerTests"); TEST_SUITE_BEGIN("LexerTests");
TEST_CASE("broken_string_works") TEST_CASE("broken_string_works")
@ -156,7 +154,7 @@ TEST_CASE("string_interpolation_basic")
Lexeme interpEnd = lexer.next(); Lexeme interpEnd = lexer.next();
CHECK_EQ(interpEnd.type, Lexeme::InterpStringEnd); CHECK_EQ(interpEnd.type, Lexeme::InterpStringEnd);
// The InterpStringEnd should start with }, not `. // 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") TEST_CASE("string_interpolation_full")
@ -177,7 +175,7 @@ TEST_CASE("string_interpolation_full")
Lexeme interpMid = lexer.next(); Lexeme interpMid = lexer.next();
CHECK_EQ(interpMid.type, Lexeme::InterpStringMid); CHECK_EQ(interpMid.type, Lexeme::InterpStringMid);
CHECK_EQ(interpMid.toString(), "} {"); 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(); Lexeme quote2 = lexer.next();
CHECK_EQ(quote2.type, Lexeme::QuotedString); CHECK_EQ(quote2.type, Lexeme::QuotedString);
@ -186,7 +184,7 @@ TEST_CASE("string_interpolation_full")
Lexeme interpEnd = lexer.next(); Lexeme interpEnd = lexer.next();
CHECK_EQ(interpEnd.type, Lexeme::InterpStringEnd); CHECK_EQ(interpEnd.type, Lexeme::InterpStringEnd);
CHECK_EQ(interpEnd.toString(), "} end`"); 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") TEST_CASE("string_interpolation_double_brace")

View file

@ -17,6 +17,8 @@
LUAU_FASTFLAG(LuauNewNonStrictWarnOnUnknownGlobals) LUAU_FASTFLAG(LuauNewNonStrictWarnOnUnknownGlobals)
LUAU_FASTFLAG(LuauNonStrictVisitorImprovements) LUAU_FASTFLAG(LuauNonStrictVisitorImprovements)
LUAU_FASTFLAG(LuauNonStrictFuncDefErrorFix)
LUAU_FASTFLAG(LuauNormalizedBufferIsNotUnknown)
using namespace Luau; using namespace Luau;
@ -359,6 +361,23 @@ end
NONSTRICT_REQUIRE_FUNC_DEFINITION_ERR(Position(1, 11), "x", result); 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") TEST_CASE_FIXTURE(NonStrictTypeCheckerFixture, "local_fn_produces_error")
{ {
CheckResult result = checkNonStrict(R"( CheckResult result = checkNonStrict(R"(
@ -649,4 +668,17 @@ TEST_CASE_FIXTURE(Fixture, "unknown_globals_in_non_strict")
LUAU_REQUIRE_ERROR_COUNT(2, result); 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(); TEST_SUITE_END();

View file

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

View file

@ -23,6 +23,7 @@ LUAU_FASTFLAG(LuauExtendStatEndPosWithSemicolon)
LUAU_FASTFLAG(LuauPreserveUnionIntersectionNodeForLeadingTokenSingleType) LUAU_FASTFLAG(LuauPreserveUnionIntersectionNodeForLeadingTokenSingleType)
LUAU_FASTFLAG(LuauAstTypeGroup3) LUAU_FASTFLAG(LuauAstTypeGroup3)
LUAU_FASTFLAG(LuauFixDoBlockEndLocation) LUAU_FASTFLAG(LuauFixDoBlockEndLocation)
LUAU_FASTFLAG(LuauParseOptionalAsNode)
namespace namespace
{ {
@ -3818,7 +3819,10 @@ TEST_CASE_FIXTURE(Fixture, "grouped_function_type")
} }
else else
CHECK(unionTy->types.data[0]->is<AstTypeFunction>()); // () -> () 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") TEST_CASE_FIXTURE(Fixture, "complex_union_in_generic_ty")

View file

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

View file

@ -15,7 +15,8 @@
#include <initializer_list> #include <initializer_list>
LUAU_FASTFLAG(LuauSolverV2); LUAU_FASTFLAG(LuauSolverV2)
LUAU_FASTFLAG(LuauNormalizedBufferIsNotUnknown)
using namespace Luau; 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_NOT_SUBTYPE(childClass, meet(builtinTypes->classType, negate(rootClass)));
TEST_IS_SUBTYPE(anotherChildClass, meet(builtinTypes->classType, negate(childClass))); 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") TEST_CASE_FIXTURE(SubtypeFixture, "Root <: class")
{ {
CHECK_IS_SUBTYPE(rootClass, builtinTypes->classType); CHECK_IS_SUBTYPE(rootClass, builtinTypes->classType);

View file

@ -5,14 +5,16 @@
#include "Fixture.h" #include "Fixture.h"
#include "Luau/TypeChecker2.h"
#include "ScopedFlags.h" #include "ScopedFlags.h"
#include "doctest.h" #include "doctest.h"
using namespace Luau; using namespace Luau;
LUAU_FASTFLAG(LuauRecursiveTypeParameterRestriction); LUAU_FASTFLAG(LuauRecursiveTypeParameterRestriction)
LUAU_FASTFLAG(LuauSolverV2); LUAU_FASTFLAG(LuauSolverV2)
LUAU_FASTFLAG(LuauAttributeSyntax); LUAU_FASTFLAG(LuauAttributeSyntax)
LUAU_FASTFLAG(LuauImproveTypePathsInErrors)
TEST_SUITE_BEGIN("ToString"); TEST_SUITE_BEGIN("ToString");
@ -871,9 +873,28 @@ TEST_CASE_FIXTURE(Fixture, "tostring_error_mismatch")
)"); )");
std::string expected; 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 = 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)"; 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 else
expected = R"(Type expected = R"(Type
'{ a: number, b: string, c: { d: string } }' '{ a: number, b: string, c: { d: string } }'

View file

@ -15,7 +15,6 @@ using namespace Luau;
LUAU_FASTFLAG(LuauStoreCSTData) LUAU_FASTFLAG(LuauStoreCSTData)
LUAU_FASTFLAG(LuauExtendStatEndPosWithSemicolon) LUAU_FASTFLAG(LuauExtendStatEndPosWithSemicolon)
LUAU_FASTFLAG(LuauAstTypeGroup3); LUAU_FASTFLAG(LuauAstTypeGroup3);
LUAU_FASTFLAG(LexerFixInterpStringStart)
TEST_SUITE_BEGIN("TranspilerTests"); TEST_SUITE_BEGIN("TranspilerTests");
@ -304,6 +303,95 @@ TEST_CASE("function")
CHECK_EQ(two, transpile(two).code); 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") TEST_CASE("returns_spaces_around_tokens")
{ {
ScopedFastFlag _{FFlag::LuauStoreCSTData, true}; ScopedFastFlag _{FFlag::LuauStoreCSTData, true};
@ -968,7 +1056,17 @@ TEST_CASE_FIXTURE(Fixture, "type_lists_should_be_emitted_correctly")
end 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) local a:(string,number,...string)->(string,...number)=function(a:string,b:number,...:string): (string,...number)
end end
@ -1157,6 +1255,49 @@ local b: Packed<(number, string)>
CHECK_EQ(code, transpile(code, {}, true).code); 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") TEST_CASE_FIXTURE(Fixture, "transpile_union_type_nested")
{ {
std::string code = "local a: ((number)->(string))|((string)->(string))"; std::string code = "local a: ((number)->(string))|((string)->(string))";
@ -1488,7 +1629,6 @@ TEST_CASE_FIXTURE(Fixture, "transpile_string_interp")
{ {
ScopedFastFlag fflags[] = { ScopedFastFlag fflags[] = {
{FFlag::LuauStoreCSTData, true}, {FFlag::LuauStoreCSTData, true},
{FFlag::LexerFixInterpStringStart, true},
}; };
std::string code = R"( local _ = `hello {name}` )"; std::string code = R"( local _ = `hello {name}` )";
@ -1499,7 +1639,6 @@ TEST_CASE_FIXTURE(Fixture, "transpile_string_interp_multiline")
{ {
ScopedFastFlag fflags[] = { ScopedFastFlag fflags[] = {
{FFlag::LuauStoreCSTData, true}, {FFlag::LuauStoreCSTData, true},
{FFlag::LexerFixInterpStringStart, true},
}; };
std::string code = R"( local _ = `hello { std::string code = R"( local _ = `hello {
name name
@ -1512,7 +1651,6 @@ TEST_CASE_FIXTURE(Fixture, "transpile_string_interp_on_new_line")
{ {
ScopedFastFlag fflags[] = { ScopedFastFlag fflags[] = {
{FFlag::LuauStoreCSTData, true}, {FFlag::LuauStoreCSTData, true},
{FFlag::LexerFixInterpStringStart, true},
}; };
std::string code = R"( std::string code = R"(
error( error(
@ -1536,7 +1674,6 @@ TEST_CASE_FIXTURE(Fixture, "transpile_string_literal_escape")
{ {
ScopedFastFlag fflags[] = { ScopedFastFlag fflags[] = {
{FFlag::LuauStoreCSTData, true}, {FFlag::LuauStoreCSTData, true},
{FFlag::LexerFixInterpStringStart, true},
}; };
std::string code = R"( local _ = ` bracket = \{, backtick = \` = {'ok'} ` )"; 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); 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") TEST_CASE_FIXTURE(Fixture, "transpile_typeof_spaces_around_tokens")
{ {
ScopedFastFlag _{FFlag::LuauStoreCSTData, true}; ScopedFastFlag _{FFlag::LuauStoreCSTData, true};
@ -1752,4 +1905,122 @@ TEST_CASE("transpile_types_preserve_parentheses_style")
CHECK_EQ(code, transpile(code, {}, true).code); 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(); TEST_SUITE_END();

View file

@ -13,8 +13,11 @@
using namespace Luau; using namespace Luau;
LUAU_FASTFLAG(LuauSolverV2) LUAU_FASTFLAG(LuauSolverV2)
LUAU_FASTFLAG(LuauIndexTypeFunctionImprovements)
LUAU_DYNAMIC_FASTINT(LuauTypeFamilyApplicationCartesianProductLimit) LUAU_DYNAMIC_FASTINT(LuauTypeFamilyApplicationCartesianProductLimit)
LUAU_FASTFLAG(LuauIndexTypeFunctionFunctionMetamethods)
LUAU_FASTFLAG(LuauMetatableTypeFunctions) LUAU_FASTFLAG(LuauMetatableTypeFunctions)
LUAU_FASTFLAG(LuauIndexAnyIsAny)
struct TypeFunctionFixture : Fixture struct TypeFunctionFixture : Fixture
{ {
@ -904,6 +907,21 @@ end
LUAU_REQUIRE_NO_ERRORS(result); 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") TEST_CASE_FIXTURE(BuiltinsFixture, "index_type_function_works")
{ {
if (!FFlag::LuauSolverV2) if (!FFlag::LuauSolverV2)
@ -965,6 +983,31 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "index_type_function_works_w_array")
LUAU_REQUIRE_NO_ERRORS(result); 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") TEST_CASE_FIXTURE(BuiltinsFixture, "index_type_function_works_w_generic_types")
{ {
if (!FFlag::LuauSolverV2) 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'"); 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") TEST_CASE_FIXTURE(BuiltinsFixture, "index_type_function_errors_w_var_indexer")
{ {
if (!FFlag::LuauSolverV2) if (!FFlag::LuauSolverV2)

View file

@ -7,13 +7,12 @@
using namespace Luau; using namespace Luau;
LUAU_FASTFLAG(LuauTypeFunFixHydratedClasses)
LUAU_FASTFLAG(LuauSolverV2) LUAU_FASTFLAG(LuauSolverV2)
LUAU_FASTFLAG(DebugLuauEqSatSimplification) LUAU_FASTFLAG(DebugLuauEqSatSimplification)
LUAU_FASTFLAG(LuauTypeFunSingletonEquality)
LUAU_FASTFLAG(LuauUserTypeFunTypeofReturnsType)
LUAU_FASTFLAG(LuauTypeFunReadWriteParents) LUAU_FASTFLAG(LuauTypeFunReadWriteParents)
LUAU_FASTFLAG(LuauTypeFunPrintFix) LUAU_FASTFLAG(LuauTypeFunPrintFix)
LUAU_FASTFLAG(LuauImproveTypePathsInErrors)
LUAU_FASTFLAG(LuauUserTypeFunTypecheck)
TEST_SUITE_BEGIN("UserDefinedTypeFunctionTests"); TEST_SUITE_BEGIN("UserDefinedTypeFunctionTests");
@ -613,9 +612,12 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "udtf_function_methods_work")
ty:setreturns(nil, types.boolean) -- (string, number) -> (...boolean) ty:setreturns(nil, types.boolean) -- (string, number) -> (...boolean)
if ty:is("function") then if ty:is("function") then
-- creating a copy of `ty` parameters -- creating a copy of `ty` parameters
local arr = {} local arr: {type} = {}
for index, val in ty:parameters().head do local args = ty:parameters().head
table.insert(arr, val) if args then
for index, val in args do
table.insert(arr, val)
end
end end
return types.newfunction({head = arr}, ty:returns()) -- (string, number) -> (...boolean) return types.newfunction({head = arr}, ty:returns()) -- (string, number) -> (...boolean)
end end
@ -648,7 +650,6 @@ TEST_CASE_FIXTURE(ClassFixture, "udtf_class_serialization_works")
TEST_CASE_FIXTURE(ClassFixture, "udtf_class_serialization_works2") TEST_CASE_FIXTURE(ClassFixture, "udtf_class_serialization_works2")
{ {
ScopedFastFlag newSolver{FFlag::LuauSolverV2, true}; ScopedFastFlag newSolver{FFlag::LuauSolverV2, true};
ScopedFastFlag luauTypeFunFixHydratedClasses{FFlag::LuauTypeFunFixHydratedClasses, true};
CheckResult result = check(R"( CheckResult result = check(R"(
type function serialize_class(arg) type function serialize_class(arg)
@ -719,7 +720,7 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "udtf_check_mutability")
readresult = types.boolean, readresult = types.boolean,
writeresult = 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} ty:setproperty(types.singleton("string"), types.number) -- {string: number, [number]: boolean}
local metatbl = types.newtable(nil, nil, ty) -- { { }, @metatable { [number]: boolean, string: number } } local metatbl = types.newtable(nil, nil, ty) -- { { }, @metatable { [number]: boolean, string: number } }
-- mutate the table -- mutate the table
@ -894,6 +895,7 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "udtf_type_overrides_eq_metamethod")
if p1 == p2 and t1 == t2 then if p1 == p2 and t1 == t2 then
return types.number return types.number
end end
return types.unknown
end end
local function ok(idx: hello<>): number return idx end local function ok(idx: hello<>): number return idx end
)"); )");
@ -988,8 +990,37 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "udtf_calling_each_other_3")
end end
)"); )");
LUAU_REQUIRE_ERROR_COUNT(4, result); if (FFlag::LuauUserTypeFunTypecheck)
CHECK(toString(result.errors[0]) == R"('third' type function errored at runtime: [string "first"]:4: attempt to call a nil value)"); {
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") 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 local function ok2(idx: bar<'y'>): nil return idx end
)"); )");
// We are only checking first errors, others are mostly duplicates if (FFlag::LuauUserTypeFunTypecheck)
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)"); // We are only checking first errors, others are mostly duplicates
CHECK(toString(result.errors[1]) == R"(Type function instance bar<"x"> is uninhabited)"); 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") 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 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 if (FFlag::LuauUserTypeFunTypecheck)
UserDefinedTypeFunctionError* e = get<UserDefinedTypeFunctionError>(result.errors[0]); {
REQUIRE(e); // We are only checking first errors, others are mostly duplicates
CHECK(e->message == "'illegal' type function errored at runtime: [string \"illegal\"]:3: this function is not supported in type functions"); 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") 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"( CheckResult result = check(R"(
type function test(x) 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 end
local function ok(tbl: test<number>): never return tbl 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); 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)"); if (FFlag::LuauImproveTypePathsInErrors)
CHECK(toString(result.errors[2]) == R"(Type pack '"table"' could not be converted into 'never'; at [0], "table" is not a subtype of never)"); {
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") TEST_CASE_FIXTURE(BuiltinsFixture, "metatable_serialization")
@ -1293,8 +1375,12 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "implicit_export")
ScopedFastFlag newSolver{FFlag::LuauSolverV2, true}; ScopedFastFlag newSolver{FFlag::LuauSolverV2, true};
fileResolver.source["game/A"] = R"( fileResolver.source["game/A"] = R"(
type function concat(a, b) type function concat(a: type, b: type)
return types.singleton(a:value() .. b:value()) local as = a:value()
local bs = b:value()
assert(typeof(as) == "string")
assert(typeof(bs) == "string")
return types.singleton(as .. bs)
end end
export type Concat<T, U> = concat<T, U> export type Concat<T, U> = concat<T, U>
local a: concat<'first', 'second'> local a: concat<'first', 'second'>
@ -1342,8 +1428,12 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "explicit_export")
ScopedFastFlag newSolver{FFlag::LuauSolverV2, true}; ScopedFastFlag newSolver{FFlag::LuauSolverV2, true};
fileResolver.source["game/A"] = R"( fileResolver.source["game/A"] = R"(
export type function concat(a, b) export type function concat(a: type, b: type)
return types.singleton(a:value() .. b:value()) local as = a:value()
local bs = b:value()
assert(typeof(as) == "string")
assert(typeof(bs) == "string")
return types.singleton(as .. bs)
end end
local a: concat<'first', 'second'> local a: concat<'first', 'second'>
return {} return {}
@ -1892,7 +1982,6 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "udtf_eqsat_opaque")
TEST_CASE_FIXTURE(BuiltinsFixture, "udtf_singleton_equality_bool") TEST_CASE_FIXTURE(BuiltinsFixture, "udtf_singleton_equality_bool")
{ {
ScopedFastFlag newSolver{FFlag::LuauSolverV2, true}; ScopedFastFlag newSolver{FFlag::LuauSolverV2, true};
ScopedFastFlag luauTypeFunSingletonEquality{FFlag::LuauTypeFunSingletonEquality, true};
CheckResult result = check(R"( CheckResult result = check(R"(
type function compare(arg) 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") TEST_CASE_FIXTURE(BuiltinsFixture, "udtf_singleton_equality_string")
{ {
ScopedFastFlag newSolver{FFlag::LuauSolverV2, true}; ScopedFastFlag newSolver{FFlag::LuauSolverV2, true};
ScopedFastFlag luauTypeFunSingletonEquality{FFlag::LuauTypeFunSingletonEquality, true};
CheckResult result = check(R"( CheckResult result = check(R"(
type function compare(arg) 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") TEST_CASE_FIXTURE(BuiltinsFixture, "typeof_type_userdata_returns_type")
{ {
ScopedFastFlag solverV2{FFlag::LuauSolverV2, true}; ScopedFastFlag solverV2{FFlag::LuauSolverV2, true};
ScopedFastFlag luauUserTypeFunTypeofReturnsType{FFlag::LuauUserTypeFunTypeofReturnsType, true};
CheckResult result = check(R"( CheckResult result = check(R"(
type function test(t) type function test(t)
@ -1982,4 +2069,38 @@ TEST_CASE_FIXTURE(ClassFixture, "udtf_class_parent_ops")
LUAU_REQUIRE_NO_ERRORS(result); 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(); TEST_SUITE_END();

View file

@ -11,6 +11,7 @@ using namespace Luau;
LUAU_FASTFLAG(LuauSolverV2) LUAU_FASTFLAG(LuauSolverV2)
LUAU_FASTFLAG(LuauFixInfiniteRecursionInNormalization) LUAU_FASTFLAG(LuauFixInfiniteRecursionInNormalization)
LUAU_FASTFLAG(LuauImproveTypePathsInErrors)
TEST_SUITE_BEGIN("TypeAliases"); TEST_SUITE_BEGIN("TypeAliases");
@ -216,7 +217,11 @@ TEST_CASE_FIXTURE(Fixture, "generic_aliases")
)"); )");
LUAU_REQUIRE_ERROR_COUNT(1, result); 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(result.errors[0].location == Location{{4, 31}, {4, 44}});
CHECK_EQ(expected, toString(result.errors[0])); CHECK_EQ(expected, toString(result.errors[0]));
} }
@ -236,7 +241,11 @@ TEST_CASE_FIXTURE(Fixture, "dependent_generic_aliases")
LUAU_REQUIRE_ERROR_COUNT(1, result); LUAU_REQUIRE_ERROR_COUNT(1, result);
const std::string expected = 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(result.errors[0].location == Location{{4, 31}, {4, 52}});
CHECK_EQ(expected, toString(result.errors[0])); CHECK_EQ(expected, toString(result.errors[0]));

View file

@ -12,7 +12,7 @@ using namespace Luau;
LUAU_FASTFLAG(LuauSolverV2) LUAU_FASTFLAG(LuauSolverV2)
LUAU_FASTFLAG(LuauTableCloneClonesType3) LUAU_FASTFLAG(LuauTableCloneClonesType3)
LUAU_FASTFLAG(LuauStringFormatErrorSuppression) LUAU_FASTFLAG(LuauStringFormatErrorSuppression)
LUAU_FASTFLAG(LuauFreezeIgnorePersistent) LUAU_FASTFLAG(LuauImproveTypePathsInErrors)
TEST_SUITE_BEGIN("BuiltinTests"); TEST_SUITE_BEGIN("BuiltinTests");
@ -146,7 +146,20 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "sort_with_bad_predicate")
)"); )");
LUAU_REQUIRE_ERROR_COUNT(1, result); 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' '(number, number) -> boolean'
could not be converted into could not be converted into
'((string, string) -> boolean)?' '((string, string) -> boolean)?'
@ -985,7 +998,14 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "tonumber_returns_optional_number_type")
)"); )");
LUAU_REQUIRE_ERROR_COUNT(1, result); 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( CHECK_EQ(
"Type 'number?' could not be converted into 'number'; type number?[1] (nil) is not a subtype of number (number)", "Type 'number?' could not be converted into 'number'; type number?[1] (nil) is not a subtype of number (number)",
toString(result.errors[0]) 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") TEST_CASE_FIXTURE(BuiltinsFixture, "table_freeze_persistent_skip")
{ {
ScopedFastFlag luauFreezeIgnorePersistent{FFlag::LuauFreezeIgnorePersistent, true};
CheckResult result = check(R"( CheckResult result = check(R"(
table.freeze(table) table.freeze(table)
)"); )");
@ -1267,8 +1285,6 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "table_freeze_persistent_skip")
TEST_CASE_FIXTURE(BuiltinsFixture, "table_clone_persistent_skip") TEST_CASE_FIXTURE(BuiltinsFixture, "table_clone_persistent_skip")
{ {
ScopedFastFlag luauFreezeIgnorePersistent{FFlag::LuauFreezeIgnorePersistent, true};
CheckResult result = check(R"( CheckResult result = check(R"(
table.clone(table) table.clone(table)
)"); )");

View file

@ -13,7 +13,8 @@
using namespace Luau; using namespace Luau;
using std::nullopt; using std::nullopt;
LUAU_FASTFLAG(LuauSolverV2); LUAU_FASTFLAG(LuauSolverV2)
LUAU_FASTFLAG(LuauImproveTypePathsInErrors)
TEST_SUITE_BEGIN("TypeInferClasses"); TEST_SUITE_BEGIN("TypeInferClasses");
@ -545,7 +546,13 @@ local b: B = a
LUAU_REQUIRE_ERRORS(result); 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"); CHECK(toString(result.errors.at(0)) == "Type 'A' could not be converted into 'B'; at [read \"x\"], ChildClass is not exactly BaseClass");
else else
{ {

View file

@ -9,9 +9,7 @@
using namespace Luau; using namespace Luau;
LUAU_FASTFLAG(LuauClipNestedAndRecursiveUnion)
LUAU_FASTINT(LuauTypeInferRecursionLimit) LUAU_FASTINT(LuauTypeInferRecursionLimit)
LUAU_FASTFLAG(LuauPreventReentrantTypeFunctionReduction)
LUAU_FASTFLAG(LuauDontForgetToReduceUnionFunc) LUAU_FASTFLAG(LuauDontForgetToReduceUnionFunc)
TEST_SUITE_BEGIN("DefinitionTests"); TEST_SUITE_BEGIN("DefinitionTests");
@ -545,8 +543,6 @@ TEST_CASE_FIXTURE(Fixture, "definition_file_has_source_module_name_set")
TEST_CASE_FIXTURE(Fixture, "recursive_redefinition_reduces_rightfully") TEST_CASE_FIXTURE(Fixture, "recursive_redefinition_reduces_rightfully")
{ {
ScopedFastFlag _{FFlag::LuauClipNestedAndRecursiveUnion, true};
LUAU_REQUIRE_NO_ERRORS(check(R"( LUAU_REQUIRE_NO_ERRORS(check(R"(
local t: {[string]: string} = {} local t: {[string]: string} = {}
@ -592,7 +588,6 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "cli_142285_reduce_minted_union_func")
TEST_CASE_FIXTURE(Fixture, "vector3_overflow") 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. // We set this to zero to ensure that we either run to completion or stack overflow here.
ScopedFastInt sfi{FInt::LuauTypeInferRecursionLimit, 0}; ScopedFastInt sfi{FInt::LuauTypeInferRecursionLimit, 0};

View file

@ -22,8 +22,8 @@ LUAU_FASTFLAG(LuauInstantiateInSubtyping)
LUAU_FASTFLAG(LuauSolverV2) LUAU_FASTFLAG(LuauSolverV2)
LUAU_FASTINT(LuauTarjanChildLimit) LUAU_FASTINT(LuauTarjanChildLimit)
LUAU_FASTFLAG(DebugLuauEqSatSimplification) LUAU_FASTFLAG(DebugLuauEqSatSimplification)
LUAU_FASTFLAG(LuauSubtypingFixTailPack)
LUAU_FASTFLAG(LuauUngeneralizedTypesForRecursiveFunctions) LUAU_FASTFLAG(LuauUngeneralizedTypesForRecursiveFunctions)
LUAU_FASTFLAG(LuauImproveTypePathsInErrors)
TEST_SUITE_BEGIN("TypeInferFunctions"); TEST_SUITE_BEGIN("TypeInferFunctions");
@ -1306,7 +1306,16 @@ f(function(a, b, c, ...) return a + b end)
LUAU_REQUIRE_ERRORS(result); LUAU_REQUIRE_ERRORS(result);
std::string expected; 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 expected = R"(Type
'<a>(number, number, a) -> number' '<a>(number, number, a) -> number'
@ -1315,6 +1324,15 @@ could not be converted into
caused by: caused by:
Argument count mismatch. Function expects 3 arguments, but only 2 are specified)"; 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 else
{ {
expected = R"(Type expected = R"(Type
@ -1519,7 +1537,14 @@ local b: B = a
)"); )");
LUAU_REQUIRE_ERROR_COUNT(1, result); 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' '(number, number) -> string'
could not be converted into could not be converted into
'(number) -> string' '(number) -> string'
@ -1542,7 +1567,14 @@ local b: B = a
)"); )");
LUAU_REQUIRE_ERROR_COUNT(1, result); 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' '(number, number) -> string'
could not be converted into could not be converted into
'(number, string) -> string' '(number, string) -> string'
@ -1566,7 +1598,13 @@ local b: B = a
)"); )");
LUAU_REQUIRE_ERROR_COUNT(1, result); 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' '(number, number) -> number'
could not be converted into could not be converted into
'(number, number) -> (number, boolean)' '(number, number) -> (number, boolean)'
@ -1589,7 +1627,14 @@ local b: B = a
)"); )");
LUAU_REQUIRE_ERROR_COUNT(1, result); 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' '(number, number) -> string'
could not be converted into could not be converted into
'(number, number) -> number' '(number, number) -> number'
@ -1613,7 +1658,14 @@ local b: B = a
)"); )");
LUAU_REQUIRE_ERROR_COUNT(1, result); 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)' '(number, number) -> (number, string)'
could not be converted into could not be converted into
'(number, number) -> (number, boolean)' '(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)" 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 else
{ {
LUAU_REQUIRE_ERROR_COUNT(2, result); LUAU_REQUIRE_ERROR_COUNT(2, result);
@ -1812,15 +1882,31 @@ function t:b() return 2 end -- not OK
)"); )");
LUAU_REQUIRE_ERROR_COUNT(1, result); 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' '(*error-type*) -> number'
could not be converted into could not be converted into
'() -> number' '() -> number'
caused by: caused by:
Argument count mismatch. Function expects 1 argument, but none are specified)", 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") 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); 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)' '(("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 could not be converted into
'("blue" | "red") -> ("blue" | "red") -> ("blue" | "red") -> false'; none of the intersection parts are compatible)"; '("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); 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"); 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 else
CHECK_EQ("Type 'string' could not be converted into 'number'", toString(result.errors[0])); 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); 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"); 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 else
CHECK_EQ("Type 'string' could not be converted into 'number'", toString(result.errors[0])); 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") TEST_CASE_FIXTURE(BuiltinsFixture, "coroutine_wrap_result_call")
{ {
ScopedFastFlag luauSubtypingFixTailPack{FFlag::LuauSubtypingFixTailPack, true};
CheckResult result = check(R"( CheckResult result = check(R"(
function foo(a, b) function foo(a, b)
coroutine.wrap(a)(b) coroutine.wrap(a)(b)

View file

@ -10,9 +10,10 @@
#include "ScopedFlags.h" #include "ScopedFlags.h"
#include "doctest.h" #include "doctest.h"
LUAU_FASTFLAG(LuauInstantiateInSubtyping); LUAU_FASTFLAG(LuauInstantiateInSubtyping)
LUAU_FASTFLAG(LuauSolverV2); LUAU_FASTFLAG(LuauSolverV2)
LUAU_FASTFLAG(LuauDeferBidirectionalInferenceForTableAssignment) LUAU_FASTFLAG(LuauDeferBidirectionalInferenceForTableAssignment)
LUAU_FASTFLAG(LuauImproveTypePathsInErrors)
using namespace Luau; using namespace Luau;
@ -875,7 +876,13 @@ y.a.c = y
CHECK(mismatch); CHECK(mismatch);
CHECK_EQ(toString(mismatch->givenType), "{ a: { c: T<string>?, d: number }, b: number }"); CHECK_EQ(toString(mismatch->givenType), "{ a: { c: T<string>?, d: number }, b: number }");
CHECK_EQ(toString(mismatch->wantedType), "T<string>"); 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); CHECK_EQ(mismatch->reason, reason);
} }
else else

View file

@ -9,7 +9,8 @@
using namespace Luau; using namespace Luau;
LUAU_FASTFLAG(LuauSolverV2); LUAU_FASTFLAG(LuauSolverV2)
LUAU_FASTFLAG(LuauImproveTypePathsInErrors)
TEST_SUITE_BEGIN("IntersectionTypes"); TEST_SUITE_BEGIN("IntersectionTypes");
@ -357,12 +358,21 @@ TEST_CASE_FIXTURE(Fixture, "table_intersection_write_sealed_indirect")
else else
{ {
LUAU_REQUIRE_ERROR_COUNT(4, result); 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' '(string, number) -> string'
could not be converted into could not be converted into
'(string) -> string' '(string) -> string'
caused by: caused by:
Argument count mismatch. Function expects 2 arguments, but only 1 is specified)"; Argument count mismatch. Function expects 2 arguments, but only 1 is specified)";
CHECK_EQ(expected, toString(result.errors[0])); 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[1]), "Cannot add property 'z' to table 'X & Y'");
CHECK_EQ(toString(result.errors[2]), "Type 'number' could not be converted into 'string'"); 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); 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' '(string, number) -> string'
could not be converted into could not be converted into
'(string) -> string' '(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) 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[1] (Y)
type number (number) is not a subtype of X & Y & Z[2] (Z))"; 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])); CHECK_EQ(dcrExprected, toString(result.errors[0]));
else else
CHECK_EQ(expected, toString(result.errors[0])); CHECK_EQ(expected, toString(result.errors[0]));
@ -450,7 +480,23 @@ end
)"); )");
LUAU_REQUIRE_ERROR_COUNT(1, result); 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( 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) 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); 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( CHECK_EQ(
R"(Type 'boolean & false' could not be converted into 'true'; type boolean & false[0] (boolean) is not a subtype of true (true) 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); LUAU_REQUIRE_ERROR_COUNT(1, result);
// TODO: odd stringification of `false & (boolean & false)`.) // 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( 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) 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) 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 local z : (number) -> number = x -- Not OK
end 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); LUAU_REQUIRE_ERROR_COUNT(2, result);
const std::string expected1 = R"(Type const std::string expected1 = R"(Type
@ -568,6 +671,15 @@ could not be converted into
CHECK_EQ(expected1, toString(result.errors[0])); CHECK_EQ(expected1, toString(result.errors[0]));
CHECK_EQ(expected2, toString(result.errors[1])); 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 else
{ {
LUAU_REQUIRE_ERROR_COUNT(1, result); LUAU_REQUIRE_ERROR_COUNT(1, result);
@ -592,11 +704,23 @@ TEST_CASE_FIXTURE(Fixture, "union_saturate_overloaded_functions")
)"); )");
LUAU_REQUIRE_ERROR_COUNT(1, result); 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)' '((number) -> number) & ((string) -> string)'
could not be converted into could not be converted into
'(boolean | number) -> boolean | number'; none of the intersection parts are compatible)"; '(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") TEST_CASE_FIXTURE(Fixture, "intersection_of_tables")
@ -609,16 +733,42 @@ TEST_CASE_FIXTURE(Fixture, "intersection_of_tables")
)"); )");
LUAU_REQUIRE_ERROR_COUNT(1, result); LUAU_REQUIRE_ERROR_COUNT(1, result);
const std::string expected =
(FFlag::LuauSolverV2) if (FFlag::LuauSolverV2 && FFlag::LuauImproveTypePathsInErrors)
? 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))" 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 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? |}' '{| p: number?, q: number?, r: number? |} & {| p: number?, q: string? |}'
could not be converted into could not be converted into
'{| p: nil |}'; none of the intersection parts are compatible)"; '{| 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") 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 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); LUAU_REQUIRE_ERROR_COUNT(1, result);
CHECK_EQ( CHECK_EQ(
@ -646,6 +817,15 @@ could not be converted into
toString(result.errors[0]) 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 else
{ {
LUAU_REQUIRE_ERROR_COUNT(1, result); LUAU_REQUIRE_ERROR_COUNT(1, result);
@ -678,7 +858,52 @@ TEST_CASE_FIXTURE(Fixture, "overloaded_functions_returning_intersections")
end 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); LUAU_REQUIRE_ERROR_COUNT(2, result);
CHECK_EQ( CHECK_EQ(
@ -703,6 +928,17 @@ could not be converted into
toString(result.errors[1]) 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 else
{ {
LUAU_REQUIRE_ERROR_COUNT(1, result); LUAU_REQUIRE_ERROR_COUNT(1, result);
@ -730,6 +966,15 @@ TEST_CASE_FIXTURE(Fixture, "overloaded_functions_mentioning_generic")
{ {
LUAU_REQUIRE_ERROR_COUNT(0, result); 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 else
{ {
LUAU_REQUIRE_ERROR_COUNT(1, result); LUAU_REQUIRE_ERROR_COUNT(1, result);
@ -757,6 +1002,15 @@ TEST_CASE_FIXTURE(Fixture, "overloaded_functions_mentioning_generics")
{ {
LUAU_REQUIRE_NO_ERRORS(result); 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 else
{ {
LUAU_REQUIRE_ERROR_COUNT(1, result); LUAU_REQUIRE_ERROR_COUNT(1, result);
@ -778,7 +1032,35 @@ TEST_CASE_FIXTURE(Fixture, "overloaded_functions_mentioning_generic_packs")
end end
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); LUAU_REQUIRE_ERROR_COUNT(2, result);
CHECK_EQ( CHECK_EQ(
@ -798,6 +1080,15 @@ could not be converted into
toString(result.errors[1]) 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 else
{ {
LUAU_REQUIRE_ERROR_COUNT(1, result); 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); 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)' '((nil) -> unknown) & ((number) -> number)'
could not be converted into could not be converted into
'(number?) -> number?'; none of the intersection parts are compatible)"; '(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") 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); 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?)' '((number) -> number?) & ((unknown) -> string?)'
could not be converted into could not be converted into
'(number?) -> nil'; none of the intersection parts are compatible)"; '(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") TEST_CASE_FIXTURE(Fixture, "overloadeded_functions_with_never_result")
@ -864,7 +1179,36 @@ TEST_CASE_FIXTURE(Fixture, "overloadeded_functions_with_never_result")
end 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); LUAU_REQUIRE_ERROR_COUNT(2, result);
CHECK_EQ( CHECK_EQ(
@ -885,6 +1229,15 @@ could not be converted into
toString(result.errors[1]) 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 else
{ {
LUAU_REQUIRE_ERROR_COUNT(1, result); LUAU_REQUIRE_ERROR_COUNT(1, result);
@ -907,7 +1260,40 @@ TEST_CASE_FIXTURE(Fixture, "overloadeded_functions_with_never_arguments")
end 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); LUAU_REQUIRE_ERROR_COUNT(2, result);
const std::string expected1 = R"(Type const std::string expected1 = R"(Type
@ -926,6 +1312,15 @@ could not be converted into
CHECK_EQ(expected1, toString(result.errors[0])); CHECK_EQ(expected1, toString(result.errors[0]));
CHECK_EQ(expected2, toString(result.errors[1])); 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 else
{ {
LUAU_REQUIRE_ERROR_COUNT(1, result); 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); 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)' '((number?) -> (...number)) & ((string?) -> number | string)'
could not be converted into could not be converted into
'(number | string) -> (number, number?)'; none of the intersection parts are compatible)"; '(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") 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); 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 else
{ {
LUAU_REQUIRE_ERROR_COUNT(1, result); 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); 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( CHECK_EQ(
R"(Type R"(Type
@ -1056,6 +1486,16 @@ could not be converted into
toString(result.errors[0]) 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 else
{ {
CHECK_EQ( CHECK_EQ(

View file

@ -12,6 +12,7 @@
LUAU_FASTFLAG(LuauInstantiateInSubtyping) LUAU_FASTFLAG(LuauInstantiateInSubtyping)
LUAU_FASTFLAG(LuauSolverV2) LUAU_FASTFLAG(LuauSolverV2)
LUAU_FASTFLAG(LuauImproveTypePathsInErrors)
using namespace Luau; using namespace Luau;
@ -461,7 +462,14 @@ local b: B.T = a
CheckResult result = frontend.check("game/C"); CheckResult result = frontend.check("game/C");
LUAU_REQUIRE_ERROR_COUNT(1, result); 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( CHECK(
toString(result.errors.at(0)) == toString(result.errors.at(0)) ==
@ -507,7 +515,14 @@ local b: B.T = a
CheckResult result = frontend.check("game/D"); CheckResult result = frontend.check("game/D");
LUAU_REQUIRE_ERROR_COUNT(1, result); 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( CHECK(
toString(result.errors.at(0)) == toString(result.errors.at(0)) ==

View file

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

View file

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

View file

@ -11,14 +11,15 @@
using namespace Luau; using namespace Luau;
LUAU_FASTFLAG(LuauSolverV2); LUAU_FASTFLAG(LuauSolverV2)
LUAU_FASTFLAG(DebugLuauEqSatSimplification); LUAU_FASTFLAG(DebugLuauEqSatSimplification)
LUAU_FASTFLAG(LuauStoreCSTData); LUAU_FASTFLAG(LuauStoreCSTData)
LUAU_FASTINT(LuauNormalizeCacheLimit); LUAU_FASTINT(LuauNormalizeCacheLimit)
LUAU_FASTINT(LuauTarjanChildLimit); LUAU_FASTINT(LuauTarjanChildLimit)
LUAU_FASTINT(LuauTypeInferIterationLimit); LUAU_FASTINT(LuauTypeInferIterationLimit)
LUAU_FASTINT(LuauTypeInferRecursionLimit); LUAU_FASTINT(LuauTypeInferRecursionLimit)
LUAU_FASTINT(LuauTypeInferTypePackLoopLimit); LUAU_FASTINT(LuauTypeInferTypePackLoopLimit)
LUAU_FASTFLAG(LuauImproveTypePathsInErrors)
TEST_SUITE_BEGIN("ProvisionalTests"); TEST_SUITE_BEGIN("ProvisionalTests");
@ -873,7 +874,15 @@ TEST_CASE_FIXTURE(Fixture, "assign_table_with_refined_property_with_a_similar_ty
else else
{ {
LUAU_REQUIRE_ERROR_COUNT(1, result); 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? |}' '{| x: number? |}'
could not be converted into could not be converted into
'{| x: number |}' '{| x: number |}'

View file

@ -6,7 +6,8 @@
using namespace Luau; using namespace Luau;
LUAU_FASTFLAG(LuauSolverV2); LUAU_FASTFLAG(LuauSolverV2)
LUAU_FASTFLAG(LuauImproveTypePathsInErrors)
TEST_SUITE_BEGIN("TypeSingletons"); TEST_SUITE_BEGIN("TypeSingletons");
@ -364,7 +365,16 @@ TEST_CASE_FIXTURE(Fixture, "table_properties_type_error_escapes")
)"); )");
LUAU_REQUIRE_ERROR_COUNT(1, result); LUAU_REQUIRE_ERROR_COUNT(1, result);
if (FFlag::LuauSolverV2)
if (FFlag::LuauSolverV2 && FFlag::LuauImproveTypePathsInErrors)
{
const std::string expected = "Type\n\t"
"'{ [\"\\n\"]: number }'"
"\ncould not be converted into\n\t"
"'{ [\"<>\"]: number }'";
CHECK(expected == toString(result.errors[0]));
}
else if (FFlag::LuauSolverV2)
CHECK( CHECK(
"Type\n" "Type\n"
" '{ [\"\\n\"]: number }'\n" " '{ [\"\\n\"]: number }'\n"
@ -440,12 +450,23 @@ TEST_CASE_FIXTURE(Fixture, "parametric_tagged_union_alias")
LUAU_REQUIRE_ERROR_COUNT(1, result); LUAU_REQUIRE_ERROR_COUNT(1, result);
const std::string expectedError = R"(Type if (FFlag::LuauImproveTypePathsInErrors)
{
const std::string expectedError = "Type\n\t"
"'{ result: string, success: boolean }'"
"\ncould not be converted into\n\t"
"'Err<number> | Ok<string>'";
CHECK(toString(result.errors[0]) == expectedError);
}
else
{
const std::string expectedError = R"(Type
'{ result: string, success: boolean }' '{ result: string, success: boolean }'
could not be converted into could not be converted into
'Err<number> | Ok<string>')"; 'Err<number> | Ok<string>')";
CHECK(toString(result.errors[0]) == expectedError); CHECK(toString(result.errors[0]) == expectedError);
}
} }
TEST_CASE_FIXTURE(Fixture, "if_then_else_expression_singleton_options") TEST_CASE_FIXTURE(Fixture, "if_then_else_expression_singleton_options")

View file

@ -4,6 +4,7 @@
#include "Luau/Error.h" #include "Luau/Error.h"
#include "Luau/Frontend.h" #include "Luau/Frontend.h"
#include "Luau/ToString.h" #include "Luau/ToString.h"
#include "Luau/TypeChecker2.h"
#include "Luau/TypeInfer.h" #include "Luau/TypeInfer.h"
#include "Luau/Type.h" #include "Luau/Type.h"
@ -17,19 +18,18 @@
using namespace Luau; using namespace Luau;
LUAU_FASTFLAG(LuauSolverV2) LUAU_FASTFLAG(LuauSolverV2)
LUAU_FASTFLAG(LuauInstantiateInSubtyping) LUAU_FASTFLAG(LuauInstantiateInSubtyping)
LUAU_FASTFLAG(LuauFixIndexerSubtypingOrdering) LUAU_FASTFLAG(LuauFixIndexerSubtypingOrdering)
LUAU_FASTFLAG(LuauTrackInteriorFreeTypesOnScope) LUAU_FASTFLAG(LuauTrackInteriorFreeTypesOnScope)
LUAU_FASTFLAG(LuauTrackInteriorFreeTablesOnScope) LUAU_FASTFLAG(LuauTrackInteriorFreeTablesOnScope)
LUAU_FASTFLAG(LuauDontInPlaceMutateTableType)
LUAU_FASTFLAG(LuauAllowNonSharedTableTypesInLiteral)
LUAU_FASTFLAG(LuauFollowTableFreeze) LUAU_FASTFLAG(LuauFollowTableFreeze)
LUAU_FASTFLAG(LuauPrecalculateMutatedFreeTypes2) LUAU_FASTFLAG(LuauPrecalculateMutatedFreeTypes2)
LUAU_FASTFLAG(LuauDeferBidirectionalInferenceForTableAssignment) LUAU_FASTFLAG(LuauDeferBidirectionalInferenceForTableAssignment)
LUAU_FASTFLAG(LuauBidirectionalInferenceUpcast) LUAU_FASTFLAG(LuauBidirectionalInferenceUpcast)
LUAU_FASTFLAG(DebugLuauAssertOnForcedConstraint) LUAU_FASTFLAG(DebugLuauAssertOnForcedConstraint)
LUAU_FASTFLAG(LuauSearchForRefineableType) LUAU_FASTFLAG(LuauSearchForRefineableType)
LUAU_FASTFLAG(LuauDoNotGeneralizeInTypeFunctions) LUAU_FASTFLAG(LuauImproveTypePathsInErrors)
TEST_SUITE_BEGIN("TableTests"); TEST_SUITE_BEGIN("TableTests");
@ -922,7 +922,16 @@ TEST_CASE_FIXTURE(Fixture, "sealed_table_indexers_must_unify")
LUAU_REQUIRE_ERROR_COUNT(1, result); LUAU_REQUIRE_ERROR_COUNT(1, result);
if (FFlag::LuauSolverV2) if (FFlag::LuauSolverV2 && FFlag::LuauImproveTypePathsInErrors)
{
CHECK(
"Type pack '{number}' could not be converted into '{string}'; \n"
"this is because in the 1st entry in the type pack, the result of indexing is `number` in the former type and `string` in the latter "
"type, "
"and `number` is not exactly `string`" == toString(result.errors[0])
);
}
else if (FFlag::LuauSolverV2)
{ {
// CLI-114879 - Error path reporting is not great // CLI-114879 - Error path reporting is not great
CHECK( CHECK(
@ -1805,7 +1814,16 @@ TEST_CASE_FIXTURE(Fixture, "table_subtyping_with_missing_props_dont_report_multi
LUAU_REQUIRE_ERROR_COUNT(1, result); LUAU_REQUIRE_ERROR_COUNT(1, result);
if (FFlag::LuauSolverV2) if (FFlag::LuauSolverV2 && FFlag::LuauImproveTypePathsInErrors)
{
CHECK_EQ(
"Type pack '{ x: number }' could not be converted into '{ x: number, y: number, z: number }'; \n"
"this is because the 1st entry in the type pack is `{ x: number }` in the former type and `{ x: number, y: number, z: number }` in the "
"latter type, and `{ x: number }` is not a subtype of `{ x: number, y: number, z: number }`",
toString(result.errors[0])
);
}
else if (FFlag::LuauSolverV2)
{ {
CHECK_EQ( CHECK_EQ(
"Type pack '{ x: number }' could not be converted into '{ x: number, y: number, z: number }';" "Type pack '{ x: number }' could not be converted into '{ x: number, y: number, z: number }';"
@ -2435,7 +2453,15 @@ local b: B = a
LUAU_REQUIRE_ERRORS(result); LUAU_REQUIRE_ERRORS(result);
if (FFlag::LuauSolverV2) if (FFlag::LuauSolverV2 && FFlag::LuauImproveTypePathsInErrors)
{
CHECK(
"Type 'A' could not be converted into 'B'; \n"
"this is because accessing `y` results in `number` in the former type and `string` in the latter type, and `number` is not exactly "
"`string`" == toString(result.errors.at(0))
);
}
else if (FFlag::LuauSolverV2)
CHECK(toString(result.errors.at(0)) == R"(Type 'A' could not be converted into 'B'; at [read "y"], number is not exactly string)"); CHECK(toString(result.errors.at(0)) == R"(Type 'A' could not be converted into 'B'; at [read "y"], number is not exactly string)");
else else
{ {
@ -2462,7 +2488,15 @@ local b: B = a
LUAU_REQUIRE_ERRORS(result); LUAU_REQUIRE_ERRORS(result);
if (FFlag::LuauSolverV2) if (FFlag::LuauSolverV2 && FFlag::LuauImproveTypePathsInErrors)
{
CHECK(
"Type 'A' could not be converted into 'B'; \n"
"this is because accessing `b.y` results in `number` in the former type and `string` in the latter type, and `number` is not exactly "
"`string`" == toString(result.errors.at(0))
);
}
else if (FFlag::LuauSolverV2)
CHECK(toString(result.errors.at(0)) == R"(Type 'A' could not be converted into 'B'; at [read "b"][read "y"], number is not exactly string)"); CHECK(toString(result.errors.at(0)) == R"(Type 'A' could not be converted into 'B'; at [read "b"][read "y"], number is not exactly string)");
else else
{ {
@ -2489,7 +2523,17 @@ local b2 = setmetatable({ x = 2, y = 4 }, { __call = function(s, t) end });
local c2: typeof(a2) = b2 local c2: typeof(a2) = b2
)"); )");
const std::string expected1 = R"(Type 'b1' could not be converted into 'a1' const std::string expected1 = (FFlag::LuauImproveTypePathsInErrors) ?
R"(Type 'b1' could not be converted into 'a1'
caused by:
Type
'{ x: number, y: string }'
could not be converted into
'{ x: number, y: number }'
caused by:
Property 'y' is not compatible.
Type 'string' could not be converted into 'number' in an invariant context)"
: R"(Type 'b1' could not be converted into 'a1'
caused by: caused by:
Type Type
'{ x: number, y: string }' '{ x: number, y: string }'
@ -2498,7 +2542,20 @@ could not be converted into
caused by: caused by:
Property 'y' is not compatible. Property 'y' is not compatible.
Type 'string' could not be converted into 'number' in an invariant context)"; Type 'string' could not be converted into 'number' in an invariant context)";
const std::string expected2 = R"(Type 'b2' could not be converted into 'a2' const std::string expected2 = (FFlag::LuauImproveTypePathsInErrors) ?
R"(Type 'b2' could not be converted into 'a2'
caused by:
Type
'{ __call: <a, b>(a, b) -> () }'
could not be converted into
'{ __call: <a>(a) -> () }'
caused by:
Property '__call' is not compatible.
Type
'<a, b>(a, b) -> ()'
could not be converted into
'<a>(a) -> ()'; different number of generic type parameters)"
: R"(Type 'b2' could not be converted into 'a2'
caused by: caused by:
Type Type
'{ __call: <a, b>(a, b) -> () }' '{ __call: <a, b>(a, b) -> () }'
@ -2511,7 +2568,23 @@ Type
could not be converted into could not be converted into
'<a>(a) -> ()'; different number of generic type parameters)"; '<a>(a) -> ()'; different number of generic type parameters)";
if (FFlag::LuauSolverV2) if (FFlag::LuauSolverV2 && FFlag::LuauImproveTypePathsInErrors)
{
// The assignment of c2 to b2 is, surprisingly, allowed under the new
// solver for two reasons:
//
// First, both of the __call functions have hidden ...any arguments
// because their exact definition is available.
//
// Second, nil <: unknown, so we consider that parameter to be optional.
LUAU_REQUIRE_ERROR_COUNT(1, result);
CHECK(
"Type 'b1' could not be converted into 'a1'; \n"
"this is because in the table portion, accessing `y` results in `string` in the former type and `number` in the latter type, and "
"`string` is not exactly `number`" == toString(result.errors[0])
);
}
else if (FFlag::LuauSolverV2)
{ {
// The assignment of c2 to b2 is, surprisingly, allowed under the new // The assignment of c2 to b2 is, surprisingly, allowed under the new
// solver for two reasons: // solver for two reasons:
@ -2576,7 +2649,15 @@ TEST_CASE_FIXTURE(Fixture, "error_detailed_indexer_key")
LUAU_REQUIRE_ERRORS(result); LUAU_REQUIRE_ERRORS(result);
if (FFlag::LuauSolverV2) if (FFlag::LuauSolverV2 && FFlag::LuauImproveTypePathsInErrors)
{
CHECK(
"Type 'A' could not be converted into 'B'; \n"
"this is because the index type is `number` in the former type and `string` in the latter type, and `number` is not exactly `string`" ==
toString(result.errors[0])
);
}
else if (FFlag::LuauSolverV2)
{ {
CHECK("Type 'A' could not be converted into 'B'; at indexer(), number is not exactly string" == toString(result.errors[0])); CHECK("Type 'A' could not be converted into 'B'; at indexer(), number is not exactly string" == toString(result.errors[0]));
} }
@ -2602,7 +2683,15 @@ TEST_CASE_FIXTURE(Fixture, "error_detailed_indexer_value")
LUAU_REQUIRE_ERRORS(result); LUAU_REQUIRE_ERRORS(result);
if (FFlag::LuauSolverV2) if (FFlag::LuauSolverV2 && FFlag::LuauImproveTypePathsInErrors)
{
CHECK(
"Type 'A' could not be converted into 'B'; \n"
"this is because the result of indexing is `number` in the former type and `string` in the latter type, and `number` is not exactly "
"`string`" == toString(result.errors[0])
);
}
else if (FFlag::LuauSolverV2)
{ {
CHECK("Type 'A' could not be converted into 'B'; at indexResult(), number is not exactly string" == toString(result.errors[0])); CHECK("Type 'A' could not be converted into 'B'; at indexResult(), number is not exactly string" == toString(result.errors[0]));
} }
@ -2651,7 +2740,13 @@ local y: number = tmp.p.y
LUAU_REQUIRE_ERROR_COUNT(1, result); LUAU_REQUIRE_ERROR_COUNT(1, result);
if (FFlag::LuauSolverV2) if (FFlag::LuauSolverV2 && FFlag::LuauImproveTypePathsInErrors)
CHECK(
"Type 'tmp' could not be converted into 'HasSuper'; \n"
"this is because accessing `p` results in `{ x: number, y: number }` in the former type and `Super` in the latter type, and `{ x: "
"number, y: number }` is not exactly `Super`" == toString(result.errors[0])
);
else if (FFlag::LuauSolverV2)
CHECK( CHECK(
"Type 'tmp' could not be converted into 'HasSuper'; at [read \"p\"], { x: number, y: number } is not exactly Super" == "Type 'tmp' could not be converted into 'HasSuper'; at [read \"p\"], { x: number, y: number } is not exactly Super" ==
toString(result.errors[0]) toString(result.errors[0])
@ -3615,7 +3710,14 @@ TEST_CASE_FIXTURE(Fixture, "mixed_tables_with_implicit_numbered_keys")
local t: { [string]: number } = { 5, 6, 7 } local t: { [string]: number } = { 5, 6, 7 }
)"); )");
if (FFlag::LuauSolverV2) if (FFlag::LuauSolverV2 && FFlag::LuauImproveTypePathsInErrors)
{
std::string expected =
"Type '{number}' could not be converted into '{ [string]: number }'; \n"
"this is because the index type is `number` in the former type and `string` in the latter type, and `number` is not exactly `string`";
CHECK(toString(result.errors[0]) == expected);
}
else if (FFlag::LuauSolverV2)
{ {
LUAU_REQUIRE_ERROR_COUNT(1, result); LUAU_REQUIRE_ERROR_COUNT(1, result);
CHECK( CHECK(
@ -3757,6 +3859,36 @@ TEST_CASE_FIXTURE(Fixture, "scalar_is_not_a_subtype_of_a_compatible_polymorphic_
CHECK("typeof(string)" == toString(tm4->givenType)); CHECK("typeof(string)" == toString(tm4->givenType));
CHECK("t1 where t1 = { read absolutely_no_scalar_has_this_method: (t1) -> (a...) }" == toString(tm4->wantedType)); CHECK("t1 where t1 = { read absolutely_no_scalar_has_this_method: (t1) -> (a...) }" == toString(tm4->wantedType));
} }
else if (FFlag::LuauImproveTypePathsInErrors)
{
LUAU_REQUIRE_ERROR_COUNT(3, result);
const std::string expected1 =
R"(Type 'string' could not be converted into 't1 where t1 = {- absolutely_no_scalar_has_this_method: (t1) -> (a...) -}'
caused by:
The former's metatable does not satisfy the requirements.
Table type 'typeof(string)' not compatible with type 't1 where t1 = {- absolutely_no_scalar_has_this_method: (t1) -> (a...) -}' because the former is missing field 'absolutely_no_scalar_has_this_method')";
CHECK_EQ(expected1, toString(result.errors[0]));
const std::string expected2 =
R"(Type '"bar"' could not be converted into 't1 where t1 = {- absolutely_no_scalar_has_this_method: (t1) -> (a...) -}'
caused by:
The former's metatable does not satisfy the requirements.
Table type 'typeof(string)' not compatible with type 't1 where t1 = {- absolutely_no_scalar_has_this_method: (t1) -> (a...) -}' because the former is missing field 'absolutely_no_scalar_has_this_method')";
CHECK_EQ(expected2, toString(result.errors[1]));
const std::string expected3 = R"(Type
'"bar" | "baz"'
could not be converted into
't1 where t1 = {- absolutely_no_scalar_has_this_method: (t1) -> (a...) -}'
caused by:
Not all union options are compatible.
Type '"bar"' could not be converted into 't1 where t1 = {- absolutely_no_scalar_has_this_method: (t1) -> (a...) -}'
caused by:
The former's metatable does not satisfy the requirements.
Table type 'typeof(string)' not compatible with type 't1 where t1 = {- absolutely_no_scalar_has_this_method: (t1) -> (a...) -}' because the former is missing field 'absolutely_no_scalar_has_this_method')";
CHECK_EQ(expected3, toString(result.errors[2]));
}
else else
{ {
LUAU_REQUIRE_ERROR_COUNT(3, result); LUAU_REQUIRE_ERROR_COUNT(3, result);
@ -4338,11 +4470,25 @@ TEST_CASE_FIXTURE(Fixture, "identify_all_problematic_table_fields")
LUAU_REQUIRE_ERROR_COUNT(1, result); LUAU_REQUIRE_ERROR_COUNT(1, result);
std::string expected =
"Type '{ a: string, b: boolean, c: number }' could not be converted into 'T'; at [read \"a\"], string is not exactly number" if (FFlag::LuauImproveTypePathsInErrors)
"\n\tat [read \"b\"], boolean is not exactly string" {
"\n\tat [read \"c\"], number is not exactly boolean"; std::string expected =
CHECK(toString(result.errors[0]) == expected); "Type '{ a: string, b: boolean, c: number }' could not be converted into 'T'; \n"
"this is because \n\t"
" * accessing `a` results in `string` in the former type and `number` in the latter type, and `string` is not exactly `number`\n\t"
" * accessing `b` results in `boolean` in the former type and `string` in the latter type, and `boolean` is not exactly `string`\n\t"
" * accessing `c` results in `number` in the former type and `boolean` in the latter type, and `number` is not exactly `boolean`";
CHECK(toString(result.errors[0]) == expected);
}
else
{
std::string expected =
"Type '{ a: string, b: boolean, c: number }' could not be converted into 'T'; at [read \"a\"], string is not exactly number"
"\n\tat [read \"b\"], boolean is not exactly string"
"\n\tat [read \"c\"], number is not exactly boolean";
CHECK(toString(result.errors[0]) == expected);
}
} }
TEST_CASE_FIXTURE(Fixture, "read_and_write_only_table_properties_are_unsupported") TEST_CASE_FIXTURE(Fixture, "read_and_write_only_table_properties_are_unsupported")
@ -4968,12 +5114,30 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "subtyping_with_a_metatable_table_path")
)"); )");
LUAU_REQUIRE_ERROR_COUNT(1, result); LUAU_REQUIRE_ERROR_COUNT(1, result);
CHECK_EQ(
"Type pack '{ @metatable { }, { } & { } }' could not be converted into 'Class'; at [0].metatable(), { } is not a subtype of nil\n" if (FFlag::LuauImproveTypePathsInErrors)
"\ttype { @metatable { }, { } & { } }[0].table()[0] ({ }) is not a subtype of Class[0].table() (nil)\n" {
"\ttype { @metatable { }, { } & { } }[0].table()[1] ({ }) is not a subtype of Class[0].table() (nil)", CHECK_EQ(
toString(result.errors[0]) "Type pack '{ @metatable { }, { } & { } }' could not be converted into 'Class'; \n"
); "this is because \n\t"
" * in the 1st entry in the type pack, the metatable portion is `{ }` in the former type and `nil` in the latter type, and `{ }` "
"is not a subtype of `nil`\n\t"
" * in the 1st entry in the type pack, the table portion has the 1st component of the intersection as `{ }` and in the 1st entry "
"in the type pack, the table portion is `nil`, and `{ }` is not a subtype of `nil`\n\t"
" * in the 1st entry in the type pack, the table portion has the 2nd component of the intersection as `{ }` and in the 1st entry "
"in the type pack, the table portion is `nil`, and `{ }` is not a subtype of `nil`",
toString(result.errors[0])
);
}
else
{
CHECK_EQ(
"Type pack '{ @metatable { }, { } & { } }' could not be converted into 'Class'; at [0].metatable(), { } is not a subtype of nil\n"
"\ttype { @metatable { }, { } & { } }[0].table()[0] ({ }) is not a subtype of Class[0].table() (nil)\n"
"\ttype { @metatable { }, { } & { } }[0].table()[1] ({ }) is not a subtype of Class[0].table() (nil)",
toString(result.errors[0])
);
}
} }
TEST_CASE_FIXTURE(BuiltinsFixture, "metatable_union_type") TEST_CASE_FIXTURE(BuiltinsFixture, "metatable_union_type")
@ -5071,7 +5235,6 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "multiple_fields_in_literal")
{ {
ScopedFastFlag sffs[] = { ScopedFastFlag sffs[] = {
{FFlag::LuauSolverV2, true}, {FFlag::LuauSolverV2, true},
{FFlag::LuauDontInPlaceMutateTableType, true},
}; };
auto result = check(R"( auto result = check(R"(
@ -5098,11 +5261,7 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "multiple_fields_in_literal")
TEST_CASE_FIXTURE(BuiltinsFixture, "multiple_fields_from_fuzzer") TEST_CASE_FIXTURE(BuiltinsFixture, "multiple_fields_from_fuzzer")
{ {
ScopedFastFlag sffs[] = { ScopedFastFlag _{FFlag::LuauSolverV2, true};
{FFlag::LuauSolverV2, true},
{FFlag::LuauDontInPlaceMutateTableType, true},
{FFlag::LuauAllowNonSharedTableTypesInLiteral, true},
};
// This would trigger an assert previously, so we really only care that // This would trigger an assert previously, so we really only care that
// there are errors (and there will be: lots of syntax errors). // there are errors (and there will be: lots of syntax errors).
@ -5113,11 +5272,7 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "multiple_fields_from_fuzzer")
TEST_CASE_FIXTURE(BuiltinsFixture, "write_only_table_field_duplicate") TEST_CASE_FIXTURE(BuiltinsFixture, "write_only_table_field_duplicate")
{ {
ScopedFastFlag sffs[] = { ScopedFastFlag _{FFlag::LuauSolverV2, true};
{FFlag::LuauSolverV2, true},
{FFlag::LuauDontInPlaceMutateTableType, true},
{FFlag::LuauAllowNonSharedTableTypesInLiteral, true},
};
auto result = check(R"( auto result = check(R"(
type WriteOnlyTable = { write x: number } type WriteOnlyTable = { write x: number }
@ -5345,7 +5500,6 @@ TEST_CASE_FIXTURE(Fixture, "deeply_nested_classish_inference")
{ {
ScopedFastFlag sffs[] = { ScopedFastFlag sffs[] = {
{FFlag::LuauSolverV2, true}, {FFlag::LuauSolverV2, true},
{FFlag::LuauDoNotGeneralizeInTypeFunctions, true},
{FFlag::LuauSearchForRefineableType, true}, {FFlag::LuauSearchForRefineableType, true},
{FFlag::DebugLuauAssertOnForcedConstraint, true}, {FFlag::DebugLuauAssertOnForcedConstraint, true},
}; };
@ -5367,5 +5521,56 @@ TEST_CASE_FIXTURE(Fixture, "deeply_nested_classish_inference")
)")); )"));
} }
TEST_CASE_FIXTURE(Fixture, "bigger_nested_table_causes_big_type_error")
{
ScopedFastFlag sffs[] = {
{FFlag::LuauSolverV2, true},
{FFlag::LuauImproveTypePathsInErrors, true},
};
auto result = check(R"(
type File = {
type: "file",
name: string,
content: string?,
}
type Dir = {
type: "dir",
name: string,
children: { File | Dir }?,
}
type DirectoryChildren = { File | Dir }
local newtree: DirectoryChildren = {
{
type = "dir",
name = "src",
children = {
{
type = "file",
path = "main.luau", -- I accidentally assign "path" instead of "name", causing a huge scary TypeError
}
}
}
}
)");
LUAU_REQUIRE_ERROR_COUNT(1, result);
std::string expected = "Type\n\t"
"'{Dir | File | { children: ({Dir | File | { content: string?, path: string, type: \"file\" }} | {Dir | File})?, name: "
"string, type: \"dir\" }}'"
"\ncould not be converted into\n\t"
"'DirectoryChildren'; \n"
"this is because in the result of indexing has the 3rd component of the union as `{ children: ({Dir | File | { content: "
"string?, path: string, type: \"file\" }} | {Dir | File})?, name: string, type: \"dir\" }` and the result of indexing is "
"`Dir | File`, and `{ children: ({Dir | File | { content: string?, path: string, type: \"file\" }} | {Dir | File})?, "
"name: string, type: \"dir\" }` is not exactly `Dir | File`";
CHECK_EQ(expected, toString(result.errors[0]));
}
TEST_SUITE_END(); TEST_SUITE_END();

View file

@ -29,6 +29,7 @@ LUAU_FASTFLAG(LuauNewNonStrictWarnOnUnknownGlobals)
LUAU_FASTFLAG(LuauInferLocalTypesInMultipleAssignments) LUAU_FASTFLAG(LuauInferLocalTypesInMultipleAssignments)
LUAU_FASTFLAG(LuauUnifyMetatableWithAny) LUAU_FASTFLAG(LuauUnifyMetatableWithAny)
LUAU_FASTFLAG(LuauExtraFollows) LUAU_FASTFLAG(LuauExtraFollows)
LUAU_FASTFLAG(LuauImproveTypePathsInErrors)
using namespace Luau; using namespace Luau;
@ -1134,7 +1135,29 @@ TEST_CASE_FIXTURE(Fixture, "cli_50041_committing_txnlog_in_apollo_client_error")
end end
)"); )");
if (FFlag::LuauInstantiateInSubtyping) if (FFlag::LuauInstantiateInSubtyping && FFlag::LuauImproveTypePathsInErrors)
{
LUAU_REQUIRE_ERROR_COUNT(1, result);
const std::string expected =
"Type 'Policies' from 'MainModule' could not be converted into 'Policies' from 'MainModule'"
"\ncaused by:\n"
" Property 'getStoreFieldName' is not compatible.\n"
"Type\n\t"
"'(Policies, FieldSpecifier & {| from: number? |}) -> (a, b...)'"
"\ncould not be converted into\n\t"
"'(Policies, FieldSpecifier) -> string'"
"\ncaused by:\n"
" Argument #2 type is not compatible.\n"
"Type\n\t"
"'FieldSpecifier'"
"\ncould not be converted into\n\t"
"'FieldSpecifier & {| from: number? |}'"
"\ncaused by:\n"
" Not all intersection parts are compatible.\n"
"Table type 'FieldSpecifier' not compatible with type '{| from: number? |}' because the former has extra field 'fieldName'";
CHECK_EQ(expected, toString(result.errors[0]));
}
else if (FFlag::LuauInstantiateInSubtyping)
{ {
// though this didn't error before the flag, it seems as though it should error since fields of a table are invariant. // though this didn't error before the flag, it seems as though it should error since fields of a table are invariant.
// the user's intent would likely be that these "method" fields would be read-only, but without an annotation, accepting this should be // the user's intent would likely be that these "method" fields would be read-only, but without an annotation, accepting this should be
@ -1911,4 +1934,17 @@ end
)"); )");
} }
TEST_CASE_FIXTURE(Fixture, "concat_string_with_string_union")
{
ScopedFastFlag _{FFlag::LuauSolverV2, true};
LUAU_REQUIRE_NO_ERRORS(check(R"(
local function foo(n : number): string return "" end
local function bar(n: number, m: string) end
local function concat_stuff(x, y)
local z = foo(x)
bar(y, z)
end
)"));
}
TEST_SUITE_END(); TEST_SUITE_END();

View file

@ -9,8 +9,10 @@
using namespace Luau; using namespace Luau;
LUAU_FASTFLAG(LuauSolverV2); LUAU_FASTFLAG(LuauSolverV2)
LUAU_FASTFLAG(LuauInstantiateInSubtyping);
LUAU_FASTFLAG(LuauInstantiateInSubtyping)
LUAU_FASTFLAG(LuauImproveTypePathsInErrors)
TEST_SUITE_BEGIN("TypePackTests"); TEST_SUITE_BEGIN("TypePackTests");
@ -951,7 +953,19 @@ a = b
LUAU_REQUIRE_ERROR_COUNT(1, result); LUAU_REQUIRE_ERROR_COUNT(1, result);
if (FFlag::LuauSolverV2) if (FFlag::LuauSolverV2 && FFlag::LuauImproveTypePathsInErrors)
{
const std::string expected = "Type\n\t"
"'() -> (number, ...boolean)'"
"\ncould not be converted into\n\t"
"'() -> (number, ...string)'; \n"
"this is because it returns a tail of the variadic `boolean` in the former type and `string` in the latter "
"type, and `boolean` is not a subtype of `string`";
CHECK(expected == toString(result.errors[0]));
}
else if (FFlag::LuauSolverV2)
{ {
const std::string expected = "Type\n" const std::string expected = "Type\n"
" '() -> (number, ...boolean)'\n" " '() -> (number, ...boolean)'\n"
@ -960,6 +974,16 @@ a = b
CHECK(expected == toString(result.errors[0])); CHECK(expected == toString(result.errors[0]));
} }
else if (FFlag::LuauImproveTypePathsInErrors)
{
const std::string expected = R"(Type
'() -> (number, ...boolean)'
could not be converted into
'() -> (number, ...string)'
caused by:
Type 'boolean' could not be converted into 'string')";
CHECK_EQ(expected, toString(result.errors[0]));
}
else else
{ {
const std::string expected = R"(Type const std::string expected = R"(Type
@ -1093,7 +1117,12 @@ TEST_CASE_FIXTURE(Fixture, "unify_variadic_tails_in_arguments_free")
)"); )");
LUAU_REQUIRE_ERROR_COUNT(1, result); LUAU_REQUIRE_ERROR_COUNT(1, result);
if (FFlag::LuauSolverV2) if (FFlag::LuauSolverV2 && FFlag::LuauImproveTypePathsInErrors)
CHECK(
toString(result.errors.at(0)) == "Type pack '...number' could not be converted into 'boolean'; \nthis is because it has a tail of "
"`...number`, which is not a subtype of `boolean`"
);
else if (FFlag::LuauSolverV2)
CHECK( CHECK(
toString(result.errors.at(0)) == toString(result.errors.at(0)) ==
"Type pack '...number' could not be converted into 'boolean'; type ...number.tail() (...number) is not a subtype of boolean (boolean)" "Type pack '...number' could not be converted into 'boolean'; type ...number.tail() (...number) is not a subtype of boolean (boolean)"

View file

@ -10,6 +10,8 @@ using namespace Luau;
LUAU_FASTFLAG(LuauSolverV2) LUAU_FASTFLAG(LuauSolverV2)
LUAU_FASTFLAG(LuauImproveTypePathsInErrors)
TEST_SUITE_BEGIN("UnionTypes"); TEST_SUITE_BEGIN("UnionTypes");
TEST_CASE_FIXTURE(Fixture, "fuzzer_union_with_one_part_assertion") TEST_CASE_FIXTURE(Fixture, "fuzzer_union_with_one_part_assertion")
@ -538,7 +540,19 @@ end
LUAU_REQUIRE_ERROR_COUNT(1, result); LUAU_REQUIRE_ERROR_COUNT(1, result);
if (FFlag::LuauSolverV2) if (FFlag::LuauSolverV2 && FFlag::LuauImproveTypePathsInErrors)
{
CHECK_EQ(
toString(result.errors[0]),
"Type 'X | Y | Z' could not be converted into '{ w: number }'; \n"
"this is because \n\t"
" * the 1st component of the union is `X`, which is not a subtype of `{ w: number }`\n\t"
" * the 2nd component of the union is `Y`, which is not a subtype of `{ w: number }`\n\t"
" * the 3rd component of the union is `Z`, which is not a subtype of `{ w: number }`"
);
}
else if (FFlag::LuauSolverV2)
{ {
CHECK_EQ( CHECK_EQ(
toString(result.errors[0]), toString(result.errors[0]),
@ -667,11 +681,23 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "table_union_write_indirect")
LUAU_REQUIRE_ERROR_COUNT(1, result); LUAU_REQUIRE_ERROR_COUNT(1, result);
// NOTE: union normalization will improve this message // NOTE: union normalization will improve this message
const std::string expected = R"(Type
if (FFlag::LuauImproveTypePathsInErrors)
{
const std::string expected = "Type\n\t"
"'(string) -> number'"
"\ncould not be converted into\n\t"
"'((number) -> string) | ((number) -> string)'; none of the union options are compatible";
CHECK_EQ(expected, toString(result.errors[0]));
}
else
{
const std::string expected = R"(Type
'(string) -> number' '(string) -> number'
could not be converted into could not be converted into
'((number) -> string) | ((number) -> string)'; none of the union options are compatible)"; '((number) -> string) | ((number) -> string)'; none of the union options are compatible)";
CHECK_EQ(expected, toString(result.errors[0])); CHECK_EQ(expected, toString(result.errors[0]));
}
} }
TEST_CASE_FIXTURE(Fixture, "union_true_and_false") TEST_CASE_FIXTURE(Fixture, "union_true_and_false")
@ -757,11 +783,23 @@ TEST_CASE_FIXTURE(Fixture, "union_of_functions_mentioning_generic_typepacks")
)"); )");
LUAU_REQUIRE_ERROR_COUNT(1, result); LUAU_REQUIRE_ERROR_COUNT(1, result);
const std::string expected = R"(Type
if (FFlag::LuauImproveTypePathsInErrors)
{
const std::string expected = "Type\n\t"
"'(number, a...) -> (number?, a...)'"
"\ncould not be converted into\n\t"
"'((number) -> number) | ((number?, a...) -> (number?, a...))'; none of the union options are compatible";
CHECK_EQ(expected, toString(result.errors[0]));
}
else
{
const std::string expected = R"(Type
'(number, a...) -> (number?, a...)' '(number, a...) -> (number?, a...)'
could not be converted into could not be converted into
'((number) -> number) | ((number?, a...) -> (number?, a...))'; none of the union options are compatible)"; '((number) -> number) | ((number?, a...) -> (number?, a...))'; none of the union options are compatible)";
CHECK_EQ(expected, toString(result.errors[0])); CHECK_EQ(expected, toString(result.errors[0]));
}
} }
TEST_CASE_FIXTURE(Fixture, "union_of_functions_with_mismatching_arg_arities") TEST_CASE_FIXTURE(Fixture, "union_of_functions_with_mismatching_arg_arities")
@ -776,11 +814,23 @@ TEST_CASE_FIXTURE(Fixture, "union_of_functions_with_mismatching_arg_arities")
)"); )");
LUAU_REQUIRE_ERROR_COUNT(1, result); LUAU_REQUIRE_ERROR_COUNT(1, result);
const std::string expected = R"(Type
if (FFlag::LuauImproveTypePathsInErrors)
{
const std::string expected = "Type\n\t"
"'(number) -> number?'"
"\ncould not be converted into\n\t"
"'((number) -> nil) | ((number, string?) -> number)'; none of the union options are compatible";
CHECK_EQ(expected, toString(result.errors[0]));
}
else
{
const std::string expected = R"(Type
'(number) -> number?' '(number) -> number?'
could not be converted into could not be converted into
'((number) -> nil) | ((number, string?) -> number)'; none of the union options are compatible)"; '((number) -> nil) | ((number, string?) -> number)'; none of the union options are compatible)";
CHECK_EQ(expected, toString(result.errors[0])); CHECK_EQ(expected, toString(result.errors[0]));
}
} }
TEST_CASE_FIXTURE(Fixture, "union_of_functions_with_mismatching_result_arities") TEST_CASE_FIXTURE(Fixture, "union_of_functions_with_mismatching_result_arities")
@ -795,11 +845,23 @@ TEST_CASE_FIXTURE(Fixture, "union_of_functions_with_mismatching_result_arities")
)"); )");
LUAU_REQUIRE_ERROR_COUNT(1, result); LUAU_REQUIRE_ERROR_COUNT(1, result);
const std::string expected = R"(Type
if (FFlag::LuauImproveTypePathsInErrors)
{
const std::string expected = "Type\n\t"
"'() -> number | string'"
"\ncould not be converted into\n\t"
"'(() -> (string, string)) | (() -> number)'; none of the union options are compatible";
CHECK_EQ(expected, toString(result.errors[0]));
}
else
{
const std::string expected = R"(Type
'() -> number | string' '() -> number | string'
could not be converted into could not be converted into
'(() -> (string, string)) | (() -> number)'; none of the union options are compatible)"; '(() -> (string, string)) | (() -> number)'; none of the union options are compatible)";
CHECK_EQ(expected, toString(result.errors[0])); CHECK_EQ(expected, toString(result.errors[0]));
}
} }
TEST_CASE_FIXTURE(Fixture, "union_of_functions_with_variadics") TEST_CASE_FIXTURE(Fixture, "union_of_functions_with_variadics")
@ -814,11 +876,23 @@ TEST_CASE_FIXTURE(Fixture, "union_of_functions_with_variadics")
)"); )");
LUAU_REQUIRE_ERROR_COUNT(1, result); LUAU_REQUIRE_ERROR_COUNT(1, result);
const std::string expected = R"(Type
if (FFlag::LuauImproveTypePathsInErrors)
{
const std::string expected = "Type\n\t"
"'(...nil) -> (...number?)'"
"\ncould not be converted into\n\t"
"'((...string?) -> (...number)) | ((...string?) -> nil)'; none of the union options are compatible";
CHECK_EQ(expected, toString(result.errors[0]));
}
else
{
const std::string expected = R"(Type
'(...nil) -> (...number?)' '(...nil) -> (...number?)'
could not be converted into could not be converted into
'((...string?) -> (...number)) | ((...string?) -> nil)'; none of the union options are compatible)"; '((...string?) -> (...number)) | ((...string?) -> nil)'; none of the union options are compatible)";
CHECK_EQ(expected, toString(result.errors[0])); CHECK_EQ(expected, toString(result.errors[0]));
}
} }
TEST_CASE_FIXTURE(Fixture, "union_of_functions_with_mismatching_arg_variadics") TEST_CASE_FIXTURE(Fixture, "union_of_functions_with_mismatching_arg_variadics")
@ -831,13 +905,29 @@ TEST_CASE_FIXTURE(Fixture, "union_of_functions_with_mismatching_arg_variadics")
)"); )");
LUAU_REQUIRE_ERROR_COUNT(1, result); LUAU_REQUIRE_ERROR_COUNT(1, result);
if (FFlag::LuauSolverV2) if (FFlag::LuauSolverV2 && FFlag::LuauImproveTypePathsInErrors)
{
const std::string expected = "Type\n\t"
"'(number) -> ()'"
"\ncould not be converted into\n\t"
"'((...number?) -> ()) | ((number?) -> ())'";
CHECK(expected == toString(result.errors[0]));
}
else if (FFlag::LuauSolverV2)
{ {
CHECK(R"(Type CHECK(R"(Type
'(number) -> ()' '(number) -> ()'
could not be converted into could not be converted into
'((...number?) -> ()) | ((number?) -> ())')" == toString(result.errors[0])); '((...number?) -> ()) | ((number?) -> ())')" == toString(result.errors[0]));
} }
else if (FFlag::LuauImproveTypePathsInErrors)
{
const std::string expected = R"(Type
'(number) -> ()'
could not be converted into
'((...number?) -> ()) | ((number?) -> ())'; none of the union options are compatible)";
CHECK_EQ(expected, toString(result.errors[0]));
}
else else
{ {
const std::string expected = R"(Type const std::string expected = R"(Type
@ -860,11 +950,23 @@ TEST_CASE_FIXTURE(Fixture, "union_of_functions_with_mismatching_result_variadics
)"); )");
LUAU_REQUIRE_ERROR_COUNT(1, result); LUAU_REQUIRE_ERROR_COUNT(1, result);
const std::string expected = R"(Type
if (FFlag::LuauImproveTypePathsInErrors)
{
const std::string expected = "Type\n\t"
"'() -> (number?, ...number)'"
"\ncould not be converted into\n\t"
"'(() -> (...number)) | (() -> number)'; none of the union options are compatible";
CHECK_EQ(expected, toString(result.errors[0]));
}
else
{
const std::string expected = R"(Type
'() -> (number?, ...number)' '() -> (number?, ...number)'
could not be converted into could not be converted into
'(() -> (...number)) | (() -> number)'; none of the union options are compatible)"; '(() -> (...number)) | (() -> number)'; none of the union options are compatible)";
CHECK_EQ(expected, toString(result.errors[0])); CHECK_EQ(expected, toString(result.errors[0]));
}
} }
TEST_CASE_FIXTURE(Fixture, "less_greedy_unification_with_union_types") TEST_CASE_FIXTURE(Fixture, "less_greedy_unification_with_union_types")
@ -991,4 +1093,17 @@ TEST_CASE_FIXTURE(Fixture, "suppress_errors_for_prop_lookup_of_a_union_that_incl
LUAU_REQUIRE_NO_ERRORS(result); LUAU_REQUIRE_NO_ERRORS(result);
} }
TEST_CASE_FIXTURE(BuiltinsFixture, "handle_multiple_optionals")
{
CheckResult result = check(R"(
function f(a: string??)
if a then
print(a:sub(1,1))
end
end
)");
LUAU_REQUIRE_NO_ERRORS(result);
}
TEST_SUITE_END(); TEST_SUITE_END();

View file

@ -554,6 +554,14 @@ TEST_CASE("chain")
CHECK(toString(PathBuilder().index(0).mt().build()) == "[0].metatable()"); CHECK(toString(PathBuilder().index(0).mt().build()) == "[0].metatable()");
} }
TEST_CASE("human_property_then_metatable_portion")
{
ScopedFastFlag sff{FFlag::LuauSolverV2, true};
CHECK(toStringHuman(PathBuilder().readProp("a").mt().build()) == "accessing `a` has the metatable portion as ");
CHECK(toStringHuman(PathBuilder().writeProp("a").mt().build()) == "writing to `a` has the metatable portion as ");
}
TEST_SUITE_END(); // TypePathToString TEST_SUITE_END(); // TypePathToString
TEST_SUITE_BEGIN("TypePathBuilder"); TEST_SUITE_BEGIN("TypePathBuilder");