Merge branch 'upstream' into merge

This commit is contained in:
Arseny Kapoulkine 2022-03-17 17:07:51 -07:00
commit eee60dcaf3
56 changed files with 5639 additions and 4761 deletions

View file

@ -107,6 +107,7 @@ struct FunctionDoesNotTakeSelf
struct FunctionRequiresSelf struct FunctionRequiresSelf
{ {
// TODO: Delete with LuauAnyInIsOptionalIsOptional
int requiredExtraNils = 0; int requiredExtraNils = 0;
bool operator==(const FunctionRequiresSelf& rhs) const; bool operator==(const FunctionRequiresSelf& rhs) const;

View file

@ -86,6 +86,8 @@ private:
void tryUnifyIndexer(const TableIndexer& subIndexer, const TableIndexer& superIndexer); void tryUnifyIndexer(const TableIndexer& subIndexer, const TableIndexer& superIndexer);
TypeId widen(TypeId ty); TypeId widen(TypeId ty);
TypePackId widen(TypePackId tp);
TypeId deeplyOptional(TypeId ty, std::unordered_map<TypeId, TypeId> seen = {}); TypeId deeplyOptional(TypeId ty, std::unordered_map<TypeId, TypeId> seen = {});
void cacheResult(TypeId subTy, TypeId superTy); void cacheResult(TypeId subTy, TypeId superTy);

View file

@ -14,6 +14,7 @@
#include <utility> #include <utility>
LUAU_FASTFLAGVARIABLE(LuauIfElseExprFixCompletionIssue, false); LUAU_FASTFLAGVARIABLE(LuauIfElseExprFixCompletionIssue, false);
LUAU_FASTFLAG(LuauSelfCallAutocompleteFix)
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"};
@ -228,11 +229,22 @@ static std::optional<TypeId> findExpectedTypeAt(const Module& module, AstNode* n
return *it; return *it;
} }
static bool checkTypeMatch(TypeArena* typeArena, TypeId subTy, TypeId superTy)
{
InternalErrorReporter iceReporter;
UnifierSharedState unifierState(&iceReporter);
Unifier unifier(typeArena, Mode::Strict, Location(), Variance::Covariant, unifierState);
return unifier.canUnify(subTy, superTy).empty();
}
static TypeCorrectKind checkTypeCorrectKind(const Module& module, TypeArena* typeArena, AstNode* node, Position position, TypeId ty) static TypeCorrectKind checkTypeCorrectKind(const Module& module, TypeArena* typeArena, AstNode* node, Position position, TypeId ty)
{ {
ty = follow(ty); ty = follow(ty);
auto canUnify = [&typeArena](TypeId subTy, TypeId superTy) { auto canUnify = [&typeArena](TypeId subTy, TypeId superTy) {
LUAU_ASSERT(!FFlag::LuauSelfCallAutocompleteFix);
InternalErrorReporter iceReporter; InternalErrorReporter iceReporter;
UnifierSharedState unifierState(&iceReporter); UnifierSharedState unifierState(&iceReporter);
Unifier unifier(typeArena, Mode::Strict, Location(), Variance::Covariant, unifierState); Unifier unifier(typeArena, Mode::Strict, Location(), Variance::Covariant, unifierState);
@ -249,20 +261,30 @@ static TypeCorrectKind checkTypeCorrectKind(const Module& module, TypeArena* typ
TypeId expectedType = follow(*typeAtPosition); TypeId expectedType = follow(*typeAtPosition);
auto checkFunctionType = [&canUnify, &expectedType](const FunctionTypeVar* ftv) { auto checkFunctionType = [typeArena, &canUnify, &expectedType](const FunctionTypeVar* ftv) {
auto [retHead, retTail] = flatten(ftv->retType); if (FFlag::LuauSelfCallAutocompleteFix)
if (!retHead.empty() && canUnify(retHead.front(), expectedType))
return true;
// We might only have a variadic tail pack, check if the element is compatible
if (retTail)
{ {
if (const VariadicTypePack* vtp = get<VariadicTypePack>(follow(*retTail)); vtp && canUnify(vtp->ty, expectedType)) if (std::optional<TypeId> firstRetTy = first(ftv->retType))
return true; return checkTypeMatch(typeArena, *firstRetTy, expectedType);
}
return false; return false;
}
else
{
auto [retHead, retTail] = flatten(ftv->retType);
if (!retHead.empty() && canUnify(retHead.front(), expectedType))
return true;
// We might only have a variadic tail pack, check if the element is compatible
if (retTail)
{
if (const VariadicTypePack* vtp = get<VariadicTypePack>(follow(*retTail)); vtp && canUnify(vtp->ty, expectedType))
return true;
}
return false;
}
}; };
// We also want to suggest functions that return compatible result // We also want to suggest functions that return compatible result
@ -281,7 +303,10 @@ static TypeCorrectKind checkTypeCorrectKind(const Module& module, TypeArena* typ
} }
} }
return canUnify(ty, expectedType) ? TypeCorrectKind::Correct : TypeCorrectKind::None; if (FFlag::LuauSelfCallAutocompleteFix)
return checkTypeMatch(typeArena, ty, expectedType) ? TypeCorrectKind::Correct : TypeCorrectKind::None;
else
return canUnify(ty, expectedType) ? TypeCorrectKind::Correct : TypeCorrectKind::None;
} }
enum class PropIndexType enum class PropIndexType
@ -291,16 +316,22 @@ enum class PropIndexType
Key, Key,
}; };
static void autocompleteProps(const Module& module, TypeArena* typeArena, TypeId ty, PropIndexType indexType, const std::vector<AstNode*>& nodes, static void autocompleteProps(const Module& module, TypeArena* typeArena, TypeId rootTy, TypeId ty, PropIndexType indexType,
AutocompleteEntryMap& result, std::unordered_set<TypeId>& seen, std::optional<const ClassTypeVar*> containingClass = std::nullopt) const std::vector<AstNode*>& nodes, AutocompleteEntryMap& result, std::unordered_set<TypeId>& seen,
std::optional<const ClassTypeVar*> containingClass = std::nullopt)
{ {
if (FFlag::LuauSelfCallAutocompleteFix)
rootTy = follow(rootTy);
ty = follow(ty); ty = follow(ty);
if (seen.count(ty)) if (seen.count(ty))
return; return;
seen.insert(ty); seen.insert(ty);
auto isWrongIndexer = [indexType, useStrictFunctionIndexers = !!get<ClassTypeVar>(ty)](Luau::TypeId type) { auto isWrongIndexer_DEPRECATED = [indexType, useStrictFunctionIndexers = !!get<ClassTypeVar>(ty)](Luau::TypeId type) {
LUAU_ASSERT(!FFlag::LuauSelfCallAutocompleteFix);
if (indexType == PropIndexType::Key) if (indexType == PropIndexType::Key)
return false; return false;
@ -331,6 +362,48 @@ static void autocompleteProps(const Module& module, TypeArena* typeArena, TypeId
return colonIndex; return colonIndex;
} }
}; };
auto isWrongIndexer = [typeArena, rootTy, indexType](Luau::TypeId type) {
LUAU_ASSERT(FFlag::LuauSelfCallAutocompleteFix);
if (indexType == PropIndexType::Key)
return false;
bool calledWithSelf = indexType == PropIndexType::Colon;
auto isCompatibleCall = [typeArena, rootTy, calledWithSelf](const FunctionTypeVar* ftv) {
if (get<ClassTypeVar>(rootTy))
{
// Calls on classes require strict match between how function is declared and how it's called
return calledWithSelf == ftv->hasSelf;
}
if (std::optional<TypeId> firstArgTy = first(ftv->argTypes))
{
if (checkTypeMatch(typeArena, rootTy, *firstArgTy))
return calledWithSelf;
}
return !calledWithSelf;
};
if (const FunctionTypeVar* ftv = get<FunctionTypeVar>(type))
return !isCompatibleCall(ftv);
// For intersections, any part that is successful makes the whole call successful
if (const IntersectionTypeVar* itv = get<IntersectionTypeVar>(type))
{
for (auto subType : itv->parts)
{
if (const FunctionTypeVar* ftv = get<FunctionTypeVar>(Luau::follow(subType)))
{
if (isCompatibleCall(ftv))
return false;
}
}
}
return calledWithSelf;
};
auto fillProps = [&](const ClassTypeVar::Props& props) { auto fillProps = [&](const ClassTypeVar::Props& props) {
for (const auto& [name, prop] : props) for (const auto& [name, prop] : props)
@ -349,7 +422,7 @@ static void autocompleteProps(const Module& module, TypeArena* typeArena, TypeId
AutocompleteEntryKind::Property, AutocompleteEntryKind::Property,
type, type,
prop.deprecated, prop.deprecated,
isWrongIndexer(type), FFlag::LuauSelfCallAutocompleteFix ? isWrongIndexer(type) : isWrongIndexer_DEPRECATED(type),
typeCorrect, typeCorrect,
containingClass, containingClass,
&prop, &prop,
@ -361,34 +434,60 @@ static void autocompleteProps(const Module& module, TypeArena* typeArena, TypeId
} }
}; };
if (auto cls = get<ClassTypeVar>(ty)) auto fillMetatableProps = [&](const TableTypeVar* mtable) {
{
containingClass = containingClass.value_or(cls);
fillProps(cls->props);
if (cls->parent)
autocompleteProps(module, typeArena, *cls->parent, indexType, nodes, result, seen, cls);
}
else if (auto tbl = get<TableTypeVar>(ty))
fillProps(tbl->props);
else if (auto mt = get<MetatableTypeVar>(ty))
{
autocompleteProps(module, typeArena, mt->table, indexType, nodes, result, seen);
auto mtable = get<TableTypeVar>(mt->metatable);
if (!mtable)
return;
auto indexIt = mtable->props.find("__index"); auto indexIt = mtable->props.find("__index");
if (indexIt != mtable->props.end()) if (indexIt != mtable->props.end())
{ {
TypeId followed = follow(indexIt->second.type); TypeId followed = follow(indexIt->second.type);
if (get<TableTypeVar>(followed) || get<MetatableTypeVar>(followed)) if (get<TableTypeVar>(followed) || get<MetatableTypeVar>(followed))
autocompleteProps(module, typeArena, followed, indexType, nodes, result, seen); {
autocompleteProps(module, typeArena, rootTy, followed, indexType, nodes, result, seen);
}
else if (auto indexFunction = get<FunctionTypeVar>(followed)) else if (auto indexFunction = get<FunctionTypeVar>(followed))
{ {
std::optional<TypeId> indexFunctionResult = first(indexFunction->retType); std::optional<TypeId> indexFunctionResult = first(indexFunction->retType);
if (indexFunctionResult) if (indexFunctionResult)
autocompleteProps(module, typeArena, *indexFunctionResult, indexType, nodes, result, seen); autocompleteProps(module, typeArena, rootTy, *indexFunctionResult, indexType, nodes, result, seen);
}
}
};
if (auto cls = get<ClassTypeVar>(ty))
{
containingClass = containingClass.value_or(cls);
fillProps(cls->props);
if (cls->parent)
autocompleteProps(module, typeArena, rootTy, *cls->parent, indexType, nodes, result, seen, cls);
}
else if (auto tbl = get<TableTypeVar>(ty))
fillProps(tbl->props);
else if (auto mt = get<MetatableTypeVar>(ty))
{
autocompleteProps(module, typeArena, rootTy, mt->table, indexType, nodes, result, seen);
if (FFlag::LuauSelfCallAutocompleteFix)
{
if (auto mtable = get<TableTypeVar>(mt->metatable))
fillMetatableProps(mtable);
}
else
{
auto mtable = get<TableTypeVar>(mt->metatable);
if (!mtable)
return;
auto indexIt = mtable->props.find("__index");
if (indexIt != mtable->props.end())
{
TypeId followed = follow(indexIt->second.type);
if (get<TableTypeVar>(followed) || get<MetatableTypeVar>(followed))
autocompleteProps(module, typeArena, rootTy, followed, indexType, nodes, result, seen);
else if (auto indexFunction = get<FunctionTypeVar>(followed))
{
std::optional<TypeId> indexFunctionResult = first(indexFunction->retType);
if (indexFunctionResult)
autocompleteProps(module, typeArena, rootTy, *indexFunctionResult, indexType, nodes, result, seen);
}
} }
} }
} }
@ -400,7 +499,7 @@ static void autocompleteProps(const Module& module, TypeArena* typeArena, TypeId
AutocompleteEntryMap inner; AutocompleteEntryMap inner;
std::unordered_set<TypeId> innerSeen = seen; std::unordered_set<TypeId> innerSeen = seen;
autocompleteProps(module, typeArena, ty, indexType, nodes, inner, innerSeen); autocompleteProps(module, typeArena, rootTy, ty, indexType, nodes, inner, innerSeen);
for (auto& pair : inner) for (auto& pair : inner)
result.insert(pair); result.insert(pair);
@ -423,14 +522,17 @@ static void autocompleteProps(const Module& module, TypeArena* typeArena, TypeId
if (iter == endIter) if (iter == endIter)
return; return;
autocompleteProps(module, typeArena, *iter, indexType, nodes, result, seen); autocompleteProps(module, typeArena, rootTy, *iter, indexType, nodes, result, seen);
++iter; ++iter;
while (iter != endIter) while (iter != endIter)
{ {
AutocompleteEntryMap inner; AutocompleteEntryMap inner;
std::unordered_set<TypeId> innerSeen = seen; std::unordered_set<TypeId> innerSeen;
if (!FFlag::LuauSelfCallAutocompleteFix)
innerSeen = seen;
if (isNil(*iter)) if (isNil(*iter))
{ {
@ -438,7 +540,7 @@ static void autocompleteProps(const Module& module, TypeArena* typeArena, TypeId
continue; continue;
} }
autocompleteProps(module, typeArena, *iter, indexType, nodes, inner, innerSeen); autocompleteProps(module, typeArena, rootTy, *iter, indexType, nodes, inner, innerSeen);
std::unordered_set<std::string> toRemove; std::unordered_set<std::string> toRemove;
@ -455,6 +557,18 @@ static void autocompleteProps(const Module& module, TypeArena* typeArena, TypeId
++iter; ++iter;
} }
} }
else if (auto pt = get<PrimitiveTypeVar>(ty); pt && FFlag::LuauSelfCallAutocompleteFix)
{
if (pt->metatable)
{
if (auto mtable = get<TableTypeVar>(*pt->metatable))
fillMetatableProps(mtable);
}
}
else if (FFlag::LuauSelfCallAutocompleteFix && get<StringSingleton>(get<SingletonTypeVar>(ty)))
{
autocompleteProps(module, typeArena, rootTy, getSingletonTypes().stringType, indexType, nodes, result, seen);
}
} }
static void autocompleteKeywords( static void autocompleteKeywords(
@ -482,7 +596,7 @@ static void autocompleteProps(
const Module& module, TypeArena* typeArena, TypeId ty, PropIndexType indexType, const std::vector<AstNode*>& nodes, AutocompleteEntryMap& result) const Module& module, TypeArena* typeArena, TypeId ty, PropIndexType indexType, const std::vector<AstNode*>& nodes, AutocompleteEntryMap& result)
{ {
std::unordered_set<TypeId> seen; std::unordered_set<TypeId> seen;
autocompleteProps(module, typeArena, ty, indexType, nodes, result, seen); autocompleteProps(module, typeArena, ty, ty, indexType, nodes, result, seen);
} }
AutocompleteEntryMap autocompleteProps( AutocompleteEntryMap autocompleteProps(
@ -1352,7 +1466,7 @@ static AutocompleteResult autocomplete(const SourceModule& sourceModule, const M
TypeId ty = follow(*it); TypeId ty = follow(*it);
PropIndexType indexType = indexName->op == ':' ? PropIndexType::Colon : PropIndexType::Point; PropIndexType indexType = indexName->op == ':' ? PropIndexType::Colon : PropIndexType::Point;
if (isString(ty)) if (!FFlag::LuauSelfCallAutocompleteFix && isString(ty))
return {autocompleteProps(*module, typeArena, typeChecker.globalScope->bindings[AstName{"string"}].typeId, indexType, finder.ancestry), return {autocompleteProps(*module, typeArena, typeChecker.globalScope->bindings[AstName{"string"}].typeId, indexType, finder.ancestry),
finder.ancestry}; finder.ancestry};
else else

View file

@ -95,104 +95,104 @@ declare os: {
declare function require(target: any): any declare function require(target: any): any
declare function getfenv(target: any?): { [string]: any } declare function getfenv(target: any): { [string]: any }
declare _G: any declare _G: any
declare _VERSION: string declare _VERSION: string
declare function gcinfo(): number declare function gcinfo(): number
declare function print<T...>(...: T...) declare function print<T...>(...: T...)
declare function type<T>(value: T): string declare function type<T>(value: T): string
declare function typeof<T>(value: T): string declare function typeof<T>(value: T): string
-- `assert` has a magic function attached that will give more detailed type information -- `assert` has a magic function attached that will give more detailed type information
declare function assert<T>(value: T, errorMessage: string?): T declare function assert<T>(value: T, errorMessage: string?): T
declare function error<T>(message: T, level: number?) declare function error<T>(message: T, level: number?)
declare function tostring<T>(value: T): string declare function tostring<T>(value: T): string
declare function tonumber<T>(value: T, radix: number?): number? declare function tonumber<T>(value: T, radix: number?): number?
declare function rawequal<T1, T2>(a: T1, b: T2): boolean declare function rawequal<T1, T2>(a: T1, b: T2): boolean
declare function rawget<K, V>(tab: {[K]: V}, k: K): V declare function rawget<K, V>(tab: {[K]: V}, k: K): V
declare function rawset<K, V>(tab: {[K]: V}, k: K, v: V): {[K]: V} declare function rawset<K, V>(tab: {[K]: V}, k: K, v: V): {[K]: V}
declare function setfenv<T..., R...>(target: number | (T...) -> R..., env: {[string]: any}): ((T...) -> R...)? declare function setfenv<T..., R...>(target: number | (T...) -> R..., env: {[string]: any}): ((T...) -> R...)?
declare function ipairs<V>(tab: {V}): (({V}, number) -> (number, V), {V}, number) declare function ipairs<V>(tab: {V}): (({V}, number) -> (number, V), {V}, number)
declare function pcall<A..., R...>(f: (A...) -> R..., ...: A...): (boolean, R...) declare function pcall<A..., R...>(f: (A...) -> R..., ...: A...): (boolean, R...)
-- FIXME: The actual type of `xpcall` is: -- FIXME: The actual type of `xpcall` is:
-- <E, A..., R1..., R2...>(f: (A...) -> R1..., err: (E) -> R2..., A...) -> (true, R1...) | (false, R2...) -- <E, A..., R1..., R2...>(f: (A...) -> R1..., err: (E) -> R2..., A...) -> (true, R1...) | (false, R2...)
-- Since we can't represent the return value, we use (boolean, R1...). -- Since we can't represent the return value, we use (boolean, R1...).
declare function xpcall<E, A..., R1..., R2...>(f: (A...) -> R1..., err: (E) -> R2..., ...: A...): (boolean, R1...) declare function xpcall<E, A..., R1..., R2...>(f: (A...) -> R1..., err: (E) -> R2..., ...: A...): (boolean, R1...)
-- `select` has a magic function attached to provide more detailed type information -- `select` has a magic function attached to provide more detailed type information
declare function select<A...>(i: string | number, ...: A...): ...any declare function select<A...>(i: string | number, ...: A...): ...any
-- FIXME: This type is not entirely correct - `loadstring` returns a function or -- FIXME: This type is not entirely correct - `loadstring` returns a function or
-- (nil, string). -- (nil, string).
declare function loadstring<A...>(src: string, chunkname: string?): (((A...) -> any)?, string?) declare function loadstring<A...>(src: string, chunkname: string?): (((A...) -> any)?, string?)
declare function newproxy(mt: boolean?): any declare function newproxy(mt: boolean?): any
declare coroutine: { declare coroutine: {
create: <A..., R...>((A...) -> R...) -> thread, create: <A..., R...>((A...) -> R...) -> thread,
resume: <A..., R...>(thread, A...) -> (boolean, R...), resume: <A..., R...>(thread, A...) -> (boolean, R...),
running: () -> thread, running: () -> thread,
status: (thread) -> string, status: (thread) -> string,
-- FIXME: This technically returns a function, but we can't represent this yet. -- FIXME: This technically returns a function, but we can't represent this yet.
wrap: <A..., R...>((A...) -> R...) -> any, wrap: <A..., R...>((A...) -> R...) -> any,
yield: <A..., R...>(A...) -> R..., yield: <A..., R...>(A...) -> R...,
isyieldable: () -> boolean, isyieldable: () -> boolean,
close: (thread) -> (boolean, any?) close: (thread) -> (boolean, any)
} }
declare table: { declare table: {
concat: <V>({V}, string?, number?, number?) -> string, concat: <V>({V}, string?, number?, number?) -> string,
insert: (<V>({V}, V) -> ()) & (<V>({V}, number, V) -> ()), insert: (<V>({V}, V) -> ()) & (<V>({V}, number, V) -> ()),
maxn: <V>({V}) -> number, maxn: <V>({V}) -> number,
remove: <V>({V}, number?) -> V?, remove: <V>({V}, number?) -> V?,
sort: <V>({V}, ((V, V) -> boolean)?) -> (), sort: <V>({V}, ((V, V) -> boolean)?) -> (),
create: <V>(number, V?) -> {V}, create: <V>(number, V?) -> {V},
find: <V>({V}, V, number?) -> number?, find: <V>({V}, V, number?) -> number?,
unpack: <V>({V}, number?, number?) -> ...V, unpack: <V>({V}, number?, number?) -> ...V,
pack: <V>(...V) -> { n: number, [number]: V }, pack: <V>(...V) -> { n: number, [number]: V },
getn: <V>({V}) -> number, getn: <V>({V}) -> number,
foreach: <K, V>({[K]: V}, (K, V) -> ()) -> (), foreach: <K, V>({[K]: V}, (K, V) -> ()) -> (),
foreachi: <V>({V}, (number, V) -> ()) -> (), foreachi: <V>({V}, (number, V) -> ()) -> (),
move: <V>({V}, number, number, number, {V}?) -> {V}, move: <V>({V}, number, number, number, {V}?) -> {V},
clear: <K, V>({[K]: V}) -> (), clear: <K, V>({[K]: V}) -> (),
isfrozen: <K, V>({[K]: V}) -> boolean, isfrozen: <K, V>({[K]: V}) -> boolean,
} }
declare debug: { declare debug: {
info: (<R...>(thread, number, string) -> R...) & (<R...>(number, string) -> R...) & (<A..., R1..., R2...>((A...) -> R1..., string) -> R2...), info: (<R...>(thread, number, string) -> R...) & (<R...>(number, string) -> R...) & (<A..., R1..., R2...>((A...) -> R1..., string) -> R2...),
traceback: ((string?, number?) -> string) & ((thread, string?, number?) -> string), traceback: ((string?, number?) -> string) & ((thread, string?, number?) -> string),
} }
declare utf8: { declare utf8: {
char: (number, ...number) -> string, char: (number, ...number) -> string,
charpattern: string, charpattern: string,
codes: (string) -> ((string, number) -> (number, number), string, number), codes: (string) -> ((string, number) -> (number, number), string, number),
-- FIXME -- FIXME
codepoint: (string, number?, number?) -> (number, ...number), codepoint: (string, number?, number?) -> (number, ...number),
len: (string, number?, number?) -> (number?, number?), len: (string, number?, number?) -> (number?, number?),
offset: (string, number?, number?) -> number, offset: (string, number?, number?) -> number,
nfdnormalize: (string) -> string, nfdnormalize: (string) -> string,
nfcnormalize: (string) -> string, nfcnormalize: (string) -> string,
graphemes: (string, number?, number?) -> (() -> (number, number)), graphemes: (string, number?, number?) -> (() -> (number, number)),
} }
-- Cannot use `typeof` here because it will produce a polytype when we expect a monotype. -- Cannot use `typeof` here because it will produce a polytype when we expect a monotype.
declare function unpack<V>(tab: {V}, i: number?, j: number?): ...V declare function unpack<V>(tab: {V}, i: number?, j: number?): ...V
)BUILTIN_SRC"; )BUILTIN_SRC";

View file

@ -7,6 +7,8 @@
#include <stdexcept> #include <stdexcept>
LUAU_FASTFLAGVARIABLE(BetterDiagnosticCodesInStudio, false);
static std::string wrongNumberOfArgsString(size_t expectedCount, size_t actualCount, const char* argPrefix = nullptr, bool isVariadic = false) static std::string wrongNumberOfArgsString(size_t expectedCount, size_t actualCount, const char* argPrefix = nullptr, bool isVariadic = false)
{ {
std::string s = "expects "; std::string s = "expects ";
@ -223,7 +225,14 @@ struct ErrorConverter
std::string operator()(const Luau::SyntaxError& e) const std::string operator()(const Luau::SyntaxError& e) const
{ {
return "Syntax error: " + e.message; if (FFlag::BetterDiagnosticCodesInStudio)
{
return e.message;
}
else
{
return "Syntax error: " + e.message;
}
} }
std::string operator()(const Luau::CodeTooComplex&) const std::string operator()(const Luau::CodeTooComplex&) const

View file

@ -14,6 +14,7 @@
LUAU_FASTFLAGVARIABLE(DebugLuauFreezeArena, false) LUAU_FASTFLAGVARIABLE(DebugLuauFreezeArena, false)
LUAU_FASTFLAGVARIABLE(DebugLuauTrackOwningArena, false) // Remove with FFlagLuauImmutableTypes LUAU_FASTFLAGVARIABLE(DebugLuauTrackOwningArena, false) // Remove with FFlagLuauImmutableTypes
LUAU_FASTINTVARIABLE(LuauTypeCloneRecursionLimit, 300) LUAU_FASTINTVARIABLE(LuauTypeCloneRecursionLimit, 300)
LUAU_FASTFLAGVARIABLE(LuauCloneDeclaredGlobals, false)
LUAU_FASTFLAG(LuauImmutableTypes) LUAU_FASTFLAG(LuauImmutableTypes)
namespace Luau namespace Luau
@ -536,6 +537,12 @@ bool Module::clonePublicInterface()
if (get<GenericTypeVar>(follow(ty))) if (get<GenericTypeVar>(follow(ty)))
*asMutable(ty) = AnyTypeVar{}; *asMutable(ty) = AnyTypeVar{};
if (FFlag::LuauCloneDeclaredGlobals)
{
for (auto& [name, ty] : declaredGlobals)
ty = clone(ty, interfaceTypes, seenTypes, seenTypePacks, cloneState);
}
freeze(internalTypes); freeze(internalTypes);
freeze(interfaceTypes); freeze(interfaceTypes);

View file

@ -29,22 +29,24 @@ LUAU_FASTFLAGVARIABLE(LuauRecursiveTypeParameterRestriction, false)
LUAU_FASTFLAGVARIABLE(LuauGenericFunctionsDontCacheTypeParams, false) LUAU_FASTFLAGVARIABLE(LuauGenericFunctionsDontCacheTypeParams, false)
LUAU_FASTFLAGVARIABLE(LuauImmutableTypes, false) LUAU_FASTFLAGVARIABLE(LuauImmutableTypes, false)
LUAU_FASTFLAGVARIABLE(LuauSealExports, false) LUAU_FASTFLAGVARIABLE(LuauSealExports, false)
LUAU_FASTFLAGVARIABLE(LuauSelfCallAutocompleteFix, false)
LUAU_FASTFLAGVARIABLE(LuauSingletonTypes, false) LUAU_FASTFLAGVARIABLE(LuauSingletonTypes, false)
LUAU_FASTFLAGVARIABLE(LuauDiscriminableUnions2, false) LUAU_FASTFLAGVARIABLE(LuauDiscriminableUnions2, false)
LUAU_FASTFLAGVARIABLE(LuauExpectedTypesOfProperties, false) LUAU_FASTFLAGVARIABLE(LuauExpectedTypesOfProperties, false)
LUAU_FASTFLAGVARIABLE(LuauErrorRecoveryType, false) LUAU_FASTFLAGVARIABLE(LuauErrorRecoveryType, false)
LUAU_FASTFLAGVARIABLE(LuauOnlyMutateInstantiatedTables, false) LUAU_FASTFLAGVARIABLE(LuauOnlyMutateInstantiatedTables, false)
LUAU_FASTFLAGVARIABLE(LuauPropertiesGetExpectedType, false) LUAU_FASTFLAGVARIABLE(LuauPropertiesGetExpectedType, false)
LUAU_FASTFLAGVARIABLE(LuauStatFunctionSimplify, false) LUAU_FASTFLAGVARIABLE(LuauStatFunctionSimplify2, false)
LUAU_FASTFLAGVARIABLE(LuauUnsealedTableLiteral, false) LUAU_FASTFLAGVARIABLE(LuauUnsealedTableLiteral, false)
LUAU_FASTFLAGVARIABLE(LuauTwoPassAliasDefinitionFix, false) LUAU_FASTFLAGVARIABLE(LuauTwoPassAliasDefinitionFix, false)
LUAU_FASTFLAGVARIABLE(LuauAssertStripsFalsyTypes, false) LUAU_FASTFLAGVARIABLE(LuauAssertStripsFalsyTypes, false)
LUAU_FASTFLAGVARIABLE(LuauReturnAnyInsteadOfICE, false) // Eventually removed as false. LUAU_FASTFLAGVARIABLE(LuauReturnAnyInsteadOfICE, false) // Eventually removed as false.
LUAU_FASTFLAG(LuauWidenIfSupertypeIsFree) LUAU_FASTFLAG(LuauWidenIfSupertypeIsFree2)
LUAU_FASTFLAGVARIABLE(LuauDoNotTryToReduce, false) LUAU_FASTFLAGVARIABLE(LuauDoNotTryToReduce, false)
LUAU_FASTFLAGVARIABLE(LuauDoNotAccidentallyDependOnPointerOrdering, false) LUAU_FASTFLAGVARIABLE(LuauDoNotAccidentallyDependOnPointerOrdering, false)
LUAU_FASTFLAGVARIABLE(LuauFixArgumentCountMismatchAmountWithGenericTypes, false) LUAU_FASTFLAGVARIABLE(LuauFixArgumentCountMismatchAmountWithGenericTypes, false)
LUAU_FASTFLAGVARIABLE(LuauFixIncorrectLineNumberDuplicateType, false) LUAU_FASTFLAGVARIABLE(LuauFixIncorrectLineNumberDuplicateType, false)
LUAU_FASTFLAG(LuauAnyInIsOptionalIsOptional)
namespace Luau namespace Luau
{ {
@ -1099,7 +1101,7 @@ void TypeChecker::check(const ScopePtr& scope, TypeId ty, const ScopePtr& funSco
scope->bindings[name->local] = {anyIfNonstrict(quantify(funScope, ty, name->local->location)), name->local->location}; scope->bindings[name->local] = {anyIfNonstrict(quantify(funScope, ty, name->local->location)), name->local->location};
return; return;
} }
else if (auto name = function.name->as<AstExprIndexName>(); name && FFlag::LuauStatFunctionSimplify) else if (auto name = function.name->as<AstExprIndexName>(); name && FFlag::LuauStatFunctionSimplify2)
{ {
TypeId exprTy = checkExpr(scope, *name->expr).type; TypeId exprTy = checkExpr(scope, *name->expr).type;
TableTypeVar* ttv = getMutableTableType(exprTy); TableTypeVar* ttv = getMutableTableType(exprTy);
@ -1111,7 +1113,10 @@ void TypeChecker::check(const ScopePtr& scope, TypeId ty, const ScopePtr& funSco
reportError(TypeError{function.location, OnlyTablesCanHaveMethods{exprTy}}); reportError(TypeError{function.location, OnlyTablesCanHaveMethods{exprTy}});
} }
else if (ttv->state == TableState::Sealed) else if (ttv->state == TableState::Sealed)
reportError(TypeError{function.location, CannotExtendTable{exprTy, CannotExtendTable::Property, name->index.value}}); {
if (!ttv->indexer || !isPrim(ttv->indexer->indexType, PrimitiveTypeVar::String))
reportError(TypeError{function.location, CannotExtendTable{exprTy, CannotExtendTable::Property, name->index.value}});
}
ty = follow(ty); ty = follow(ty);
@ -1134,7 +1139,7 @@ void TypeChecker::check(const ScopePtr& scope, TypeId ty, const ScopePtr& funSco
if (ttv && ttv->state != TableState::Sealed) if (ttv && ttv->state != TableState::Sealed)
ttv->props[name->index.value] = {follow(quantify(funScope, ty, name->indexLocation)), /* deprecated */ false, {}, name->indexLocation}; ttv->props[name->index.value] = {follow(quantify(funScope, ty, name->indexLocation)), /* deprecated */ false, {}, name->indexLocation};
} }
else if (FFlag::LuauStatFunctionSimplify) else if (FFlag::LuauStatFunctionSimplify2)
{ {
LUAU_ASSERT(function.name->is<AstExprError>()); LUAU_ASSERT(function.name->is<AstExprError>());
@ -1144,7 +1149,7 @@ void TypeChecker::check(const ScopePtr& scope, TypeId ty, const ScopePtr& funSco
} }
else if (function.func->self) else if (function.func->self)
{ {
LUAU_ASSERT(!FFlag::LuauStatFunctionSimplify); LUAU_ASSERT(!FFlag::LuauStatFunctionSimplify2);
AstExprIndexName* indexName = function.name->as<AstExprIndexName>(); AstExprIndexName* indexName = function.name->as<AstExprIndexName>();
if (!indexName) if (!indexName)
@ -1183,7 +1188,7 @@ void TypeChecker::check(const ScopePtr& scope, TypeId ty, const ScopePtr& funSco
} }
else else
{ {
LUAU_ASSERT(!FFlag::LuauStatFunctionSimplify); LUAU_ASSERT(!FFlag::LuauStatFunctionSimplify2);
TypeId leftType = checkLValueBinding(scope, *function.name); TypeId leftType = checkLValueBinding(scope, *function.name);
@ -1410,6 +1415,9 @@ void TypeChecker::check(const ScopePtr& scope, const AstStatDeclareClass& declar
{ {
ftv->argNames.insert(ftv->argNames.begin(), FunctionArgument{"self", {}}); ftv->argNames.insert(ftv->argNames.begin(), FunctionArgument{"self", {}});
ftv->argTypes = addTypePack(TypePack{{classTy}, ftv->argTypes}); ftv->argTypes = addTypePack(TypePack{{classTy}, ftv->argTypes});
if (FFlag::LuauSelfCallAutocompleteFix)
ftv->hasSelf = true;
} }
} }
@ -1883,19 +1891,27 @@ std::optional<TypeId> TypeChecker::tryStripUnionFromNil(TypeId ty)
{ {
if (const UnionTypeVar* utv = get<UnionTypeVar>(ty)) if (const UnionTypeVar* utv = get<UnionTypeVar>(ty))
{ {
bool hasNil = false; if (FFlag::LuauAnyInIsOptionalIsOptional)
for (TypeId option : utv)
{ {
if (isNil(option)) if (!std::any_of(begin(utv), end(utv), isNil))
{ return ty;
hasNil = true;
break;
}
} }
else
{
bool hasNil = false;
if (!hasNil) for (TypeId option : utv)
return ty; {
if (isNil(option))
{
hasNil = true;
break;
}
}
if (!hasNil)
return ty;
}
std::vector<TypeId> result; std::vector<TypeId> result;
@ -1916,14 +1932,34 @@ std::optional<TypeId> TypeChecker::tryStripUnionFromNil(TypeId ty)
TypeId TypeChecker::stripFromNilAndReport(TypeId ty, const Location& location) TypeId TypeChecker::stripFromNilAndReport(TypeId ty, const Location& location)
{ {
if (isOptional(ty)) if (FFlag::LuauAnyInIsOptionalIsOptional)
{ {
if (std::optional<TypeId> strippedUnion = tryStripUnionFromNil(follow(ty))) ty = follow(ty);
if (auto utv = get<UnionTypeVar>(ty))
{
if (!std::any_of(begin(utv), end(utv), isNil))
return ty;
}
if (std::optional<TypeId> strippedUnion = tryStripUnionFromNil(ty))
{ {
reportError(location, OptionalValueAccess{ty}); reportError(location, OptionalValueAccess{ty});
return follow(*strippedUnion); return follow(*strippedUnion);
} }
} }
else
{
if (isOptional(ty))
{
if (std::optional<TypeId> strippedUnion = tryStripUnionFromNil(follow(ty)))
{
reportError(location, OptionalValueAccess{ty});
return follow(*strippedUnion);
}
}
}
return ty; return ty;
} }
@ -2935,9 +2971,25 @@ TypeId TypeChecker::checkFunctionName(const ScopePtr& scope, AstExpr& funName, T
return errorRecoveryType(scope); return errorRecoveryType(scope);
} }
// Cannot extend sealed table, but we dont report an error here because it will be reported during AstStatFunction check if (FFlag::LuauStatFunctionSimplify2)
if (lhsType->persistent || ttv->state == TableState::Sealed) {
return errorRecoveryType(scope); if (lhsType->persistent)
return errorRecoveryType(scope);
// Cannot extend sealed table, but we dont report an error here because it will be reported during AstStatFunction check
if (ttv->state == TableState::Sealed)
{
if (ttv->indexer && isPrim(ttv->indexer->indexType, PrimitiveTypeVar::String))
return ttv->indexer->indexResultType;
else
return errorRecoveryType(scope);
}
}
else
{
if (lhsType->persistent || ttv->state == TableState::Sealed)
return errorRecoveryType(scope);
}
Name name = indexName->index.value; Name name = indexName->index.value;
@ -3393,7 +3445,7 @@ void TypeChecker::checkArgumentList(
else if (state.log.getMutable<ErrorTypeVar>(t)) else if (state.log.getMutable<ErrorTypeVar>(t))
{ {
} // ok } // ok
else if (isNonstrictMode() && state.log.getMutable<AnyTypeVar>(t)) else if (!FFlag::LuauAnyInIsOptionalIsOptional && isNonstrictMode() && state.log.getMutable<AnyTypeVar>(t))
{ {
} // ok } // ok
else else
@ -3467,7 +3519,11 @@ void TypeChecker::checkArgumentList(
} }
TypePackId varPack = addTypePack(TypePackVar{TypePack{rest, argIter.tail()}}); TypePackId varPack = addTypePack(TypePackVar{TypePack{rest, argIter.tail()}});
state.tryUnify(varPack, tail); if (FFlag::LuauWidenIfSupertypeIsFree2)
state.tryUnify(varPack, tail);
else
state.tryUnify(tail, varPack);
return; return;
} }
else if (state.log.getMutable<FreeTypePack>(tail)) else if (state.log.getMutable<FreeTypePack>(tail))
@ -3542,6 +3598,23 @@ ExprResult<TypePackId> TypeChecker::checkExprPack(const ScopePtr& scope, const A
actualFunctionType = follow(actualFunctionType); actualFunctionType = follow(actualFunctionType);
TypePackId retPack;
if (!FFlag::LuauWidenIfSupertypeIsFree2)
{
retPack = freshTypePack(scope->level);
}
else
{
if (auto free = get<FreeTypeVar>(actualFunctionType))
{
retPack = freshTypePack(free->level);
TypePackId freshArgPack = freshTypePack(free->level);
*asMutable(actualFunctionType) = FunctionTypeVar(free->level, freshArgPack, retPack);
}
else
retPack = freshTypePack(scope->level);
}
// checkExpr will log the pre-instantiated type of the function. // checkExpr will log the pre-instantiated type of the function.
// That's not nearly as interesting as the instantiated type, which will include details about how // That's not nearly as interesting as the instantiated type, which will include details about how
// generic functions are being instantiated for this particular callsite. // generic functions are being instantiated for this particular callsite.
@ -3550,8 +3623,6 @@ ExprResult<TypePackId> TypeChecker::checkExprPack(const ScopePtr& scope, const A
std::vector<TypeId> overloads = flattenIntersection(actualFunctionType); std::vector<TypeId> overloads = flattenIntersection(actualFunctionType);
TypePackId retPack = freshTypePack(scope->level);
std::vector<std::optional<TypeId>> expectedTypes = getExpectedTypesForCall(overloads, expr.args.size, expr.self); std::vector<std::optional<TypeId>> expectedTypes = getExpectedTypesForCall(overloads, expr.args.size, expr.self);
ExprResult<TypePackId> argListResult = checkExprList(scope, expr.location, expr.args, false, {}, expectedTypes); ExprResult<TypePackId> argListResult = checkExprList(scope, expr.location, expr.args, false, {}, expectedTypes);
@ -3682,7 +3753,7 @@ std::optional<ExprResult<TypePackId>> TypeChecker::checkCallOverload(const Scope
// has been instantiated, so is a monotype. We can therefore // has been instantiated, so is a monotype. We can therefore
// unify it with a monomorphic function. // unify it with a monomorphic function.
TypeId r = addType(FunctionTypeVar(scope->level, argPack, retPack)); TypeId r = addType(FunctionTypeVar(scope->level, argPack, retPack));
if (FFlag::LuauWidenIfSupertypeIsFree) if (FFlag::LuauWidenIfSupertypeIsFree2)
{ {
UnifierOptions options; UnifierOptions options;
options.isFunctionCall = true; options.isFunctionCall = true;
@ -3772,7 +3843,7 @@ std::optional<ExprResult<TypePackId>> TypeChecker::checkCallOverload(const Scope
{ {
state.log.commit(); state.log.commit();
if (isNonstrictMode() && !expr.self && expr.func->is<AstExprIndexName>() && ftv->hasSelf) if (!FFlag::LuauAnyInIsOptionalIsOptional && isNonstrictMode() && !expr.self && expr.func->is<AstExprIndexName>() && ftv->hasSelf)
{ {
// If we are running in nonstrict mode, passing fewer arguments than the function is declared to take AND // If we are running in nonstrict mode, passing fewer arguments than the function is declared to take AND
// the function is declared with colon notation AND we use dot notation, warn. // the function is declared with colon notation AND we use dot notation, warn.

View file

@ -26,6 +26,7 @@ LUAU_FASTINT(LuauTypeInferRecursionLimit)
LUAU_FASTFLAG(LuauErrorRecoveryType) LUAU_FASTFLAG(LuauErrorRecoveryType)
LUAU_FASTFLAG(LuauSubtypingAddOptPropsToUnsealedTables) LUAU_FASTFLAG(LuauSubtypingAddOptPropsToUnsealedTables)
LUAU_FASTFLAG(LuauDiscriminableUnions2) LUAU_FASTFLAG(LuauDiscriminableUnions2)
LUAU_FASTFLAGVARIABLE(LuauAnyInIsOptionalIsOptional, false)
namespace Luau namespace Luau
{ {
@ -201,11 +202,16 @@ bool isOptional(TypeId ty)
if (isNil(ty)) if (isNil(ty))
return true; return true;
auto utv = get<UnionTypeVar>(follow(ty)); ty = follow(ty);
if (FFlag::LuauAnyInIsOptionalIsOptional && get<AnyTypeVar>(ty))
return true;
auto utv = get<UnionTypeVar>(ty);
if (!utv) if (!utv)
return false; return false;
return std::any_of(begin(utv), end(utv), isNil); return std::any_of(begin(utv), end(utv), FFlag::LuauAnyInIsOptionalIsOptional ? isOptional : isNil);
} }
bool isTableIntersection(TypeId ty) bool isTableIntersection(TypeId ty)

View file

@ -20,11 +20,13 @@ LUAU_FASTFLAGVARIABLE(LuauTableSubtypingVariance2, false);
LUAU_FASTFLAG(LuauSingletonTypes) LUAU_FASTFLAG(LuauSingletonTypes)
LUAU_FASTFLAG(LuauErrorRecoveryType); LUAU_FASTFLAG(LuauErrorRecoveryType);
LUAU_FASTFLAGVARIABLE(LuauSubtypingAddOptPropsToUnsealedTables, false) LUAU_FASTFLAGVARIABLE(LuauSubtypingAddOptPropsToUnsealedTables, false)
LUAU_FASTFLAGVARIABLE(LuauWidenIfSupertypeIsFree, false) LUAU_FASTFLAGVARIABLE(LuauWidenIfSupertypeIsFree2, false)
LUAU_FASTFLAGVARIABLE(LuauDifferentOrderOfUnificationDoesntMatter, false) LUAU_FASTFLAGVARIABLE(LuauDifferentOrderOfUnificationDoesntMatter, false)
LUAU_FASTFLAGVARIABLE(LuauTxnLogSeesTypePacks2, false) LUAU_FASTFLAGVARIABLE(LuauTxnLogSeesTypePacks2, false)
LUAU_FASTFLAGVARIABLE(LuauTxnLogCheckForInvalidation, false) LUAU_FASTFLAGVARIABLE(LuauTxnLogCheckForInvalidation, false)
LUAU_FASTFLAGVARIABLE(LuauTxnLogRefreshFunctionPointers, false)
LUAU_FASTFLAGVARIABLE(LuauTxnLogDontRetryForIndexers, false) LUAU_FASTFLAGVARIABLE(LuauTxnLogDontRetryForIndexers, false)
LUAU_FASTFLAG(LuauAnyInIsOptionalIsOptional)
namespace Luau namespace Luau
{ {
@ -272,12 +274,6 @@ TypePackId Widen::clean(TypePackId)
bool Widen::ignoreChildren(TypeId ty) bool Widen::ignoreChildren(TypeId ty)
{ {
// Sometimes we unify ("hi") -> free1 with (free2) -> free3, so don't ignore functions.
// TODO: should we be doing this? we would need to rework how checkCallOverload does the unification.
if (log->is<FunctionTypeVar>(ty))
return false;
// We only care about unions.
return !log->is<UnionTypeVar>(ty); return !log->is<UnionTypeVar>(ty);
} }
@ -990,7 +986,7 @@ void Unifier::tryUnify_(TypePackId subTp, TypePackId superTp, bool isFunctionCal
if (!log.getMutable<ErrorTypeVar>(superTp)) if (!log.getMutable<ErrorTypeVar>(superTp))
{ {
log.replace(superTp, Unifiable::Bound<TypePackId>(subTp)); log.replace(superTp, Unifiable::Bound<TypePackId>(widen(subTp)));
} }
} }
else if (log.getMutable<Unifiable::Free>(subTp)) else if (log.getMutable<Unifiable::Free>(subTp))
@ -1107,7 +1103,7 @@ void Unifier::tryUnify_(TypePackId subTp, TypePackId superTp, bool isFunctionCal
} }
// In nonstrict mode, any also marks an optional argument. // In nonstrict mode, any also marks an optional argument.
else if (superIter.good() && isNonstrictMode() && log.getMutable<AnyTypeVar>(log.follow(*superIter))) else if (!FFlag::LuauAnyInIsOptionalIsOptional && superIter.good() && isNonstrictMode() && log.getMutable<AnyTypeVar>(log.follow(*superIter)))
{ {
superIter.advance(); superIter.advance();
continue; continue;
@ -1280,6 +1276,13 @@ void Unifier::tryUnifyFunctions(TypeId subTy, TypeId superTy, bool isFunctionCal
tryUnify_(subFunction->retType, superFunction->retType); tryUnify_(subFunction->retType, superFunction->retType);
} }
if (FFlag::LuauTxnLogRefreshFunctionPointers)
{
// Updating the log may have invalidated the function pointers
superFunction = log.getMutable<FunctionTypeVar>(superTy);
subFunction = log.getMutable<FunctionTypeVar>(subTy);
}
if (!FFlag::LuauImmutableTypes) if (!FFlag::LuauImmutableTypes)
{ {
if (superFunction->definition && !subFunction->definition && !subTy->persistent) if (superFunction->definition && !subFunction->definition && !subTy->persistent)
@ -1357,10 +1360,18 @@ void Unifier::tryUnifyTables(TypeId subTy, TypeId superTy, bool isIntersection)
{ {
auto subIter = subTable->props.find(propName); auto subIter = subTable->props.find(propName);
bool isAny = log.getMutable<AnyTypeVar>(log.follow(superProp.type)); if (FFlag::LuauAnyInIsOptionalIsOptional)
{
if (subIter == subTable->props.end() && (!FFlag::LuauSubtypingAddOptPropsToUnsealedTables || subTable->state == TableState::Unsealed) && !isOptional(superProp.type))
missingProperties.push_back(propName);
}
else
{
bool isAny = log.getMutable<AnyTypeVar>(log.follow(superProp.type));
if (subIter == subTable->props.end() && (!FFlag::LuauSubtypingAddOptPropsToUnsealedTables || subTable->state == TableState::Unsealed) && !isOptional(superProp.type) && !isAny) if (subIter == subTable->props.end() && (!FFlag::LuauSubtypingAddOptPropsToUnsealedTables || subTable->state == TableState::Unsealed) && !isOptional(superProp.type) && !isAny)
missingProperties.push_back(propName); missingProperties.push_back(propName);
}
} }
if (!missingProperties.empty()) if (!missingProperties.empty())
@ -1378,9 +1389,17 @@ void Unifier::tryUnifyTables(TypeId subTy, TypeId superTy, bool isIntersection)
{ {
auto superIter = superTable->props.find(propName); auto superIter = superTable->props.find(propName);
bool isAny = log.is<AnyTypeVar>(log.follow(subProp.type)); if (FFlag::LuauAnyInIsOptionalIsOptional)
if (superIter == superTable->props.end() && (FFlag::LuauSubtypingAddOptPropsToUnsealedTables || (!isOptional(subProp.type) && !isAny))) {
extraProperties.push_back(propName); if (superIter == superTable->props.end() && (FFlag::LuauSubtypingAddOptPropsToUnsealedTables || !isOptional(subProp.type)))
extraProperties.push_back(propName);
}
else
{
bool isAny = log.is<AnyTypeVar>(log.follow(subProp.type));
if (superIter == superTable->props.end() && (FFlag::LuauSubtypingAddOptPropsToUnsealedTables || (!isOptional(subProp.type) && !isAny)))
extraProperties.push_back(propName);
}
} }
if (!extraProperties.empty()) if (!extraProperties.empty())
@ -1424,6 +1443,12 @@ void Unifier::tryUnifyTables(TypeId subTy, TypeId superTy, bool isIntersection)
if (innerState.errors.empty()) if (innerState.errors.empty())
log.concat(std::move(innerState.log)); log.concat(std::move(innerState.log));
} }
else if (FFlag::LuauAnyInIsOptionalIsOptional && (!FFlag::LuauSubtypingAddOptPropsToUnsealedTables || subTable->state == TableState::Unsealed) && isOptional(prop.type))
// This is sound because unsealed table types are precise, so `{ p : T } <: { p : T, q : U? }`
// since if `t : { p : T }` then we are guaranteed that `t.q` is `nil`.
// TODO: if the supertype is written to, the subtype may no longer be precise (alias analysis?)
{
}
else if ((!FFlag::LuauSubtypingAddOptPropsToUnsealedTables || subTable->state == TableState::Unsealed) && (isOptional(prop.type) || get<AnyTypeVar>(follow(prop.type)))) else if ((!FFlag::LuauSubtypingAddOptPropsToUnsealedTables || subTable->state == TableState::Unsealed) && (isOptional(prop.type) || get<AnyTypeVar>(follow(prop.type))))
// This is sound because unsealed table types are precise, so `{ p : T } <: { p : T, q : U? }` // This is sound because unsealed table types are precise, so `{ p : T } <: { p : T, q : U? }`
// since if `t : { p : T }` then we are guaranteed that `t.q` is `nil`. // since if `t : { p : T }` then we are guaranteed that `t.q` is `nil`.
@ -1497,6 +1522,9 @@ void Unifier::tryUnifyTables(TypeId subTy, TypeId superTy, bool isIntersection)
else if (variance == Covariant) else if (variance == Covariant)
{ {
} }
else if (FFlag::LuauAnyInIsOptionalIsOptional && !FFlag::LuauSubtypingAddOptPropsToUnsealedTables && isOptional(prop.type))
{
}
else if (!FFlag::LuauSubtypingAddOptPropsToUnsealedTables && (isOptional(prop.type) || get<AnyTypeVar>(follow(prop.type)))) else if (!FFlag::LuauSubtypingAddOptPropsToUnsealedTables && (isOptional(prop.type) || get<AnyTypeVar>(follow(prop.type))))
{ {
} }
@ -1618,7 +1646,7 @@ void Unifier::tryUnifyTables(TypeId subTy, TypeId superTy, bool isIntersection)
TypeId Unifier::widen(TypeId ty) TypeId Unifier::widen(TypeId ty)
{ {
if (!FFlag::LuauWidenIfSupertypeIsFree) if (!FFlag::LuauWidenIfSupertypeIsFree2)
return ty; return ty;
Widen widen{types}; Widen widen{types};
@ -1627,10 +1655,21 @@ TypeId Unifier::widen(TypeId ty)
return result.value_or(ty); return result.value_or(ty);
} }
TypePackId Unifier::widen(TypePackId tp)
{
if (!FFlag::LuauWidenIfSupertypeIsFree2)
return tp;
Widen widen{types};
std::optional<TypePackId> result = widen.substitute(tp);
// TODO: what does it mean for substitution to fail to widen?
return result.value_or(tp);
}
TypeId Unifier::deeplyOptional(TypeId ty, std::unordered_map<TypeId, TypeId> seen) TypeId Unifier::deeplyOptional(TypeId ty, std::unordered_map<TypeId, TypeId> seen)
{ {
ty = follow(ty); ty = follow(ty);
if (get<AnyTypeVar>(ty)) if (!FFlag::LuauAnyInIsOptionalIsOptional && get<AnyTypeVar>(ty))
return ty; return ty;
else if (isOptional(ty)) else if (isOptional(ty))
return ty; return ty;
@ -1744,7 +1783,10 @@ void Unifier::tryUnifyFreeTable(TypeId subTy, TypeId superTy)
{ {
if (auto subProp = findTablePropertyRespectingMeta(subTy, freeName)) if (auto subProp = findTablePropertyRespectingMeta(subTy, freeName))
{ {
tryUnify_(freeProp.type, *subProp); if (FFlag::LuauWidenIfSupertypeIsFree2)
tryUnify_(*subProp, freeProp.type);
else
tryUnify_(freeProp.type, *subProp);
/* /*
* TypeVars are commonly cyclic, so it is entirely possible * TypeVars are commonly cyclic, so it is entirely possible

View file

@ -11,18 +11,11 @@
LUAU_FASTINTVARIABLE(LuauRecursionLimit, 1000) LUAU_FASTINTVARIABLE(LuauRecursionLimit, 1000)
LUAU_FASTINTVARIABLE(LuauParseErrorLimit, 100) LUAU_FASTINTVARIABLE(LuauParseErrorLimit, 100)
LUAU_FASTFLAGVARIABLE(LuauParseSingletonTypes, false) LUAU_FASTFLAGVARIABLE(LuauParseSingletonTypes, false)
LUAU_FASTFLAGVARIABLE(LuauParseAllHotComments, false)
LUAU_FASTFLAGVARIABLE(LuauTableFieldFunctionDebugname, false) LUAU_FASTFLAGVARIABLE(LuauTableFieldFunctionDebugname, false)
namespace Luau namespace Luau
{ {
static bool isComment(const Lexeme& lexeme)
{
LUAU_ASSERT(!FFlag::LuauParseAllHotComments);
return lexeme.type == Lexeme::Comment || lexeme.type == Lexeme::BlockComment;
}
ParseError::ParseError(const Location& location, const std::string& message) ParseError::ParseError(const Location& location, const std::string& message)
: location(location) : location(location)
, message(message) , message(message)
@ -146,54 +139,13 @@ ParseResult Parser::parse(const char* buffer, size_t bufferSize, AstNameTable& n
{ {
LUAU_TIMETRACE_SCOPE("Parser::parse", "Parser"); LUAU_TIMETRACE_SCOPE("Parser::parse", "Parser");
Parser p(buffer, bufferSize, names, allocator, FFlag::LuauParseAllHotComments ? options : ParseOptions()); Parser p(buffer, bufferSize, names, allocator, options);
try try
{ {
if (FFlag::LuauParseAllHotComments) AstStatBlock* root = p.parseChunk();
{
AstStatBlock* root = p.parseChunk();
return ParseResult{root, std::move(p.hotcomments), std::move(p.parseErrors), std::move(p.commentLocations)}; return ParseResult{root, std::move(p.hotcomments), std::move(p.parseErrors), std::move(p.commentLocations)};
}
else
{
std::vector<HotComment> hotcomments;
while (isComment(p.lexer.current()) || p.lexer.current().type == Lexeme::BrokenComment)
{
const char* text = p.lexer.current().data;
unsigned int length = p.lexer.current().length;
if (length && text[0] == '!')
{
unsigned int end = length;
while (end > 0 && isSpace(text[end - 1]))
--end;
hotcomments.push_back({true, p.lexer.current().location, std::string(text + 1, text + end)});
}
const Lexeme::Type type = p.lexer.current().type;
const Location loc = p.lexer.current().location;
if (options.captureComments)
p.commentLocations.push_back(Comment{type, loc});
if (type == Lexeme::BrokenComment)
break;
p.lexer.next();
}
p.lexer.setSkipComments(true);
p.options = options;
AstStatBlock* root = p.parseChunk();
return ParseResult{root, hotcomments, p.parseErrors, std::move(p.commentLocations)};
}
} }
catch (ParseError& err) catch (ParseError& err)
{ {
@ -225,10 +177,11 @@ Parser::Parser(const char* buffer, size_t bufferSize, AstNameTable& names, Alloc
matchRecoveryStopOnToken.assign(Lexeme::Type::Reserved_END, 0); matchRecoveryStopOnToken.assign(Lexeme::Type::Reserved_END, 0);
matchRecoveryStopOnToken[Lexeme::Type::Eof] = 1; matchRecoveryStopOnToken[Lexeme::Type::Eof] = 1;
if (FFlag::LuauParseAllHotComments) // required for lookahead() to work across a comment boundary and for nextLexeme() to work when captureComments is false
lexer.setSkipComments(true); lexer.setSkipComments(true);
// read first lexeme // read first lexeme (any hot comments get .header = true)
LUAU_ASSERT(hotcommentHeader);
nextLexeme(); nextLexeme();
// all hot comments parsed after the first non-comment lexeme are special in that they don't affect type checking / linting mode // all hot comments parsed after the first non-comment lexeme are special in that they don't affect type checking / linting mode
@ -2831,49 +2784,31 @@ void Parser::nextLexeme()
{ {
if (options.captureComments) if (options.captureComments)
{ {
if (FFlag::LuauParseAllHotComments) Lexeme::Type type = lexer.next(/* skipComments= */ false).type;
while (type == Lexeme::BrokenComment || type == Lexeme::Comment || type == Lexeme::BlockComment)
{ {
Lexeme::Type type = lexer.next(/* skipComments= */ false).type; const Lexeme& lexeme = lexer.current();
commentLocations.push_back(Comment{lexeme.type, lexeme.location});
while (type == Lexeme::BrokenComment || type == Lexeme::Comment || type == Lexeme::BlockComment) // Subtlety: Broken comments are weird because we record them as comments AND pass them to the parser as a lexeme.
// The parser will turn this into a proper syntax error.
if (lexeme.type == Lexeme::BrokenComment)
return;
// Comments starting with ! are called "hot comments" and contain directives for type checking / linting
if (lexeme.type == Lexeme::Comment && lexeme.length && lexeme.data[0] == '!')
{ {
const Lexeme& lexeme = lexer.current(); const char* text = lexeme.data;
commentLocations.push_back(Comment{lexeme.type, lexeme.location});
// Subtlety: Broken comments are weird because we record them as comments AND pass them to the parser as a lexeme. unsigned int end = lexeme.length;
// The parser will turn this into a proper syntax error. while (end > 0 && isSpace(text[end - 1]))
if (lexeme.type == Lexeme::BrokenComment) --end;
return;
// Comments starting with ! are called "hot comments" and contain directives for type checking / linting hotcomments.push_back({hotcommentHeader, lexeme.location, std::string(text + 1, text + end)});
if (lexeme.type == Lexeme::Comment && lexeme.length && lexeme.data[0] == '!')
{
const char* text = lexeme.data;
unsigned int end = lexeme.length;
while (end > 0 && isSpace(text[end - 1]))
--end;
hotcomments.push_back({hotcommentHeader, lexeme.location, std::string(text + 1, text + end)});
}
type = lexer.next(/* skipComments= */ false).type;
}
}
else
{
while (true)
{
const Lexeme& lexeme = lexer.next(/*skipComments*/ false);
// Subtlety: Broken comments are weird because we record them as comments AND pass them to the parser as a lexeme.
// The parser will turn this into a proper syntax error.
if (lexeme.type == Lexeme::BrokenComment)
commentLocations.push_back(Comment{lexeme.type, lexeme.location});
if (isComment(lexeme))
commentLocations.push_back(Comment{lexeme.type, lexeme.location});
else
return;
} }
type = lexer.next(/* skipComments= */ false).type;
} }
} }
else else

View file

@ -4,8 +4,6 @@
#include "Luau/Bytecode.h" #include "Luau/Bytecode.h"
#include "Luau/Compiler.h" #include "Luau/Compiler.h"
LUAU_FASTFLAGVARIABLE(LuauCompileSelectBuiltin2, false)
namespace Luau namespace Luau
{ {
namespace Compile namespace Compile
@ -64,7 +62,7 @@ int getBuiltinFunctionId(const Builtin& builtin, const CompileOptions& options)
if (builtin.isGlobal("unpack")) if (builtin.isGlobal("unpack"))
return LBF_TABLE_UNPACK; return LBF_TABLE_UNPACK;
if (FFlag::LuauCompileSelectBuiltin2 && builtin.isGlobal("select")) if (builtin.isGlobal("select"))
return LBF_SELECT_VARARG; return LBF_SELECT_VARARG;
if (builtin.object == "math") if (builtin.object == "math")

View file

@ -15,8 +15,6 @@
#include <bitset> #include <bitset>
#include <math.h> #include <math.h>
LUAU_FASTFLAG(LuauCompileSelectBuiltin2)
namespace Luau namespace Luau
{ {
@ -265,7 +263,6 @@ struct Compiler
void compileExprSelectVararg(AstExprCall* expr, uint8_t target, uint8_t targetCount, bool targetTop, bool multRet, uint8_t regs) void compileExprSelectVararg(AstExprCall* expr, uint8_t target, uint8_t targetCount, bool targetTop, bool multRet, uint8_t regs)
{ {
LUAU_ASSERT(FFlag::LuauCompileSelectBuiltin2);
LUAU_ASSERT(targetCount == 1); LUAU_ASSERT(targetCount == 1);
LUAU_ASSERT(!expr->self); LUAU_ASSERT(!expr->self);
LUAU_ASSERT(expr->args.size == 2 && expr->args.data[1]->is<AstExprVarargs>()); LUAU_ASSERT(expr->args.size == 2 && expr->args.data[1]->is<AstExprVarargs>());
@ -407,7 +404,6 @@ struct Compiler
if (bfid == LBF_SELECT_VARARG) if (bfid == LBF_SELECT_VARARG)
{ {
LUAU_ASSERT(FFlag::LuauCompileSelectBuiltin2);
// Optimization: compile select(_, ...) as FASTCALL1; the builtin will read variadic arguments directly // Optimization: compile select(_, ...) as FASTCALL1; the builtin will read variadic arguments directly
// note: for now we restrict this to single-return expressions since our runtime code doesn't deal with general cases // note: for now we restrict this to single-return expressions since our runtime code doesn't deal with general cases
if (multRet == false && targetCount == 1 && expr->args.size == 2 && expr->args.data[1]->is<AstExprVarargs>()) if (multRet == false && targetCount == 1 && expr->args.size == 2 && expr->args.data[1]->is<AstExprVarargs>())

View file

@ -232,11 +232,18 @@ if(TARGET Luau.UnitTest)
tests/Transpiler.test.cpp tests/Transpiler.test.cpp
tests/TypeInfer.aliases.test.cpp tests/TypeInfer.aliases.test.cpp
tests/TypeInfer.annotations.test.cpp tests/TypeInfer.annotations.test.cpp
tests/TypeInfer.anyerror.test.cpp
tests/TypeInfer.builtins.test.cpp tests/TypeInfer.builtins.test.cpp
tests/TypeInfer.classes.test.cpp tests/TypeInfer.classes.test.cpp
tests/TypeInfer.definitions.test.cpp tests/TypeInfer.definitions.test.cpp
tests/TypeInfer.functions.test.cpp
tests/TypeInfer.generics.test.cpp tests/TypeInfer.generics.test.cpp
tests/TypeInfer.intersectionTypes.test.cpp tests/TypeInfer.intersectionTypes.test.cpp
tests/TypeInfer.loops.test.cpp
tests/TypeInfer.modules.test.cpp
tests/TypeInfer.oop.test.cpp
tests/TypeInfer.operators.test.cpp
tests/TypeInfer.primitives.test.cpp
tests/TypeInfer.provisional.test.cpp tests/TypeInfer.provisional.test.cpp
tests/TypeInfer.refinements.test.cpp tests/TypeInfer.refinements.test.cpp
tests/TypeInfer.singletons.test.cpp tests/TypeInfer.singletons.test.cpp

View file

@ -172,9 +172,12 @@ LUA_API const char* lua_pushvfstring(lua_State* L, const char* fmt, va_list argp
LUA_API LUA_PRINTF_ATTR(2, 3) const char* lua_pushfstringL(lua_State* L, const char* fmt, ...); LUA_API LUA_PRINTF_ATTR(2, 3) const char* lua_pushfstringL(lua_State* L, const char* fmt, ...);
LUA_API void lua_pushcclosurek(lua_State* L, lua_CFunction fn, const char* debugname, int nup, lua_Continuation cont); LUA_API void lua_pushcclosurek(lua_State* L, lua_CFunction fn, const char* debugname, int nup, lua_Continuation cont);
LUA_API void lua_pushboolean(lua_State* L, int b); LUA_API void lua_pushboolean(lua_State* L, int b);
LUA_API void lua_pushlightuserdata(lua_State* L, void* p);
LUA_API int lua_pushthread(lua_State* L); LUA_API int lua_pushthread(lua_State* L);
LUA_API void lua_pushlightuserdata(lua_State* L, void* p);
LUA_API void* lua_newuserdatatagged(lua_State* L, size_t sz, int tag);
LUA_API void* lua_newuserdatadtor(lua_State* L, size_t sz, void (*dtor)(void*));
/* /*
** get functions (Lua -> stack) ** get functions (Lua -> stack)
*/ */
@ -189,8 +192,6 @@ LUA_API void lua_setreadonly(lua_State* L, int idx, int enabled);
LUA_API int lua_getreadonly(lua_State* L, int idx); LUA_API int lua_getreadonly(lua_State* L, int idx);
LUA_API void lua_setsafeenv(lua_State* L, int idx, int enabled); LUA_API void lua_setsafeenv(lua_State* L, int idx, int enabled);
LUA_API void* lua_newuserdatatagged(lua_State* L, size_t sz, int tag);
LUA_API void* lua_newuserdatadtor(lua_State* L, size_t sz, void (*dtor)(void*));
LUA_API int lua_getmetatable(lua_State* L, int objindex); LUA_API int lua_getmetatable(lua_State* L, int objindex);
LUA_API void lua_getfenv(lua_State* L, int idx); LUA_API void lua_getfenv(lua_State* L, int idx);
@ -276,6 +277,14 @@ enum lua_GCOp
LUA_API int lua_gc(lua_State* L, int what, int data); LUA_API int lua_gc(lua_State* L, int what, int data);
/*
** memory statistics
** all allocated bytes are attributed to the memory category of the running thread (0..LUA_MEMORY_CATEGORIES-1)
*/
LUA_API void lua_setmemcat(lua_State* L, int category);
LUA_API size_t lua_totalbytes(lua_State* L, int category);
/* /*
** miscellaneous functions ** miscellaneous functions
*/ */

View file

@ -35,8 +35,8 @@ const char* luau_ident = "$Luau: Copyright (C) 2019-2022 Roblox Corporation $\n"
static Table* getcurrenv(lua_State* L) static Table* getcurrenv(lua_State* L)
{ {
if (L->ci == L->base_ci) /* no enclosing function? */ if (L->ci == L->base_ci) /* no enclosing function? */
return L->gt; /* use global table as environment */ return L->gt; /* use global table as environment */
else else
return curr_func(L)->env; return curr_func(L)->env;
} }
@ -1188,7 +1188,7 @@ void lua_concat(lua_State* L, int n)
void* lua_newuserdatatagged(lua_State* L, size_t sz, int tag) void* lua_newuserdatatagged(lua_State* L, size_t sz, int tag)
{ {
api_check(L, unsigned(tag) < LUA_UTAG_LIMIT); api_check(L, unsigned(tag) < LUA_UTAG_LIMIT || tag == UTAG_PROXY);
luaC_checkGC(L); luaC_checkGC(L);
luaC_checkthreadsleep(L); luaC_checkthreadsleep(L);
Udata* u = luaU_newudata(L, sz, tag); Udata* u = luaU_newudata(L, sz, tag);
@ -1317,7 +1317,7 @@ void lua_setuserdatadtor(lua_State* L, int tag, void (*dtor)(void*))
L->global->udatagc[tag] = dtor; L->global->udatagc[tag] = dtor;
} }
LUA_API void lua_clonefunction(lua_State* L, int idx) void lua_clonefunction(lua_State* L, int idx)
{ {
StkId p = index2addr(L, idx); StkId p = index2addr(L, idx);
api_check(L, isLfunction(p)); api_check(L, isLfunction(p));
@ -1333,3 +1333,15 @@ lua_Callbacks* lua_callbacks(lua_State* L)
{ {
return &L->global->cb; return &L->global->cb;
} }
void lua_setmemcat(lua_State* L, int category)
{
api_check(L, unsigned(category) < LUA_MEMORY_CATEGORIES);
L->activememcat = uint8_t(category);
}
size_t lua_totalbytes(lua_State* L, int category)
{
api_check(L, category < LUA_MEMORY_CATEGORIES);
return category < 0 ? L->global->totalbytes : L->global->memcatbytes[category];
}

View file

@ -5,6 +5,7 @@
#include "lstate.h" #include "lstate.h"
#include "lapi.h" #include "lapi.h"
#include "ldo.h" #include "ldo.h"
#include "ludata.h"
#include <ctype.h> #include <ctype.h>
#include <stdio.h> #include <stdio.h>
@ -190,6 +191,7 @@ static int luaB_type(lua_State* L)
luaL_checkany(L, 1); luaL_checkany(L, 1);
if (DFFlag::LuauMorePreciseLuaLTypeName) if (DFFlag::LuauMorePreciseLuaLTypeName)
{ {
/* resulting name doesn't differentiate between userdata types */
lua_pushstring(L, lua_typename(L, lua_type(L, 1))); lua_pushstring(L, lua_typename(L, lua_type(L, 1)));
} }
else else
@ -202,8 +204,16 @@ static int luaB_type(lua_State* L)
static int luaB_typeof(lua_State* L) static int luaB_typeof(lua_State* L)
{ {
luaL_checkany(L, 1); luaL_checkany(L, 1);
const TValue* obj = luaA_toobject(L, 1); if (DFFlag::LuauMorePreciseLuaLTypeName)
lua_pushstring(L, luaT_objtypename(L, obj)); {
/* resulting name returns __type if specified unless the input is a newproxy-created userdata */
lua_pushstring(L, luaL_typename(L, 1));
}
else
{
const TValue* obj = luaA_toobject(L, 1);
lua_pushstring(L, luaT_objtypename(L, obj));
}
return 1; return 1;
} }
@ -412,7 +422,7 @@ static int luaB_newproxy(lua_State* L)
bool needsmt = lua_toboolean(L, 1); bool needsmt = lua_toboolean(L, 1);
lua_newuserdata(L, 0); lua_newuserdatatagged(L, 0, UTAG_PROXY);
if (needsmt) if (needsmt)
{ {

View file

@ -9,9 +9,9 @@
/* /*
** Default settings for GC tunables (settable via lua_gc) ** Default settings for GC tunables (settable via lua_gc)
*/ */
#define LUAI_GCGOAL 200 /* 200% (allow heap to double compared to live heap size) */ #define LUAI_GCGOAL 200 /* 200% (allow heap to double compared to live heap size) */
#define LUAI_GCSTEPMUL 200 /* GC runs 'twice the speed' of memory allocation */ #define LUAI_GCSTEPMUL 200 /* GC runs 'twice the speed' of memory allocation */
#define LUAI_GCSTEPSIZE 1 /* GC runs every KB of memory allocation */ #define LUAI_GCSTEPSIZE 1 /* GC runs every KB of memory allocation */
/* /*
** Possible states of the Garbage Collector ** Possible states of the Garbage Collector

View file

@ -98,7 +98,7 @@
*/ */
#if defined(__APPLE__) #if defined(__APPLE__)
#define ABISWITCH(x64, ms32, gcc32) (sizeof(void*) == 8 ? x64 : gcc32) #define ABISWITCH(x64, ms32, gcc32) (sizeof(void*) == 8 ? x64 : gcc32)
#elif defined(__i386__) #elif defined(__i386__) && !defined(_MSC_VER)
#define ABISWITCH(x64, ms32, gcc32) (gcc32) #define ABISWITCH(x64, ms32, gcc32) (gcc32)
#else #else
// Android somehow uses a similar ABI to MSVC, *not* to iOS... // Android somehow uses a similar ABI to MSVC, *not* to iOS...

View file

@ -53,7 +53,7 @@ void luaS_resize(lua_State* L, int newsize)
{ {
TString* p = tb->hash[i]; TString* p = tb->hash[i];
while (p) while (p)
{ /* for each node in the list */ { /* for each node in the list */
TString* next = p->next; /* save next */ TString* next = p->next; /* save next */
unsigned int h = p->hash; unsigned int h = p->hash;
int h1 = lmod(h, newsize); /* new position */ int h1 = lmod(h, newsize); /* new position */

View file

@ -24,6 +24,8 @@
#include <string.h> #include <string.h>
LUAU_FASTFLAGVARIABLE(LuauTableRehashRework, false)
// max size of both array and hash part is 2^MAXBITS // max size of both array and hash part is 2^MAXBITS
#define MAXBITS 26 #define MAXBITS 26
#define MAXSIZE (1 << MAXBITS) #define MAXSIZE (1 << MAXBITS)
@ -351,6 +353,22 @@ static void setnodevector(lua_State* L, Table* t, int size)
t->lastfree = size; /* all positions are free */ t->lastfree = size; /* all positions are free */
} }
static TValue* newkey(lua_State* L, Table* t, const TValue* key);
static TValue* arrayornewkey(lua_State* L, Table* t, const TValue* key)
{
if (ttisnumber(key))
{
int k;
double n = nvalue(key);
luai_num2int(k, n);
if (luai_numeq(cast_num(k), n) && cast_to(unsigned int, k - 1) < cast_to(unsigned int, t->sizearray))
return &t->array[k - 1];
}
return newkey(L, t, key);
}
static void resize(lua_State* L, Table* t, int nasize, int nhsize) static void resize(lua_State* L, Table* t, int nasize, int nhsize)
{ {
if (nasize > MAXSIZE || nhsize > MAXSIZE) if (nasize > MAXSIZE || nhsize > MAXSIZE)
@ -369,22 +387,50 @@ static void resize(lua_State* L, Table* t, int nasize, int nhsize)
for (int i = nasize; i < oldasize; i++) for (int i = nasize; i < oldasize; i++)
{ {
if (!ttisnil(&t->array[i])) if (!ttisnil(&t->array[i]))
setobjt2t(L, luaH_setnum(L, t, i + 1), &t->array[i]); {
if (FFlag::LuauTableRehashRework)
{
TValue ok;
setnvalue(&ok, cast_num(i + 1));
setobjt2t(L, newkey(L, t, &ok), &t->array[i]);
}
else
{
setobjt2t(L, luaH_setnum(L, t, i + 1), &t->array[i]);
}
}
} }
/* shrink array */ /* shrink array */
luaM_reallocarray(L, t->array, oldasize, nasize, TValue, t->memcat); luaM_reallocarray(L, t->array, oldasize, nasize, TValue, t->memcat);
} }
/* re-insert elements from hash part */ /* re-insert elements from hash part */
for (int i = twoto(oldhsize) - 1; i >= 0; i--) if (FFlag::LuauTableRehashRework)
{ {
LuaNode* old = nold + i; for (int i = twoto(oldhsize) - 1; i >= 0; i--)
if (!ttisnil(gval(old)))
{ {
TValue ok; LuaNode* old = nold + i;
getnodekey(L, &ok, old); if (!ttisnil(gval(old)))
setobjt2t(L, luaH_set(L, t, &ok), gval(old)); {
TValue ok;
getnodekey(L, &ok, old);
setobjt2t(L, arrayornewkey(L, t, &ok), gval(old));
}
} }
} }
else
{
for (int i = twoto(oldhsize) - 1; i >= 0; i--)
{
LuaNode* old = nold + i;
if (!ttisnil(gval(old)))
{
TValue ok;
getnodekey(L, &ok, old);
setobjt2t(L, luaH_set(L, t, &ok), gval(old));
}
}
}
if (nold != dummynode) if (nold != dummynode)
luaM_freearray(L, nold, twoto(oldhsize), LuaNode, t->memcat); /* free old array */ luaM_freearray(L, nold, twoto(oldhsize), LuaNode, t->memcat); /* free old array */
} }
@ -482,7 +528,16 @@ static TValue* newkey(lua_State* L, Table* t, const TValue* key)
if (n == NULL) if (n == NULL)
{ /* cannot find a free place? */ { /* cannot find a free place? */
rehash(L, t, key); /* grow table */ rehash(L, t, key); /* grow table */
return luaH_set(L, t, key); /* re-insert key into grown table */
if (!FFlag::LuauTableRehashRework)
{
return luaH_set(L, t, key); /* re-insert key into grown table */
}
else
{
// after rehash, numeric keys might be located in the new array part, but won't be found in the node part
return arrayornewkey(L, t, key);
}
} }
LUAU_ASSERT(n != dummynode); LUAU_ASSERT(n != dummynode);
TValue mk; TValue mk;

View file

@ -4,6 +4,7 @@
#include "lstate.h" #include "lstate.h"
#include "lstring.h" #include "lstring.h"
#include "ludata.h"
#include "ltable.h" #include "ltable.h"
#include "lgc.h" #include "lgc.h"
@ -116,7 +117,7 @@ const TValue* luaT_gettmbyobj(lua_State* L, const TValue* o, TMS event)
const TString* luaT_objtypenamestr(lua_State* L, const TValue* o) const TString* luaT_objtypenamestr(lua_State* L, const TValue* o)
{ {
if (ttisuserdata(o) && uvalue(o)->tag && uvalue(o)->metatable) if (ttisuserdata(o) && uvalue(o)->tag != UTAG_PROXY && uvalue(o)->metatable)
{ {
const TValue* type = luaH_getstr(uvalue(o)->metatable, L->global->tmname[TM_TYPE]); const TValue* type = luaH_getstr(uvalue(o)->metatable, L->global->tmname[TM_TYPE]);

View file

@ -22,13 +22,11 @@ Udata* luaU_newudata(lua_State* L, size_t s, int tag)
void luaU_freeudata(lua_State* L, Udata* u, lua_Page* page) void luaU_freeudata(lua_State* L, Udata* u, lua_Page* page)
{ {
LUAU_ASSERT(u->tag < LUA_UTAG_LIMIT || u->tag == UTAG_IDTOR);
void (*dtor)(void*) = nullptr; void (*dtor)(void*) = nullptr;
if (u->tag == UTAG_IDTOR) if (u->tag < LUA_UTAG_LIMIT)
memcpy(&dtor, &u->data + u->len - sizeof(dtor), sizeof(dtor));
else if (u->tag)
dtor = L->global->udatagc[u->tag]; dtor = L->global->udatagc[u->tag];
else if (u->tag == UTAG_IDTOR)
memcpy(&dtor, &u->data + u->len - sizeof(dtor), sizeof(dtor));
if (dtor) if (dtor)
dtor(u->data); dtor(u->data);

View file

@ -7,6 +7,9 @@
/* special tag value is used for user data with inline dtors */ /* special tag value is used for user data with inline dtors */
#define UTAG_IDTOR LUA_UTAG_LIMIT #define UTAG_IDTOR LUA_UTAG_LIMIT
/* special tag value is used for newproxy-created user data (all other user data objects are host-exposed) */
#define UTAG_PROXY (LUA_UTAG_LIMIT + 1)
#define sizeudata(len) (offsetof(Udata, data) + len) #define sizeudata(len) (offsetof(Udata, data) + len)
LUAI_FUNC Udata* luaU_newudata(lua_State* L, size_t s, int tag); LUAI_FUNC Udata* luaU_newudata(lua_State* L, size_t s, int tag);

View file

@ -77,7 +77,7 @@
if (LUAU_UNLIKELY(!!interrupt)) \ if (LUAU_UNLIKELY(!!interrupt)) \
{ /* the interrupt hook is called right before we advance pc */ \ { /* the interrupt hook is called right before we advance pc */ \
VM_PROTECT(L->ci->savedpc++; interrupt(L, -1)); \ VM_PROTECT(L->ci->savedpc++; interrupt(L, -1)); \
if (L->status != 0) \ if (L->status != 0) \
{ \ { \
L->ci->savedpc--; \ L->ci->savedpc--; \
goto exit; \ goto exit; \

View file

@ -2755,4 +2755,166 @@ local abc = b@1
CHECK(ac.entryMap["bar"].parens == ParenthesesRecommendation::CursorInside); CHECK(ac.entryMap["bar"].parens == ParenthesesRecommendation::CursorInside);
} }
TEST_CASE_FIXTURE(ACFixture, "no_incompatible_self_calls_on_class")
{
ScopedFastFlag selfCallAutocompleteFix{"LuauSelfCallAutocompleteFix", true};
loadDefinition(R"(
declare class Foo
function one(self): number
two: () -> number
end
)");
{
check(R"(
local t: Foo
t:@1
)");
auto ac = autocomplete('1');
REQUIRE(ac.entryMap.count("one"));
REQUIRE(ac.entryMap.count("two"));
CHECK(!ac.entryMap["one"].wrongIndexType);
CHECK(ac.entryMap["two"].wrongIndexType);
}
{
check(R"(
local t: Foo
t.@1
)");
auto ac = autocomplete('1');
REQUIRE(ac.entryMap.count("one"));
REQUIRE(ac.entryMap.count("two"));
CHECK(ac.entryMap["one"].wrongIndexType);
CHECK(!ac.entryMap["two"].wrongIndexType);
}
}
TEST_CASE_FIXTURE(ACFixture, "no_incompatible_self_calls")
{
ScopedFastFlag selfCallAutocompleteFix{"LuauSelfCallAutocompleteFix", true};
check(R"(
local t = {}
function t.m() end
t:@1
)");
auto ac = autocomplete('1');
REQUIRE(ac.entryMap.count("m"));
CHECK(ac.entryMap["m"].wrongIndexType);
}
TEST_CASE_FIXTURE(ACFixture, "no_incompatible_self_calls_2")
{
ScopedFastFlag selfCallAutocompleteFix{"LuauSelfCallAutocompleteFix", true};
check(R"(
local f: (() -> number) & ((number) -> number) = function(x: number?) return 2 end
local t = {}
t.f = f
t:@1
)");
auto ac = autocomplete('1');
REQUIRE(ac.entryMap.count("f"));
CHECK(ac.entryMap["f"].wrongIndexType);
}
TEST_CASE_FIXTURE(ACFixture, "no_incompatible_self_calls_provisional")
{
check(R"(
local t = {}
function t.m(x: typeof(t)) end
t:@1
)");
auto ac = autocomplete('1');
REQUIRE(ac.entryMap.count("m"));
// We can make changes to mark this as a wrong way to call even though it's compatible
CHECK(!ac.entryMap["m"].wrongIndexType);
}
TEST_CASE_FIXTURE(ACFixture, "string_prim_self_calls_are_fine")
{
ScopedFastFlag selfCallAutocompleteFix{"LuauSelfCallAutocompleteFix", true};
check(R"(
local s = "hello"
s:@1
)");
auto ac = autocomplete('1');
REQUIRE(ac.entryMap.count("byte"));
CHECK(ac.entryMap["byte"].wrongIndexType == false);
REQUIRE(ac.entryMap.count("char"));
CHECK(ac.entryMap["char"].wrongIndexType == true);
REQUIRE(ac.entryMap.count("sub"));
CHECK(ac.entryMap["sub"].wrongIndexType == false);
}
TEST_CASE_FIXTURE(ACFixture, "string_prim_non_self_calls_are_avoided")
{
ScopedFastFlag selfCallAutocompleteFix{"LuauSelfCallAutocompleteFix", true};
check(R"(
local s = "hello"
s.@1
)");
auto ac = autocomplete('1');
REQUIRE(ac.entryMap.count("byte"));
CHECK(ac.entryMap["byte"].wrongIndexType == true);
REQUIRE(ac.entryMap.count("char"));
CHECK(ac.entryMap["char"].wrongIndexType == false);
REQUIRE(ac.entryMap.count("sub"));
CHECK(ac.entryMap["sub"].wrongIndexType == true);
}
TEST_CASE_FIXTURE(ACFixture, "string_library_non_self_calls_are_fine")
{
ScopedFastFlag selfCallAutocompleteFix{"LuauSelfCallAutocompleteFix", true};
check(R"(
string.@1
)");
auto ac = autocomplete('1');
REQUIRE(ac.entryMap.count("byte"));
CHECK(ac.entryMap["byte"].wrongIndexType == false);
REQUIRE(ac.entryMap.count("char"));
CHECK(ac.entryMap["char"].wrongIndexType == false);
REQUIRE(ac.entryMap.count("sub"));
CHECK(ac.entryMap["sub"].wrongIndexType == false);
}
TEST_CASE_FIXTURE(ACFixture, "string_library_self_calls_are_invalid")
{
ScopedFastFlag selfCallAutocompleteFix{"LuauSelfCallAutocompleteFix", true};
check(R"(
string:@1
)");
auto ac = autocomplete('1');
REQUIRE(ac.entryMap.count("byte"));
CHECK(ac.entryMap["byte"].wrongIndexType == true);
REQUIRE(ac.entryMap.count("char"));
CHECK(ac.entryMap["char"].wrongIndexType == true);
REQUIRE(ac.entryMap.count("sub"));
CHECK(ac.entryMap["sub"].wrongIndexType == true);
}
TEST_SUITE_END(); TEST_SUITE_END();

View file

@ -2819,8 +2819,6 @@ RETURN R1 -1
TEST_CASE("FastcallSelect") TEST_CASE("FastcallSelect")
{ {
ScopedFastFlag sff("LuauCompileSelectBuiltin2", true);
// select(_, ...) compiles to a builtin call // select(_, ...) compiles to a builtin call
CHECK_EQ("\n" + compileFunction0("return (select('#', ...))"), R"( CHECK_EQ("\n" + compileFunction0("return (select('#', ...))"), R"(
LOADK R1 K0 LOADK R1 K0

View file

@ -569,6 +569,11 @@ TEST_CASE("Debugger")
CHECK(lua_tointeger(L, -1) == 50); CHECK(lua_tointeger(L, -1) == 50);
lua_pop(L, 1); lua_pop(L, 1);
int v = lua_getargument(L, 0, 2);
REQUIRE(v);
CHECK(lua_tointeger(L, -1) == 42);
lua_pop(L, 1);
// test lua_getlocal // test lua_getlocal
const char* l = lua_getlocal(L, 0, 1); const char* l = lua_getlocal(L, 0, 1);
REQUIRE(l); REQUIRE(l);
@ -652,31 +657,6 @@ TEST_CASE("SameHash")
CHECK(luaS_hash(buf + 1, 120) == luaS_hash(buf + 2, 120)); CHECK(luaS_hash(buf + 1, 120) == luaS_hash(buf + 2, 120));
} }
TEST_CASE("InlineDtor")
{
static int dtorhits = 0;
dtorhits = 0;
{
StateRef globalState(luaL_newstate(), lua_close);
lua_State* L = globalState.get();
void* u1 = lua_newuserdatadtor(L, 4, [](void* data) {
dtorhits += *(int*)data;
});
void* u2 = lua_newuserdatadtor(L, 1, [](void* data) {
dtorhits += *(char*)data;
});
*(int*)u1 = 39;
*(char*)u2 = 3;
}
CHECK(dtorhits == 42);
}
TEST_CASE("Reference") TEST_CASE("Reference")
{ {
static int dtorhits = 0; static int dtorhits = 0;
@ -969,7 +949,7 @@ TEST_CASE("StringConversion")
TEST_CASE("GCDump") TEST_CASE("GCDump")
{ {
// internal function, declared in lgc.h - not exposed via lua.h // internal function, declared in lgc.h - not exposed via lua.h
extern void luaC_dump(lua_State* L, void* file, const char* (*categoryName)(lua_State* L, uint8_t memcat)); extern void luaC_dump(lua_State * L, void* file, const char* (*categoryName)(lua_State * L, uint8_t memcat));
StateRef globalState(luaL_newstate(), lua_close); StateRef globalState(luaL_newstate(), lua_close);
lua_State* L = globalState.get(); lua_State* L = globalState.get();
@ -1015,4 +995,114 @@ TEST_CASE("GCDump")
fclose(f); fclose(f);
} }
TEST_CASE("Interrupt")
{
static const int expectedhits[] = {
2,
9,
5,
5,
5,
5,
5,
5,
5,
5,
5,
5,
5,
6,
11,
};
static int index;
index = 0;
runConformance(
"interrupt.lua",
[](lua_State* L) {
auto* cb = lua_callbacks(L);
// note: for simplicity here we setup the interrupt callback once
// however, this carries a noticeable performance cost. in a real application,
// it's advised to set interrupt callback on a timer from a different thread,
// and set it back to nullptr once the interrupt triggered.
cb->interrupt = [](lua_State* L, int gc) {
if (gc >= 0)
return;
CHECK(index < int(std::size(expectedhits)));
lua_Debug ar = {};
lua_getinfo(L, 0, "l", &ar);
CHECK(ar.currentline == expectedhits[index]);
index++;
// check that we can yield inside an interrupt
if (index == 5)
lua_yield(L, 0);
};
},
[](lua_State* L) {
CHECK(index == 5); // a single yield point
});
CHECK(index == int(std::size(expectedhits)));
}
TEST_CASE("UserdataApi")
{
static int dtorhits = 0;
dtorhits = 0;
StateRef globalState(luaL_newstate(), lua_close);
lua_State* L = globalState.get();
// setup dtor for tag 42 (created later)
lua_setuserdatadtor(L, 42, [](void* data) {
dtorhits += *(int*)data;
});
// light user data
int lud;
lua_pushlightuserdata(L, &lud);
CHECK(lua_touserdata(L, -1) == &lud);
CHECK(lua_topointer(L, -1) == &lud);
// regular user data
int* ud1 = (int*)lua_newuserdata(L, 4);
*ud1 = 42;
CHECK(lua_touserdata(L, -1) == ud1);
CHECK(lua_topointer(L, -1) == ud1);
// tagged user data
int* ud2 = (int*)lua_newuserdatatagged(L, 4, 42);
*ud2 = -4;
CHECK(lua_touserdatatagged(L, -1, 42) == ud2);
CHECK(lua_touserdatatagged(L, -1, 41) == nullptr);
CHECK(lua_userdatatag(L, -1) == 42);
// user data with inline dtor
void* ud3 = lua_newuserdatadtor(L, 4, [](void* data) {
dtorhits += *(int*)data;
});
void* ud4 = lua_newuserdatadtor(L, 1, [](void* data) {
dtorhits += *(char*)data;
});
*(int*)ud3 = 43;
*(char*)ud4 = 3;
globalState.reset();
CHECK(dtorhits == 42);
}
TEST_SUITE_END(); TEST_SUITE_END();

View file

@ -1667,8 +1667,6 @@ _ = (math.random() < 0.5 and false) or 42 -- currently ignored
TEST_CASE_FIXTURE(Fixture, "WrongComment") TEST_CASE_FIXTURE(Fixture, "WrongComment")
{ {
ScopedFastFlag sff("LuauParseAllHotComments", true);
LintResult result = lint(R"( LintResult result = lint(R"(
--!strict --!strict
--!struct --!struct

View file

@ -248,10 +248,12 @@ TEST_CASE_FIXTURE(Fixture, "clone_seal_free_tables")
TEST_CASE_FIXTURE(Fixture, "clone_self_property") TEST_CASE_FIXTURE(Fixture, "clone_self_property")
{ {
ScopedFastFlag sff{"LuauAnyInIsOptionalIsOptional", true};
fileResolver.source["Module/A"] = R"( fileResolver.source["Module/A"] = R"(
--!nonstrict --!nonstrict
local a = {} local a = {}
function a:foo(x) function a:foo(x: number)
return -x; return -x;
end end
return a; return a;
@ -267,10 +269,10 @@ TEST_CASE_FIXTURE(Fixture, "clone_self_property")
)"; )";
result = frontend.check("Module/B"); result = frontend.check("Module/B");
LUAU_REQUIRE_ERRORS(result);
CHECK_EQ(toString(result.errors[0]), "This function was declared to accept self, but you did not pass enough arguments. Use a colon instead of a " LUAU_REQUIRE_ERROR_COUNT(1, result);
"dot or pass 1 extra nil to suppress this warning");
CHECK_EQ("This function must be called with self. Did you mean to use a colon instead of a dot?", toString(result.errors[0]));
} }
TEST_CASE_FIXTURE(Fixture, "clone_recursion_limit") TEST_CASE_FIXTURE(Fixture, "clone_recursion_limit")

View file

@ -126,6 +126,8 @@ TEST_CASE_FIXTURE(Fixture, "parameters_having_type_any_are_optional")
TEST_CASE_FIXTURE(Fixture, "local_tables_are_not_any") TEST_CASE_FIXTURE(Fixture, "local_tables_are_not_any")
{ {
ScopedFastFlag sff{"LuauAnyInIsOptionalIsOptional", true};
CheckResult result = check(R"( CheckResult result = check(R"(
--!nonstrict --!nonstrict
local T = {} local T = {}
@ -136,31 +138,25 @@ TEST_CASE_FIXTURE(Fixture, "local_tables_are_not_any")
T:staticmethod() T:staticmethod()
)"); )");
LUAU_REQUIRE_ERROR_COUNT(2, result); LUAU_REQUIRE_ERROR_COUNT(1, result);
CHECK(std::any_of(result.errors.begin(), result.errors.end(), [](const TypeError& e) { CHECK_EQ("This function does not take self. Did you mean to use a dot instead of a colon?", toString(result.errors[0]));
return get<FunctionDoesNotTakeSelf>(e);
}));
CHECK(std::any_of(result.errors.begin(), result.errors.end(), [](const TypeError& e) {
return get<FunctionRequiresSelf>(e);
}));
} }
TEST_CASE_FIXTURE(Fixture, "offer_a_hint_if_you_use_a_dot_instead_of_a_colon") TEST_CASE_FIXTURE(Fixture, "offer_a_hint_if_you_use_a_dot_instead_of_a_colon")
{ {
ScopedFastFlag sff{"LuauAnyInIsOptionalIsOptional", true};
CheckResult result = check(R"( CheckResult result = check(R"(
--!nonstrict --!nonstrict
local T = {} local T = {}
function T:method() end function T:method(x: number) end
T.method() T.method(5)
)"); )");
LUAU_REQUIRE_ERROR_COUNT(1, result); LUAU_REQUIRE_ERROR_COUNT(1, result);
auto e = get<FunctionRequiresSelf>(result.errors[0]); CHECK_EQ("This function must be called with self. Did you mean to use a colon instead of a dot?", toString(result.errors[0]));
REQUIRE(e != nullptr);
REQUIRE_EQ(1, e->requiredExtraNils);
} }
TEST_CASE_FIXTURE(Fixture, "table_props_are_any") TEST_CASE_FIXTURE(Fixture, "table_props_are_any")

View file

@ -676,4 +676,25 @@ TEST_CASE_FIXTURE(Fixture, "forward_declared_alias_is_not_clobbered_by_prior_uni
LUAU_REQUIRE_NO_ERRORS(result); LUAU_REQUIRE_NO_ERRORS(result);
} }
TEST_CASE_FIXTURE(Fixture, "recursive_types_restriction_ok")
{
CheckResult result = check(R"(
type Tree<T> = { data: T, children: {Tree<T>} }
)");
LUAU_REQUIRE_NO_ERRORS(result);
}
TEST_CASE_FIXTURE(Fixture, "recursive_types_restriction_not_ok")
{
ScopedFastFlag sff{"LuauRecursiveTypeParameterRestriction", true};
CheckResult result = check(R"(
-- this would be an infinite type if we allowed it
type Tree<T> = { data: T, children: {Tree<{T}>} }
)");
LUAU_REQUIRE_ERRORS(result);
}
TEST_SUITE_END(); TEST_SUITE_END();

View file

@ -0,0 +1,335 @@
// This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details
#include "Luau/AstQuery.h"
#include "Luau/BuiltinDefinitions.h"
#include "Luau/Scope.h"
#include "Luau/TypeInfer.h"
#include "Luau/TypeVar.h"
#include "Luau/VisitTypeVar.h"
#include "Fixture.h"
#include "doctest.h"
using namespace Luau;
TEST_SUITE_BEGIN("TypeInferAnyError");
TEST_CASE_FIXTURE(Fixture, "for_in_loop_iterator_returns_any")
{
CheckResult result = check(R"(
function bar(): any
return true
end
local a
for b in bar do
a = b
end
)");
LUAU_REQUIRE_NO_ERRORS(result);
CHECK_EQ(typeChecker.anyType, requireType("a"));
}
TEST_CASE_FIXTURE(Fixture, "for_in_loop_iterator_returns_any2")
{
CheckResult result = check(R"(
function bar(): any
return true
end
local a
for b in bar() do
a = b
end
)");
LUAU_REQUIRE_NO_ERRORS(result);
CHECK_EQ("any", toString(requireType("a")));
}
TEST_CASE_FIXTURE(Fixture, "for_in_loop_iterator_is_any")
{
CheckResult result = check(R"(
local bar: any
local a
for b in bar do
a = b
end
)");
LUAU_REQUIRE_NO_ERRORS(result);
CHECK_EQ("any", toString(requireType("a")));
}
TEST_CASE_FIXTURE(Fixture, "for_in_loop_iterator_is_any2")
{
CheckResult result = check(R"(
local bar: any
local a
for b in bar() do
a = b
end
)");
LUAU_REQUIRE_NO_ERRORS(result);
CHECK_EQ("any", toString(requireType("a")));
}
TEST_CASE_FIXTURE(Fixture, "for_in_loop_iterator_is_error")
{
CheckResult result = check(R"(
local a
for b in bar do
a = b
end
)");
LUAU_REQUIRE_ERROR_COUNT(1, result);
CHECK_EQ("*unknown*", toString(requireType("a")));
}
TEST_CASE_FIXTURE(Fixture, "for_in_loop_iterator_is_error2")
{
CheckResult result = check(R"(
function bar(c) return c end
local a
for b in bar() do
a = b
end
)");
LUAU_REQUIRE_ERROR_COUNT(1, result);
CHECK_EQ("*unknown*", toString(requireType("a")));
}
TEST_CASE_FIXTURE(Fixture, "length_of_error_type_does_not_produce_an_error")
{
CheckResult result = check(R"(
local l = #this_is_not_defined
)");
LUAU_REQUIRE_ERROR_COUNT(1, result);
}
TEST_CASE_FIXTURE(Fixture, "indexing_error_type_does_not_produce_an_error")
{
CheckResult result = check(R"(
local originalReward = unknown.Parent.Reward:GetChildren()[1]
)");
LUAU_REQUIRE_ERROR_COUNT(1, result);
}
TEST_CASE_FIXTURE(Fixture, "dot_on_error_type_does_not_produce_an_error")
{
CheckResult result = check(R"(
local foo = (true).x
foo.x = foo.y
)");
LUAU_REQUIRE_ERROR_COUNT(1, result);
}
TEST_CASE_FIXTURE(Fixture, "any_type_propagates")
{
CheckResult result = check(R"(
local foo: any
local bar = foo:method("argument")
)");
LUAU_REQUIRE_NO_ERRORS(result);
CHECK_EQ("any", toString(requireType("bar")));
}
TEST_CASE_FIXTURE(Fixture, "can_subscript_any")
{
CheckResult result = check(R"(
local foo: any
local bar = foo[5]
)");
LUAU_REQUIRE_NO_ERRORS(result);
CHECK_EQ("any", toString(requireType("bar")));
}
// Not strictly correct: metatables permit overriding this
TEST_CASE_FIXTURE(Fixture, "can_get_length_of_any")
{
CheckResult result = check(R"(
local foo: any = {}
local bar = #foo
)");
LUAU_REQUIRE_NO_ERRORS(result);
CHECK_EQ(PrimitiveTypeVar::Number, getPrimitiveType(requireType("bar")));
}
TEST_CASE_FIXTURE(Fixture, "assign_prop_to_table_by_calling_any_yields_any")
{
CheckResult result = check(R"(
local f: any
local T = {}
T.prop = f()
return T
)");
LUAU_REQUIRE_NO_ERRORS(result);
TableTypeVar* ttv = getMutable<TableTypeVar>(requireType("T"));
REQUIRE(ttv);
REQUIRE(ttv->props.count("prop"));
REQUIRE_EQ("any", toString(ttv->props["prop"].type));
}
TEST_CASE_FIXTURE(Fixture, "quantify_any_does_not_bind_to_itself")
{
CheckResult result = check(R"(
local A : any
function A.B() end
A:C()
)");
LUAU_REQUIRE_NO_ERRORS(result);
TypeId aType = requireType("A");
CHECK_EQ(aType, typeChecker.anyType);
}
TEST_CASE_FIXTURE(Fixture, "calling_error_type_yields_error")
{
CheckResult result = check(R"(
local a = unknown.Parent.Reward.GetChildren()
)");
LUAU_REQUIRE_ERROR_COUNT(1, result);
UnknownSymbol* err = get<UnknownSymbol>(result.errors[0]);
REQUIRE(err != nullptr);
CHECK_EQ("unknown", err->name);
CHECK_EQ("*unknown*", toString(requireType("a")));
}
TEST_CASE_FIXTURE(Fixture, "chain_calling_error_type_yields_error")
{
CheckResult result = check(R"(
local a = Utility.Create "Foo" {}
)");
CHECK_EQ("*unknown*", toString(requireType("a")));
}
TEST_CASE_FIXTURE(Fixture, "replace_every_free_type_when_unifying_a_complex_function_with_any")
{
CheckResult result = check(R"(
local a: any
local b
for _, i in pairs(a) do
b = i
end
)");
LUAU_REQUIRE_NO_ERRORS(result);
CHECK_EQ("any", toString(requireType("b")));
}
TEST_CASE_FIXTURE(Fixture, "call_to_any_yields_any")
{
CheckResult result = check(R"(
local a: any
local b = a()
)");
REQUIRE_EQ("any", toString(requireType("b")));
}
TEST_CASE_FIXTURE(Fixture, "CheckMethodsOfAny")
{
CheckResult result = check(R"(
local x: any = {}
function x:y(z: number)
local s: string = z
end
)");
LUAU_REQUIRE_ERROR_COUNT(1, result);
}
TEST_CASE_FIXTURE(Fixture, "CheckMethodsOfError")
{
CheckResult result = check(R"(
local x = (true).foo
function x:y(z: number)
local s: string = z
end
)");
LUAU_REQUIRE_ERRORS(result);
}
TEST_CASE_FIXTURE(Fixture, "metatable_of_any_can_be_a_table")
{
CheckResult result = check(R"(
--!strict
local T: any
T = {}
T.__index = T
function T.new(...)
local self = {}
setmetatable(self, T)
self:construct(...)
return self
end
function T:construct(index)
end
)");
LUAU_REQUIRE_NO_ERRORS(result);
}
TEST_CASE_FIXTURE(Fixture, "type_error_addition")
{
CheckResult result = check(R"(
--!strict
local foo = makesandwich()
local bar = foo.nutrition + 100
)");
LUAU_REQUIRE_ERROR_COUNT(1, result);
// We should definitely get this error
CHECK_EQ("Unknown global 'makesandwich'", toString(result.errors[0]));
// We get this error if makesandwich() returns a free type
// CHECK_EQ("Unknown type used in + operation; consider adding a type annotation to 'foo'", toString(result.errors[1]));
}
TEST_CASE_FIXTURE(Fixture, "prop_access_on_any_with_other_options")
{
CheckResult result = check(R"(
local function f(thing: any | string)
local foo = thing.SomeRandomKey
end
)");
LUAU_REQUIRE_NO_ERRORS(result);
}
TEST_SUITE_END();

View file

@ -871,6 +871,7 @@ TEST_CASE_FIXTURE(Fixture, "assert_removes_falsy_types")
ScopedFastFlag sff[]{ ScopedFastFlag sff[]{
{"LuauAssertStripsFalsyTypes", true}, {"LuauAssertStripsFalsyTypes", true},
{"LuauDiscriminableUnions2", true}, {"LuauDiscriminableUnions2", true},
{"LuauWidenIfSupertypeIsFree2", true},
}; };
CheckResult result = check(R"( CheckResult result = check(R"(
@ -879,6 +880,26 @@ TEST_CASE_FIXTURE(Fixture, "assert_removes_falsy_types")
end end
)"); )");
LUAU_REQUIRE_NO_ERRORS(result);
CHECK_EQ("((boolean | number)?) -> boolean | number", toString(requireType("f")));
}
TEST_CASE_FIXTURE(Fixture, "assert_removes_falsy_types2")
{
ScopedFastFlag sff[]{
{"LuauParseSingletonTypes", true},
{"LuauSingletonTypes", true},
{"LuauAssertStripsFalsyTypes", true},
{"LuauDiscriminableUnions2", true},
{"LuauWidenIfSupertypeIsFree2", true},
};
CheckResult result = check(R"(
local function f(x: (number | boolean)?): number | true
return assert(x)
end
)");
LUAU_REQUIRE_NO_ERRORS(result); LUAU_REQUIRE_NO_ERRORS(result);
CHECK_EQ("((boolean | number)?) -> number | true", toString(requireType("f"))); CHECK_EQ("((boolean | number)?) -> number | true", toString(requireType("f")));
} }
@ -958,4 +979,43 @@ a:b({})
CHECK_EQ(result.errors[1], (TypeError{Location{{3, 0}, {3, 5}}, CountMismatch{2, 1}})); CHECK_EQ(result.errors[1], (TypeError{Location{{3, 0}, {3, 5}}, CountMismatch{2, 1}}));
} }
TEST_CASE_FIXTURE(Fixture, "typeof_unresolved_function")
{
CheckResult result = check(R"(
local function f(a: typeof(f)) end
)");
LUAU_REQUIRE_ERROR_COUNT(1, result);
CHECK_EQ("Unknown global 'f'", toString(result.errors[0]));
}
TEST_CASE_FIXTURE(Fixture, "no_persistent_typelevel_change")
{
TypeId mathTy = requireType(typeChecker.globalScope, "math");
REQUIRE(mathTy);
TableTypeVar* ttv = getMutable<TableTypeVar>(mathTy);
REQUIRE(ttv);
const FunctionTypeVar* ftv = get<FunctionTypeVar>(ttv->props["frexp"].type);
REQUIRE(ftv);
auto original = ftv->level;
CheckResult result = check("local a = math.frexp");
LUAU_REQUIRE_NO_ERRORS(result);
CHECK(ftv->level.level == original.level);
CHECK(ftv->level.subLevel == original.subLevel);
}
TEST_CASE_FIXTURE(Fixture, "global_singleton_types_are_sealed")
{
CheckResult result = check(R"(
local function f(x: string)
local p = x:split('a')
p = table.pack(table.unpack(p, 1, #p - 1))
return p
end
)");
LUAU_REQUIRE_NO_ERRORS(result);
}
TEST_SUITE_END(); TEST_SUITE_END();

View file

@ -293,4 +293,22 @@ TEST_CASE_FIXTURE(Fixture, "documentation_symbols_dont_attach_to_persistent_type
CHECK_EQ(ty->type->documentationSymbol, std::nullopt); CHECK_EQ(ty->type->documentationSymbol, std::nullopt);
} }
TEST_CASE_FIXTURE(Fixture, "single_class_type_identity_in_global_types")
{
ScopedFastFlag luauCloneDeclaredGlobals{"LuauCloneDeclaredGlobals", true};
loadDefinition(R"(
declare class Cls
end
declare GetCls: () -> (Cls)
)");
CheckResult result = check(R"(
local s : Cls = GetCls()
)");
LUAU_REQUIRE_NO_ERRORS(result);
}
TEST_SUITE_END(); TEST_SUITE_END();

File diff suppressed because it is too large Load diff

View file

@ -1,6 +1,9 @@
// This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details // This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details
#include "Luau/TypeInfer.h" #include "Luau/TypeInfer.h"
#include "Luau/TypeVar.h" #include "Luau/TypeVar.h"
#include "Luau/Scope.h"
#include <algorithm>
#include "Fixture.h" #include "Fixture.h"
@ -830,5 +833,303 @@ wrapper(test2, 1, "", 3)
CHECK_EQ(toString(result.errors[0]), R"(Argument count mismatch. Function expects 1 argument, but 4 are specified)"); CHECK_EQ(toString(result.errors[0]), R"(Argument count mismatch. Function expects 1 argument, but 4 are specified)");
} }
TEST_CASE_FIXTURE(Fixture, "generic_function")
{
CheckResult result = check(R"(
function id(x) return x end
local a = id(55)
local b = id(nil)
)");
LUAU_REQUIRE_NO_ERRORS(result);
CHECK_EQ(*typeChecker.numberType, *requireType("a"));
CHECK_EQ(*typeChecker.nilType, *requireType("b"));
}
TEST_CASE_FIXTURE(Fixture, "generic_table_method")
{
CheckResult result = check(R"(
local T = {}
function T:bar(i)
return i
end
)");
LUAU_REQUIRE_NO_ERRORS(result);
TypeId tType = requireType("T");
TableTypeVar* tTable = getMutable<TableTypeVar>(tType);
REQUIRE(tTable != nullptr);
TypeId barType = tTable->props["bar"].type;
REQUIRE(barType != nullptr);
const FunctionTypeVar* ftv = get<FunctionTypeVar>(follow(barType));
REQUIRE_MESSAGE(ftv != nullptr, "Should be a function: " << *barType);
std::vector<TypeId> args = flatten(ftv->argTypes).first;
TypeId argType = args.at(1);
CHECK_MESSAGE(get<Unifiable::Generic>(argType), "Should be generic: " << *barType);
}
TEST_CASE_FIXTURE(Fixture, "correctly_instantiate_polymorphic_member_functions")
{
CheckResult result = check(R"(
local T = {}
function T:foo()
return T:bar(5)
end
function T:bar(i)
return i
end
)");
LUAU_REQUIRE_NO_ERRORS(result);
dumpErrors(result);
const TableTypeVar* t = get<TableTypeVar>(requireType("T"));
REQUIRE(t != nullptr);
std::optional<Property> fooProp = get(t->props, "foo");
REQUIRE(bool(fooProp));
const FunctionTypeVar* foo = get<FunctionTypeVar>(follow(fooProp->type));
REQUIRE(bool(foo));
std::optional<TypeId> ret_ = first(foo->retType);
REQUIRE(bool(ret_));
TypeId ret = follow(*ret_);
REQUIRE_EQ(getPrimitiveType(ret), PrimitiveTypeVar::Number);
}
/*
* We had a bug in instantiation where the argument types of 'f' and 'g' would be inferred as
* f {+ method: function(<CYCLE>): (t2, T3...) +}
* g {+ method: function({+ method: function(<CYCLE>): (t2, T3...) +}): (t5, T6...) +}
*
* The type of 'g' is totally wrong as t2 and t5 should be unified, as should T3 with T6.
*
* The correct unification of the argument to 'g' is
*
* {+ method: function(<CYCLE>): (t5, T6...) +}
*/
TEST_CASE_FIXTURE(Fixture, "instantiate_cyclic_generic_function")
{
auto result = check(R"(
function f(o)
o:method()
end
function g(o)
f(o)
end
)");
TypeId g = requireType("g");
const FunctionTypeVar* gFun = get<FunctionTypeVar>(g);
REQUIRE(gFun != nullptr);
auto optionArg = first(gFun->argTypes);
REQUIRE(bool(optionArg));
TypeId arg = follow(*optionArg);
const TableTypeVar* argTable = get<TableTypeVar>(arg);
REQUIRE(argTable != nullptr);
std::optional<Property> methodProp = get(argTable->props, "method");
REQUIRE(bool(methodProp));
const FunctionTypeVar* methodFunction = get<FunctionTypeVar>(methodProp->type);
REQUIRE(methodFunction != nullptr);
std::optional<TypeId> methodArg = first(methodFunction->argTypes);
REQUIRE(bool(methodArg));
REQUIRE_EQ(follow(*methodArg), follow(arg));
}
TEST_CASE_FIXTURE(Fixture, "instantiate_generic_function_in_assignments")
{
CheckResult result = check(R"(
function foo(a, b)
return a(b)
end
function bar()
local c: ((number)->number, number)->number = foo -- no error
c = foo -- no error
local d: ((number)->number, string)->number = foo -- error from arg 2 (string) not being convertable to number from the call a(b)
end
)");
LUAU_REQUIRE_ERROR_COUNT(1, result);
TypeMismatch* tm = get<TypeMismatch>(result.errors[0]);
REQUIRE(tm);
CHECK_EQ("((number) -> number, string) -> number", toString(tm->wantedType));
CHECK_EQ("((number) -> number, number) -> number", toString(tm->givenType));
}
TEST_CASE_FIXTURE(Fixture, "instantiate_generic_function_in_assignments2")
{
CheckResult result = check(R"(
function foo(a, b)
return a(b)
end
function bar()
local _: (string, string)->number = foo -- string cannot be converted to (string)->number
end
)");
LUAU_REQUIRE_ERROR_COUNT(1, result);
TypeMismatch* tm = get<TypeMismatch>(result.errors[0]);
REQUIRE(tm);
CHECK_EQ("(string, string) -> number", toString(tm->wantedType));
CHECK_EQ("((string) -> number, string) -> number", toString(*tm->givenType));
}
TEST_CASE_FIXTURE(Fixture, "self_recursive_instantiated_param")
{
ScopedFastFlag sff{"LuauOnlyMutateInstantiatedTables", true};
// Mutability in type function application right now can create strange recursive types
CheckResult result = check(R"(
type Table = { a: number }
type Self<T> = T
local a: Self<Table>
)");
LUAU_REQUIRE_NO_ERRORS(result);
CHECK_EQ(toString(requireType("a")), "Table");
}
TEST_CASE_FIXTURE(Fixture, "no_stack_overflow_from_quantifying")
{
CheckResult result = check(R"(
function _(l0:t0): (any, ()->())
end
type t0 = t0 | {}
)");
CHECK_LE(0, result.errors.size());
std::optional<TypeFun> t0 = getMainModule()->getModuleScope()->lookupType("t0");
REQUIRE(t0);
CHECK_EQ("*unknown*", toString(t0->type));
auto it = std::find_if(result.errors.begin(), result.errors.end(), [](TypeError& err) {
return get<OccursCheckFailed>(err);
});
CHECK(it != result.errors.end());
}
TEST_CASE_FIXTURE(Fixture, "infer_generic_function_function_argument")
{
ScopedFastFlag sff{"LuauUnsealedTableLiteral", true};
CheckResult result = check(R"(
local function sum<a>(x: a, y: a, f: (a, a) -> a) return f(x, y) end
return sum(2, 3, function(a, b) return a + b end)
)");
LUAU_REQUIRE_NO_ERRORS(result);
result = check(R"(
local function map<a, b>(arr: {a}, f: (a) -> b) local r = {} for i,v in ipairs(arr) do table.insert(r, f(v)) end return r end
local a = {1, 2, 3}
local r = map(a, function(a) return a + a > 100 end)
)");
LUAU_REQUIRE_NO_ERRORS(result);
REQUIRE_EQ("{boolean}", toString(requireType("r")));
check(R"(
local function foldl<a, b>(arr: {a}, init: b, f: (b, a) -> b) local r = init for i,v in ipairs(arr) do r = f(r, v) end return r end
local a = {1, 2, 3}
local r = foldl(a, {s=0,c=0}, function(a, b) return {s = a.s + b, c = a.c + 1} end)
)");
LUAU_REQUIRE_NO_ERRORS(result);
REQUIRE_EQ("{ c: number, s: number }", toString(requireType("r")));
}
TEST_CASE_FIXTURE(Fixture, "infer_generic_function_function_argument_overloaded")
{
CheckResult result = check(R"(
local function g1<T>(a: T, f: (T) -> T) return f(a) end
local function g2<T>(a: T, b: T, f: (T, T) -> T) return f(a, b) end
local g12: typeof(g1) & typeof(g2)
g12(1, function(x) return x + x end)
g12(1, 2, function(x, y) return x + y end)
)");
LUAU_REQUIRE_NO_ERRORS(result);
result = check(R"(
local function g1<T>(a: T, f: (T) -> T) return f(a) end
local function g2<T>(a: T, b: T, f: (T, T) -> T) return f(a, b) end
local g12: typeof(g1) & typeof(g2)
g12({x=1}, function(x) return {x=-x.x} end)
g12({x=1}, {x=2}, function(x, y) return {x=x.x + y.x} end)
)");
LUAU_REQUIRE_NO_ERRORS(result);
}
TEST_CASE_FIXTURE(Fixture, "infer_generic_lib_function_function_argument")
{
CheckResult result = check(R"(
local a = {{x=4}, {x=7}, {x=1}}
table.sort(a, function(x, y) return x.x < y.x end)
)");
LUAU_REQUIRE_NO_ERRORS(result);
}
TEST_CASE_FIXTURE(Fixture, "do_not_infer_generic_functions")
{
CheckResult result = check(R"(
local function sum<a>(x: a, y: a, f: (a, a) -> a) return f(x, y) end
local function sumrec(f: typeof(sum))
return sum(2, 3, function(a, b) return a + b end)
end
local b = sumrec(sum) -- ok
local c = sumrec(function(x, y, f) return f(x, y) end) -- type binders are not inferred
)");
LUAU_REQUIRE_ERRORS(result);
CHECK_EQ("Type '(a, b, (a, b) -> (c...)) -> (c...)' could not be converted into '<a>(a, a, (a, a) -> a) -> a'; different number of generic type "
"parameters",
toString(result.errors[0]));
}
TEST_CASE_FIXTURE(Fixture, "substitution_with_bound_table")
{
CheckResult result = check(R"(
type A = { x: number }
local a: A = { x = 1 }
local b = a
type B = typeof(b)
type X<T> = T
local c: X<B>
)");
LUAU_REQUIRE_NO_ERRORS(result);
}
TEST_SUITE_END(); TEST_SUITE_END();

View file

@ -377,4 +377,32 @@ local b: number = a
CHECK_EQ(toString(result.errors[0]), R"(Type 'X & Y & Z' could not be converted into 'number'; none of the intersection parts are compatible)"); CHECK_EQ(toString(result.errors[0]), R"(Type 'X & Y & Z' could not be converted into 'number'; none of the intersection parts are compatible)");
} }
TEST_CASE_FIXTURE(Fixture, "overload_is_not_a_function")
{
check(R"(
--!nonstrict
function _(...):((typeof(not _))&(typeof(not _)))&((typeof(not _))&(typeof(not _)))
_(...)(setfenv,_,not _,"")[_] = nil
end
do end
_(...)(...,setfenv,_):_G()
)");
}
TEST_CASE_FIXTURE(Fixture, "no_stack_overflow_from_flattenintersection")
{
CheckResult result = check(R"(
local l0,l0
repeat
type t0 = ((any)|((any)&((any)|((any)&((any)|(any))))))&(t0)
function _(l0):(t0)&(t0)
while nil do
end
end
until _(_)(_)._
)");
CHECK_LE(0, result.errors.size());
}
TEST_SUITE_END(); TEST_SUITE_END();

View file

@ -0,0 +1,473 @@
// This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details
#include "Luau/AstQuery.h"
#include "Luau/BuiltinDefinitions.h"
#include "Luau/Scope.h"
#include "Luau/TypeInfer.h"
#include "Luau/TypeVar.h"
#include "Luau/VisitTypeVar.h"
#include "Fixture.h"
#include "doctest.h"
using namespace Luau;
TEST_SUITE_BEGIN("TypeInferLoops");
TEST_CASE_FIXTURE(Fixture, "for_loop")
{
CheckResult result = check(R"(
local q
for i=0, 50, 2 do
q = i
end
)");
LUAU_REQUIRE_NO_ERRORS(result);
CHECK_EQ(*typeChecker.numberType, *requireType("q"));
}
TEST_CASE_FIXTURE(Fixture, "for_in_loop")
{
CheckResult result = check(R"(
local n
local s
for i, v in pairs({ "foo" }) do
n = i
s = v
end
)");
LUAU_REQUIRE_NO_ERRORS(result);
CHECK_EQ(*typeChecker.numberType, *requireType("n"));
CHECK_EQ(*typeChecker.stringType, *requireType("s"));
}
TEST_CASE_FIXTURE(Fixture, "for_in_loop_with_next")
{
CheckResult result = check(R"(
local n
local s
for i, v in next, { "foo", "bar" } do
n = i
s = v
end
)");
LUAU_REQUIRE_NO_ERRORS(result);
CHECK_EQ(*typeChecker.numberType, *requireType("n"));
CHECK_EQ(*typeChecker.stringType, *requireType("s"));
}
TEST_CASE_FIXTURE(Fixture, "for_in_with_an_iterator_of_type_any")
{
CheckResult result = check(R"(
local it: any
local a, b
for i, v in it do
a, b = i, v
end
)");
LUAU_REQUIRE_NO_ERRORS(result);
}
TEST_CASE_FIXTURE(Fixture, "for_in_loop_should_fail_with_non_function_iterator")
{
CheckResult result = check(R"(
local foo = "bar"
for i, v in foo do
end
)");
LUAU_REQUIRE_ERROR_COUNT(1, result);
}
TEST_CASE_FIXTURE(Fixture, "for_in_with_just_one_iterator_is_ok")
{
CheckResult result = check(R"(
local function keys(dictionary)
local new = {}
local index = 1
for key in pairs(dictionary) do
new[index] = key
index = index + 1
end
return new
end
)");
LUAU_REQUIRE_NO_ERRORS(result);
}
TEST_CASE_FIXTURE(Fixture, "for_in_with_a_custom_iterator_should_type_check")
{
CheckResult result = check(R"(
local function range(l, h): () -> number
return function()
return l
end
end
for n: string in range(1, 10) do
print(n)
end
)");
LUAU_REQUIRE_ERROR_COUNT(1, result);
}
TEST_CASE_FIXTURE(Fixture, "for_in_loop_on_error")
{
CheckResult result = check(R"(
function f(x)
gobble.prop = x.otherprop
end
local p
for _, part in i_am_not_defined do
p = part
f(part)
part.thirdprop = false
end
)");
CHECK_EQ(2, result.errors.size());
TypeId p = requireType("p");
CHECK_EQ("*unknown*", toString(p));
}
TEST_CASE_FIXTURE(Fixture, "for_in_loop_on_non_function")
{
CheckResult result = check(R"(
local bad_iter = 5
for a in bad_iter() do
end
)");
LUAU_REQUIRE_ERROR_COUNT(1, result);
REQUIRE(get<CannotCallNonFunction>(result.errors[0]));
}
TEST_CASE_FIXTURE(Fixture, "for_in_loop_error_on_factory_not_returning_the_right_amount_of_values")
{
CheckResult result = check(R"(
local function hasDivisors(value: number, table)
return false
end
function prime_iter(state, index)
while hasDivisors(index, state) do
index += 1
end
state[index] = true
return index
end
function primes1()
return prime_iter, {}
end
function primes2()
return prime_iter, {}, ""
end
function primes3()
return prime_iter, {}, 2
end
for p in primes1() do print(p) end -- mismatch in argument count
for p in primes2() do print(p) end -- mismatch in argument types, prime_iter takes {}, number, we are given {}, string
for p in primes3() do print(p) end -- no error
)");
LUAU_REQUIRE_ERROR_COUNT(2, result);
CountMismatch* acm = get<CountMismatch>(result.errors[0]);
REQUIRE(acm);
CHECK_EQ(acm->context, CountMismatch::Arg);
CHECK_EQ(2, acm->expected);
CHECK_EQ(1, acm->actual);
TypeMismatch* tm = get<TypeMismatch>(result.errors[1]);
REQUIRE(tm);
CHECK_EQ(typeChecker.numberType, tm->wantedType);
CHECK_EQ(typeChecker.stringType, tm->givenType);
}
TEST_CASE_FIXTURE(Fixture, "for_in_loop_error_on_iterator_requiring_args_but_none_given")
{
CheckResult result = check(R"(
function prime_iter(state, index)
return 1
end
for p in prime_iter do print(p) end
)");
LUAU_REQUIRE_ERROR_COUNT(1, result);
CountMismatch* acm = get<CountMismatch>(result.errors[0]);
REQUIRE(acm);
CHECK_EQ(acm->context, CountMismatch::Arg);
CHECK_EQ(2, acm->expected);
CHECK_EQ(0, acm->actual);
}
TEST_CASE_FIXTURE(Fixture, "for_in_loop_with_custom_iterator")
{
CheckResult result = check(R"(
function primes()
return function (state: number) end, 2
end
for p, q in primes do
q = ""
end
)");
LUAU_REQUIRE_ERROR_COUNT(1, result);
TypeMismatch* tm = get<TypeMismatch>(result.errors[0]);
REQUIRE(tm);
CHECK_EQ(typeChecker.numberType, tm->wantedType);
CHECK_EQ(typeChecker.stringType, tm->givenType);
}
TEST_CASE_FIXTURE(Fixture, "while_loop")
{
CheckResult result = check(R"(
local i
while true do
i = 8
end
)");
LUAU_REQUIRE_NO_ERRORS(result);
CHECK_EQ(*typeChecker.numberType, *requireType("i"));
}
TEST_CASE_FIXTURE(Fixture, "repeat_loop")
{
CheckResult result = check(R"(
local i
repeat
i = 'hi'
until true
)");
LUAU_REQUIRE_NO_ERRORS(result);
CHECK_EQ(*typeChecker.stringType, *requireType("i"));
}
TEST_CASE_FIXTURE(Fixture, "repeat_loop_condition_binds_to_its_block")
{
CheckResult result = check(R"(
repeat
local x = true
until x
)");
LUAU_REQUIRE_NO_ERRORS(result);
}
TEST_CASE_FIXTURE(Fixture, "symbols_in_repeat_block_should_not_be_visible_beyond_until_condition")
{
CheckResult result = check(R"(
repeat
local x = true
until x
print(x)
)");
LUAU_REQUIRE_ERROR_COUNT(1, result);
}
TEST_CASE_FIXTURE(Fixture, "varlist_declared_by_for_in_loop_should_be_free")
{
CheckResult result = check(R"(
local T = {}
function T.f(p)
for i, v in pairs(p) do
T.f(v)
end
end
)");
LUAU_REQUIRE_NO_ERRORS(result);
}
TEST_CASE_FIXTURE(Fixture, "properly_infer_iteratee_is_a_free_table")
{
// In this case, we cannot know the element type of the table {}. It could be anything.
// We therefore must initially ascribe a free typevar to iter.
CheckResult result = check(R"(
for iter in pairs({}) do
iter:g().p = true
end
)");
LUAU_REQUIRE_NO_ERRORS(result);
}
TEST_CASE_FIXTURE(Fixture, "correctly_scope_locals_while")
{
CheckResult result = check(R"(
while true do
local a = 1
end
print(a) -- oops!
)");
LUAU_REQUIRE_ERROR_COUNT(1, result);
UnknownSymbol* us = get<UnknownSymbol>(result.errors[0]);
REQUIRE(us);
CHECK_EQ(us->name, "a");
}
TEST_CASE_FIXTURE(Fixture, "ipairs_produces_integral_indices")
{
CheckResult result = check(R"(
local key
for i, e in ipairs({}) do key = i end
)");
LUAU_REQUIRE_NO_ERRORS(result);
REQUIRE_EQ("number", toString(requireType("key")));
}
TEST_CASE_FIXTURE(Fixture, "for_in_loop_where_iteratee_is_free")
{
// This code doesn't pass typechecking. We just care that it doesn't crash.
(void)check(R"(
--!nonstrict
function _:_(...)
end
repeat
if _ then
else
_ = ...
end
until _
for _ in _() do
end
)");
}
TEST_CASE_FIXTURE(Fixture, "unreachable_code_after_infinite_loop")
{
{
CheckResult result = check(R"(
function unreachablecodepath(a): number
while true do
if a then return 10 end
end
-- unreachable
end
unreachablecodepath(4)
)");
LUAU_REQUIRE_ERROR_COUNT(0, result);
}
{
CheckResult result = check(R"(
function reachablecodepath(a): number
while true do
if a then break end
return 10
end
print("x") -- correct error
end
reachablecodepath(4)
)");
LUAU_REQUIRE_ERRORS(result);
CHECK(get<FunctionExitsWithoutReturning>(result.errors[0]));
}
{
CheckResult result = check(R"(
function unreachablecodepath(a): number
repeat
if a then return 10 end
until false
-- unreachable
end
unreachablecodepath(4)
)");
LUAU_REQUIRE_ERROR_COUNT(0, result);
}
{
CheckResult result = check(R"(
function reachablecodepath(a, b): number
repeat
if a then break end
if b then return 10 end
until false
print("x") -- correct error
end
reachablecodepath(4)
)");
LUAU_REQUIRE_ERRORS(result);
CHECK(get<FunctionExitsWithoutReturning>(result.errors[0]));
}
{
CheckResult result = check(R"(
function unreachablecodepath(a: number?): number
repeat
return 10
until a ~= nil
-- unreachable
end
unreachablecodepath(4)
)");
LUAU_REQUIRE_ERROR_COUNT(0, result);
}
}
TEST_CASE_FIXTURE(Fixture, "loop_typecheck_crash_on_empty_optional")
{
CheckResult result = check(R"(
local t = {}
for _ in t do
for _ in assert(missing()) do
end
end
)");
LUAU_REQUIRE_ERROR_COUNT(2, result);
}
TEST_SUITE_END();

View file

@ -0,0 +1,310 @@
// This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details
#include "Luau/AstQuery.h"
#include "Luau/BuiltinDefinitions.h"
#include "Luau/Scope.h"
#include "Luau/TypeInfer.h"
#include "Luau/TypeVar.h"
#include "Luau/VisitTypeVar.h"
#include "Fixture.h"
#include "doctest.h"
using namespace Luau;
TEST_SUITE_BEGIN("TypeInferModules");
TEST_CASE_FIXTURE(Fixture, "require")
{
fileResolver.source["game/A"] = R"(
local function hooty(x: number): string
return "Hi there!"
end
return {hooty=hooty}
)";
fileResolver.source["game/B"] = R"(
local Hooty = require(game.A)
local h -- free!
local i = Hooty.hooty(h)
)";
CheckResult aResult = frontend.check("game/A");
dumpErrors(aResult);
LUAU_REQUIRE_NO_ERRORS(aResult);
CheckResult bResult = frontend.check("game/B");
dumpErrors(bResult);
LUAU_REQUIRE_NO_ERRORS(bResult);
ModulePtr b = frontend.moduleResolver.modules["game/B"];
REQUIRE(b != nullptr);
dumpErrors(bResult);
std::optional<TypeId> iType = requireType(b, "i");
REQUIRE_EQ("string", toString(*iType));
std::optional<TypeId> hType = requireType(b, "h");
REQUIRE_EQ("number", toString(*hType));
}
TEST_CASE_FIXTURE(Fixture, "require_types")
{
fileResolver.source["workspace/A"] = R"(
export type Point = {x: number, y: number}
return {}
)";
fileResolver.source["workspace/B"] = R"(
local Hooty = require(workspace.A)
local h: Hooty.Point
)";
CheckResult bResult = frontend.check("workspace/B");
dumpErrors(bResult);
ModulePtr b = frontend.moduleResolver.modules["workspace/B"];
REQUIRE(b != nullptr);
TypeId hType = requireType(b, "h");
REQUIRE_MESSAGE(bool(get<TableTypeVar>(hType)), "Expected table but got " << toString(hType));
}
TEST_CASE_FIXTURE(Fixture, "require_a_variadic_function")
{
fileResolver.source["game/A"] = R"(
local T = {}
function T.f(...) end
return T
)";
fileResolver.source["game/B"] = R"(
local A = require(game.A)
local f = A.f
)";
CheckResult result = frontend.check("game/B");
ModulePtr bModule = frontend.moduleResolver.getModule("game/B");
REQUIRE(bModule != nullptr);
TypeId f = follow(requireType(bModule, "f"));
const FunctionTypeVar* ftv = get<FunctionTypeVar>(f);
REQUIRE(ftv);
auto iter = begin(ftv->argTypes);
auto endIter = end(ftv->argTypes);
REQUIRE(iter == endIter);
REQUIRE(iter.tail());
CHECK(get<VariadicTypePack>(*iter.tail()));
}
TEST_CASE_FIXTURE(Fixture, "type_error_of_unknown_qualified_type")
{
CheckResult result = check(R"(
local p: SomeModule.DoesNotExist
)");
LUAU_REQUIRE_ERROR_COUNT(1, result);
REQUIRE_EQ(result.errors[0], (TypeError{Location{{1, 17}, {1, 40}}, UnknownSymbol{"SomeModule.DoesNotExist"}}));
}
TEST_CASE_FIXTURE(Fixture, "require_module_that_does_not_export")
{
const std::string sourceA = R"(
)";
const std::string sourceB = R"(
local Hooty = require(script.Parent.A)
)";
fileResolver.source["game/Workspace/A"] = sourceA;
fileResolver.source["game/Workspace/B"] = sourceB;
frontend.check("game/Workspace/A");
frontend.check("game/Workspace/B");
ModulePtr aModule = frontend.moduleResolver.modules["game/Workspace/A"];
ModulePtr bModule = frontend.moduleResolver.modules["game/Workspace/B"];
CHECK(aModule->errors.empty());
REQUIRE_EQ(1, bModule->errors.size());
CHECK_MESSAGE(get<IllegalRequire>(bModule->errors[0]), "Should be IllegalRequire: " << toString(bModule->errors[0]));
auto hootyType = requireType(bModule, "Hooty");
CHECK_EQ("*unknown*", toString(hootyType));
}
TEST_CASE_FIXTURE(Fixture, "warn_if_you_try_to_require_a_non_modulescript")
{
fileResolver.source["Modules/A"] = "";
fileResolver.sourceTypes["Modules/A"] = SourceCode::Local;
fileResolver.source["Modules/B"] = R"(
local M = require(script.Parent.A)
)";
CheckResult result = frontend.check("Modules/B");
LUAU_REQUIRE_ERROR_COUNT(1, result);
CHECK(get<IllegalRequire>(result.errors[0]));
}
TEST_CASE_FIXTURE(Fixture, "general_require_call_expression")
{
fileResolver.source["game/A"] = R"(
--!strict
return { def = 4 }
)";
fileResolver.source["game/B"] = R"(
--!strict
local tbl = { abc = require(game.A) }
local a : string = ""
a = tbl.abc.def
)";
CheckResult result = frontend.check("game/B");
LUAU_REQUIRE_ERROR_COUNT(1, result);
CHECK_EQ("Type 'number' could not be converted into 'string'", toString(result.errors[0]));
}
TEST_CASE_FIXTURE(Fixture, "general_require_type_mismatch")
{
fileResolver.source["game/A"] = R"(
return { def = 4 }
)";
fileResolver.source["game/B"] = R"(
local tbl: string = require(game.A)
)";
CheckResult result = frontend.check("game/B");
LUAU_REQUIRE_ERROR_COUNT(1, result);
CHECK_EQ("Type '{| def: number |}' could not be converted into 'string'", toString(result.errors[0]));
}
TEST_CASE_FIXTURE(Fixture, "bound_free_table_export_is_ok")
{
CheckResult result = check(R"(
local n = {}
function n:Clone() end
local m = {}
function m.a(x)
x:Clone()
end
function m.b()
m.a(n)
end
return m
)");
LUAU_REQUIRE_NO_ERRORS(result);
}
TEST_CASE_FIXTURE(Fixture, "custom_require_global")
{
CheckResult result = check(R"(
--!nonstrict
require = function(a) end
local crash = require(game.A)
)");
LUAU_REQUIRE_NO_ERRORS(result);
}
TEST_CASE_FIXTURE(Fixture, "require_failed_module")
{
fileResolver.source["game/A"] = R"(
return unfortunately()
)";
CheckResult aResult = frontend.check("game/A");
LUAU_REQUIRE_ERRORS(aResult);
CheckResult result = check(R"(
local ModuleA = require(game.A)
)");
LUAU_REQUIRE_NO_ERRORS(result);
std::optional<TypeId> oty = requireType("ModuleA");
CHECK_EQ("*unknown*", toString(*oty));
}
TEST_CASE_FIXTURE(Fixture, "do_not_modify_imported_types")
{
fileResolver.source["game/A"] = R"(
export type Type = { unrelated: boolean }
return {}
)";
fileResolver.source["game/B"] = R"(
local types = require(game.A)
type Type = types.Type
local x: Type = {}
function x:Destroy(): () end
)";
CheckResult result = frontend.check("game/B");
LUAU_REQUIRE_ERRORS(result);
}
TEST_CASE_FIXTURE(Fixture, "do_not_modify_imported_types_2")
{
ScopedFastFlag immutableTypes{"LuauImmutableTypes", true};
fileResolver.source["game/A"] = R"(
export type Type = { x: { a: number } }
return {}
)";
fileResolver.source["game/B"] = R"(
local types = require(game.A)
type Type = types.Type
local x: Type = { x = { a = 2 } }
type Rename = typeof(x.x)
)";
CheckResult result = frontend.check("game/B");
LUAU_REQUIRE_NO_ERRORS(result);
}
TEST_CASE_FIXTURE(Fixture, "do_not_modify_imported_types_3")
{
ScopedFastFlag immutableTypes{"LuauImmutableTypes", true};
fileResolver.source["game/A"] = R"(
local y = setmetatable({}, {})
export type Type = { x: typeof(y) }
return { x = y }
)";
fileResolver.source["game/B"] = R"(
local types = require(game.A)
type Type = types.Type
local x: Type = types
type Rename = typeof(x.x)
)";
CheckResult result = frontend.check("game/B");
LUAU_REQUIRE_NO_ERRORS(result);
}
TEST_SUITE_END();

View file

@ -0,0 +1,275 @@
// This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details
#include "Luau/AstQuery.h"
#include "Luau/BuiltinDefinitions.h"
#include "Luau/Scope.h"
#include "Luau/TypeInfer.h"
#include "Luau/TypeVar.h"
#include "Luau/VisitTypeVar.h"
#include "Fixture.h"
#include "doctest.h"
using namespace Luau;
TEST_SUITE_BEGIN("TypeInferOOP");
TEST_CASE_FIXTURE(Fixture, "dont_suggest_using_colon_rather_than_dot_if_not_defined_with_colon")
{
CheckResult result = check(R"(
local someTable = {}
someTable.Function1 = function(Arg1)
end
someTable.Function1() -- Argument count mismatch
)");
LUAU_REQUIRE_ERROR_COUNT(1, result);
REQUIRE(get<CountMismatch>(result.errors[0]));
}
TEST_CASE_FIXTURE(Fixture, "dont_suggest_using_colon_rather_than_dot_if_it_wont_help_2")
{
CheckResult result = check(R"(
local someTable = {}
someTable.Function2 = function(Arg1, Arg2)
end
someTable.Function2() -- Argument count mismatch
)");
LUAU_REQUIRE_ERROR_COUNT(1, result);
REQUIRE(get<CountMismatch>(result.errors[0]));
}
TEST_CASE_FIXTURE(Fixture, "dont_suggest_using_colon_rather_than_dot_if_another_overload_works")
{
CheckResult result = check(R"(
type T = {method: ((T, number) -> number) & ((number) -> number)}
local T: T
T.method(4)
)");
LUAU_REQUIRE_NO_ERRORS(result);
}
TEST_CASE_FIXTURE(Fixture, "method_depends_on_table")
{
CheckResult result = check(R"(
-- This catches a bug where x:m didn't count as a use of x
-- so toposort would happily reorder a definition of
-- function x:m before the definition of x.
function g() f() end
local x = {}
function x:m() end
function f() x:m() end
)");
LUAU_REQUIRE_NO_ERRORS(result);
}
TEST_CASE_FIXTURE(Fixture, "methods_are_topologically_sorted")
{
CheckResult result = check(R"(
local T = {}
function T:foo()
return T:bar(999), T:bar("hi")
end
function T:bar(i)
return i
end
local a, b = T:foo()
)");
LUAU_REQUIRE_NO_ERRORS(result);
dumpErrors(result);
CHECK_EQ(PrimitiveTypeVar::Number, getPrimitiveType(requireType("a")));
CHECK_EQ(PrimitiveTypeVar::String, getPrimitiveType(requireType("b")));
}
TEST_CASE_FIXTURE(Fixture, "quantify_methods_defined_using_dot_syntax_and_explicit_self_parameter")
{
check(R"(
local T = {}
function T.method(self)
self:method()
end
function T.method2(self)
self:method()
end
T:method2()
)");
}
TEST_CASE_FIXTURE(Fixture, "inferring_hundreds_of_self_calls_should_not_suffocate_memory")
{
CheckResult result = check(R"(
("foo")
:lower()
:lower()
:lower()
:lower()
:lower()
:lower()
:lower()
:lower()
:lower()
:lower()
:lower()
:lower()
:lower()
:lower()
:lower()
:lower()
:lower()
:lower()
)");
ModulePtr module = getMainModule();
CHECK_GE(50, module->internalTypes.typeVars.size());
}
TEST_CASE_FIXTURE(Fixture, "object_constructor_can_refer_to_method_of_self")
{
// CLI-30902
CheckResult result = check(R"(
--!strict
type Foo = {
fooConn: () -> () | nil
}
local Foo = {}
Foo.__index = Foo
function Foo.new()
local self: Foo = {
fooConn = nil,
}
setmetatable(self, Foo)
self.fooConn = function()
self:method() -- Key 'method' not found in table self
end
return self
end
function Foo:method()
print("foo")
end
local foo = Foo.new()
-- TODO This is the best our current refinement support can offer :(
local bar = foo.fooConn
if bar then bar() end
-- foo.fooConn()
)");
LUAU_REQUIRE_NO_ERRORS(result);
}
TEST_CASE_FIXTURE(Fixture, "CheckMethodsOfSealed")
{
CheckResult result = check(R"(
local x: {prop: number} = {prop=9999}
function x:y(z: number)
local s: string = z
end
)");
LUAU_REQUIRE_ERROR_COUNT(2, result);
}
TEST_CASE_FIXTURE(Fixture, "nonstrict_self_mismatch_tail")
{
CheckResult result = check(R"(
--!nonstrict
local f = {}
function f:foo(a: number, b: number) end
function bar(...)
f.foo(f, 1, ...)
end
bar(2)
)");
LUAU_REQUIRE_NO_ERRORS(result);
}
TEST_CASE_FIXTURE(Fixture, "inferred_methods_of_free_tables_have_the_same_level_as_the_enclosing_table")
{
check(R"(
function Base64FileReader(data)
local reader = {}
local index: number
function reader:PeekByte()
return data:byte(index)
end
function reader:Byte()
return data:byte(index - 1)
end
return reader
end
Base64FileReader()
function ReadMidiEvents(data)
local reader = Base64FileReader(data)
while reader:HasMore() do
(reader:Byte() % 128)
end
end
)");
}
TEST_CASE_FIXTURE(Fixture, "table_oop")
{
CheckResult result = check(R"(
--!strict
local Class = {}
Class.__index = Class
type Class = typeof(setmetatable({} :: { x: number }, Class))
function Class.new(x: number): Class
return setmetatable({x = x}, Class)
end
function Class.getx(self: Class)
return self.x
end
function test()
local c = Class.new(42)
local n = c:getx()
local nn = c.x
print(string.format("%d %d", n, nn))
end
)");
LUAU_REQUIRE_NO_ERRORS(result);
}
TEST_SUITE_END();

View file

@ -0,0 +1,759 @@
// This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details
#include "Luau/AstQuery.h"
#include "Luau/BuiltinDefinitions.h"
#include "Luau/Scope.h"
#include "Luau/TypeInfer.h"
#include "Luau/TypeVar.h"
#include "Luau/VisitTypeVar.h"
#include "Fixture.h"
#include "doctest.h"
using namespace Luau;
TEST_SUITE_BEGIN("TypeInferOperators");
TEST_CASE_FIXTURE(Fixture, "or_joins_types")
{
CheckResult result = check(R"(
local s = "a" or 10
local x:string|number = s
)");
LUAU_REQUIRE_NO_ERRORS(result);
CHECK_EQ(toString(*requireType("s")), "number | string");
CHECK_EQ(toString(*requireType("x")), "number | string");
}
TEST_CASE_FIXTURE(Fixture, "or_joins_types_with_no_extras")
{
CheckResult result = check(R"(
local s = "a" or 10
local x:number|string = s
local y = x or "s"
)");
CHECK_EQ(0, result.errors.size());
CHECK_EQ(toString(*requireType("s")), "number | string");
CHECK_EQ(toString(*requireType("y")), "number | string");
}
TEST_CASE_FIXTURE(Fixture, "or_joins_types_with_no_superfluous_union")
{
CheckResult result = check(R"(
local s = "a" or "b"
local x:string = s
)");
CHECK_EQ(0, result.errors.size());
CHECK_EQ(*requireType("s"), *typeChecker.stringType);
}
TEST_CASE_FIXTURE(Fixture, "and_adds_boolean")
{
CheckResult result = check(R"(
local s = "a" and 10
local x:boolean|number = s
)");
CHECK_EQ(0, result.errors.size());
CHECK_EQ(toString(*requireType("s")), "boolean | number");
}
TEST_CASE_FIXTURE(Fixture, "and_adds_boolean_no_superfluous_union")
{
CheckResult result = check(R"(
local s = "a" and true
local x:boolean = s
)");
CHECK_EQ(0, result.errors.size());
CHECK_EQ(*requireType("x"), *typeChecker.booleanType);
}
TEST_CASE_FIXTURE(Fixture, "and_or_ternary")
{
CheckResult result = check(R"(
local s = (1/2) > 0.5 and "a" or 10
)");
CHECK_EQ(0, result.errors.size());
CHECK_EQ(toString(*requireType("s")), "number | string");
}
TEST_CASE_FIXTURE(Fixture, "primitive_arith_no_metatable")
{
CheckResult result = check(R"(
function add(a: number, b: string)
return a + (tonumber(b) :: number), a .. b
end
local n, s = add(2,"3")
)");
LUAU_REQUIRE_NO_ERRORS(result);
const FunctionTypeVar* functionType = get<FunctionTypeVar>(requireType("add"));
std::optional<TypeId> retType = first(functionType->retType);
CHECK_EQ(std::optional<TypeId>(typeChecker.numberType), retType);
CHECK_EQ(requireType("n"), typeChecker.numberType);
CHECK_EQ(requireType("s"), typeChecker.stringType);
}
TEST_CASE_FIXTURE(Fixture, "primitive_arith_no_metatable_with_follows")
{
CheckResult result = check(R"(
local PI=3.1415926535897931
local SOLAR_MASS=4*PI * PI
)");
LUAU_REQUIRE_NO_ERRORS(result);
CHECK_EQ(requireType("SOLAR_MASS"), typeChecker.numberType);
}
TEST_CASE_FIXTURE(Fixture, "primitive_arith_possible_metatable")
{
CheckResult result = check(R"(
function add(a: number, b: any)
return a + b
end
local t = add(1,2)
)");
LUAU_REQUIRE_NO_ERRORS(result);
CHECK_EQ("any", toString(requireType("t")));
}
TEST_CASE_FIXTURE(Fixture, "some_primitive_binary_ops")
{
CheckResult result = check(R"(
local a = 4 + 8
local b = a + 9
local s = 'hotdogs'
local t = s .. s
local c = b - a
)");
LUAU_REQUIRE_NO_ERRORS(result);
CHECK_EQ("number", toString(requireType("a")));
CHECK_EQ("number", toString(requireType("b")));
CHECK_EQ("string", toString(requireType("s")));
CHECK_EQ("string", toString(requireType("t")));
CHECK_EQ("number", toString(requireType("c")));
}
TEST_CASE_FIXTURE(Fixture, "typecheck_overloaded_multiply_that_is_an_intersection")
{
ScopedFastFlag sff{"LuauErrorRecoveryType", true};
CheckResult result = check(R"(
--!strict
local Vec3 = {}
Vec3.__index = Vec3
function Vec3.new()
return setmetatable({x=0, y=0, z=0}, Vec3)
end
export type Vec3 = typeof(Vec3.new())
local thefun: any = function(self, o) return self end
local multiply: ((Vec3, Vec3) -> Vec3) & ((Vec3, number) -> Vec3) = thefun
Vec3.__mul = multiply
local a = Vec3.new()
local b = Vec3.new()
local c = a * b
local d = a * 2
local e = a * 'cabbage'
)");
LUAU_REQUIRE_ERROR_COUNT(1, result);
CHECK_EQ("Vec3", toString(requireType("a")));
CHECK_EQ("Vec3", toString(requireType("b")));
CHECK_EQ("Vec3", toString(requireType("c")));
CHECK_EQ("Vec3", toString(requireType("d")));
CHECK_EQ("Vec3", toString(requireType("e")));
}
TEST_CASE_FIXTURE(Fixture, "typecheck_overloaded_multiply_that_is_an_intersection_on_rhs")
{
ScopedFastFlag sff{"LuauErrorRecoveryType", true};
CheckResult result = check(R"(
--!strict
local Vec3 = {}
Vec3.__index = Vec3
function Vec3.new()
return setmetatable({x=0, y=0, z=0}, Vec3)
end
export type Vec3 = typeof(Vec3.new())
local thefun: any = function(self, o) return self end
local multiply: ((Vec3, Vec3) -> Vec3) & ((Vec3, number) -> Vec3) = thefun
Vec3.__mul = multiply
local a = Vec3.new()
local b = Vec3.new()
local c = b * a
local d = 2 * a
local e = 'cabbage' * a
)");
LUAU_REQUIRE_ERROR_COUNT(1, result);
CHECK_EQ("Vec3", toString(requireType("a")));
CHECK_EQ("Vec3", toString(requireType("b")));
CHECK_EQ("Vec3", toString(requireType("c")));
CHECK_EQ("Vec3", toString(requireType("d")));
CHECK_EQ("Vec3", toString(requireType("e")));
}
TEST_CASE_FIXTURE(Fixture, "compare_numbers")
{
CheckResult result = check(R"(
local a = 441
local b = 0
local c = a < b
)");
LUAU_REQUIRE_NO_ERRORS(result);
}
TEST_CASE_FIXTURE(Fixture, "compare_strings")
{
CheckResult result = check(R"(
local a = '441'
local b = '0'
local c = a < b
)");
LUAU_REQUIRE_NO_ERRORS(result);
}
TEST_CASE_FIXTURE(Fixture, "cannot_indirectly_compare_types_that_do_not_have_a_metatable")
{
CheckResult result = check(R"(
local a = {}
local b = {}
local c = a < b
)");
LUAU_REQUIRE_ERROR_COUNT(1, result);
GenericError* gen = get<GenericError>(result.errors[0]);
REQUIRE_EQ(gen->message, "Type a cannot be compared with < because it has no metatable");
}
TEST_CASE_FIXTURE(Fixture, "cannot_indirectly_compare_types_that_do_not_offer_overloaded_ordering_operators")
{
CheckResult result = check(R"(
local M = {}
function M.new()
return setmetatable({}, M)
end
type M = typeof(M.new())
local a = M.new()
local b = M.new()
local c = a < b
)");
LUAU_REQUIRE_ERROR_COUNT(1, result);
GenericError* gen = get<GenericError>(result.errors[0]);
REQUIRE(gen != nullptr);
REQUIRE_EQ(gen->message, "Table M does not offer metamethod __lt");
}
TEST_CASE_FIXTURE(Fixture, "cannot_compare_tables_that_do_not_have_the_same_metatable")
{
CheckResult result = check(R"(
--!strict
local M = {}
function M.new()
return setmetatable({}, M)
end
function M.__lt(left, right) return true end
local a = M.new()
local b = {}
local c = a < b -- line 10
local d = b < a -- line 11
)");
LUAU_REQUIRE_ERROR_COUNT(2, result);
REQUIRE_EQ((Location{{10, 18}, {10, 23}}), result.errors[0].location);
REQUIRE_EQ((Location{{11, 18}, {11, 23}}), result.errors[1].location);
}
TEST_CASE_FIXTURE(Fixture, "produce_the_correct_error_message_when_comparing_a_table_with_a_metatable_with_one_that_does_not")
{
CheckResult result = check(R"(
--!strict
local M = {}
function M.new()
return setmetatable({}, M)
end
function M.__lt(left, right) return true end
type M = typeof(M.new())
local a = M.new()
local b = {}
local c = a < b -- line 10
)");
LUAU_REQUIRE_ERROR_COUNT(1, result);
auto err = get<GenericError>(result.errors[0]);
REQUIRE(err != nullptr);
// Frail. :|
REQUIRE_EQ("Types M and b cannot be compared with < because they do not have the same metatable", err->message);
}
TEST_CASE_FIXTURE(Fixture, "in_nonstrict_mode_strip_nil_from_intersections_when_considering_relational_operators")
{
CheckResult result = check(R"(
--!nonstrict
function maybe_a_number(): number?
return 50
end
local a = maybe_a_number() < maybe_a_number()
)");
LUAU_REQUIRE_NO_ERRORS(result);
}
TEST_CASE_FIXTURE(Fixture, "compound_assign_basic")
{
CheckResult result = check(R"(
local s = 10
s += 20
)");
CHECK_EQ(0, result.errors.size());
CHECK_EQ(toString(*requireType("s")), "number");
}
TEST_CASE_FIXTURE(Fixture, "compound_assign_mismatch_op")
{
CheckResult result = check(R"(
local s = 10
s += true
)");
LUAU_REQUIRE_ERROR_COUNT(1, result);
CHECK_EQ(result.errors[0], (TypeError{Location{{2, 13}, {2, 17}}, TypeMismatch{typeChecker.numberType, typeChecker.booleanType}}));
}
TEST_CASE_FIXTURE(Fixture, "compound_assign_mismatch_result")
{
CheckResult result = check(R"(
local s = 'hello'
s += 10
)");
LUAU_REQUIRE_ERROR_COUNT(2, result);
CHECK_EQ(result.errors[0], (TypeError{Location{{2, 8}, {2, 9}}, TypeMismatch{typeChecker.numberType, typeChecker.stringType}}));
CHECK_EQ(result.errors[1], (TypeError{Location{{2, 8}, {2, 15}}, TypeMismatch{typeChecker.stringType, typeChecker.numberType}}));
}
TEST_CASE_FIXTURE(Fixture, "compound_assign_metatable")
{
CheckResult result = check(R"(
--!strict
type V2B = { x: number, y: number }
local v2b: V2B = { x = 0, y = 0 }
local VMT = {}
type V2 = typeof(setmetatable(v2b, VMT))
function VMT.__add(a: V2, b: V2): V2
return setmetatable({ x = a.x + b.x, y = a.y + b.y }, VMT)
end
local v1: V2 = setmetatable({ x = 1, y = 2 }, VMT)
local v2: V2 = setmetatable({ x = 3, y = 4 }, VMT)
v1 += v2
)");
CHECK_EQ(0, result.errors.size());
}
TEST_CASE_FIXTURE(Fixture, "compound_assign_mismatch_metatable")
{
CheckResult result = check(R"(
--!strict
type V2B = { x: number, y: number }
local v2b: V2B = { x = 0, y = 0 }
local VMT = {}
type V2 = typeof(setmetatable(v2b, VMT))
function VMT.__mod(a: V2, b: V2): number
return a.x * b.x + a.y * b.y
end
local v1: V2 = setmetatable({ x = 1, y = 2 }, VMT)
local v2: V2 = setmetatable({ x = 3, y = 4 }, VMT)
v1 %= v2
)");
LUAU_REQUIRE_ERROR_COUNT(1, result);
TypeMismatch* tm = get<TypeMismatch>(result.errors[0]);
CHECK_EQ(*tm->wantedType, *requireType("v2"));
CHECK_EQ(*tm->givenType, *typeChecker.numberType);
}
TEST_CASE_FIXTURE(Fixture, "CallOrOfFunctions")
{
CheckResult result = check(R"(
function f() return 1; end
function g() return 2; end
(f or g)()
)");
LUAU_REQUIRE_NO_ERRORS(result);
}
TEST_CASE_FIXTURE(Fixture, "CallAndOrOfFunctions")
{
CheckResult result = check(R"(
function f() return 1; end
function g() return 2; end
local x = false
(x and f or g)()
)");
LUAU_REQUIRE_NO_ERRORS(result);
}
TEST_CASE_FIXTURE(Fixture, "typecheck_unary_minus")
{
CheckResult result = check(R"(
--!strict
local foo = {
value = 10
}
local mt = {}
setmetatable(foo, mt)
mt.__unm = function(val: typeof(foo)): string
return val.value .. "test"
end
local a = -foo
local b = 1+-1
local bar = {
value = 10
}
local c = -bar -- disallowed
)");
LUAU_REQUIRE_ERROR_COUNT(1, result);
CHECK_EQ("string", toString(requireType("a")));
CHECK_EQ("number", toString(requireType("b")));
GenericError* gen = get<GenericError>(result.errors[0]);
REQUIRE_EQ(gen->message, "Unary operator '-' not supported by type 'bar'");
}
TEST_CASE_FIXTURE(Fixture, "unary_not_is_boolean")
{
CheckResult result = check(R"(
local b = not "string"
local c = not (math.random() > 0.5 and "string" or 7)
)");
LUAU_REQUIRE_NO_ERRORS(result);
REQUIRE_EQ("boolean", toString(requireType("b")));
REQUIRE_EQ("boolean", toString(requireType("c")));
}
TEST_CASE_FIXTURE(Fixture, "disallow_string_and_types_without_metatables_from_arithmetic_binary_ops")
{
CheckResult result = check(R"(
--!strict
local a = "1.24" + 123 -- not allowed
local foo = {
value = 10
}
local b = foo + 1 -- not allowed
local bar = {
value = 1
}
local mt = {}
setmetatable(bar, mt)
mt.__add = function(a: typeof(bar), b: number): number
return a.value + b
end
local c = bar + 1 -- allowed
local d = bar + foo -- not allowed
)");
LUAU_REQUIRE_ERROR_COUNT(3, result);
TypeMismatch* tm = get<TypeMismatch>(result.errors[0]);
REQUIRE_EQ(*tm->wantedType, *typeChecker.numberType);
REQUIRE_EQ(*tm->givenType, *typeChecker.stringType);
TypeMismatch* tm2 = get<TypeMismatch>(result.errors[2]);
CHECK_EQ(*tm2->wantedType, *typeChecker.numberType);
CHECK_EQ(*tm2->givenType, *requireType("foo"));
GenericError* gen2 = get<GenericError>(result.errors[1]);
REQUIRE_EQ(gen2->message, "Binary operator '+' not supported by types 'foo' and 'number'");
}
// CLI-29033
TEST_CASE_FIXTURE(Fixture, "unknown_type_in_comparison")
{
CheckResult result = check(R"(
function merge(lower, greater)
if lower.y == greater.y then
end
end
)");
LUAU_REQUIRE_NO_ERRORS(result);
}
TEST_CASE_FIXTURE(Fixture, "concat_op_on_free_lhs_and_string_rhs")
{
CheckResult result = check(R"(
local function f(x)
return x .. "y"
end
)");
LUAU_REQUIRE_ERROR_COUNT(1, result);
REQUIRE(get<CannotInferBinaryOperation>(result.errors[0]));
}
TEST_CASE_FIXTURE(Fixture, "concat_op_on_string_lhs_and_free_rhs")
{
CheckResult result = check(R"(
local function f(x)
return "foo" .. x
end
)");
LUAU_REQUIRE_NO_ERRORS(result);
CHECK_EQ("(string) -> string", toString(requireType("f")));
}
TEST_CASE_FIXTURE(Fixture, "strict_binary_op_where_lhs_unknown")
{
std::vector<std::string> ops = {"+", "-", "*", "/", "%", "^", ".."};
std::string src = R"(
function foo(a, b)
)";
for (const auto& op : ops)
src += "local _ = a " + op + "b\n";
src += "end";
CheckResult result = check(src);
LUAU_REQUIRE_ERROR_COUNT(ops.size(), result);
CHECK_EQ("Unknown type used in + operation; consider adding a type annotation to 'a'", toString(result.errors[0]));
}
TEST_CASE_FIXTURE(Fixture, "and_binexps_dont_unify")
{
CheckResult result = check(R"(
--!strict
local t = {}
while true and t[1] do
print(t[1].test)
end
)");
LUAU_REQUIRE_NO_ERRORS(result);
}
TEST_CASE_FIXTURE(Fixture, "error_on_invalid_operand_types_to_relational_operators")
{
CheckResult result = check(R"(
local a: boolean = true
local b: boolean = false
local foo = a < b
)");
LUAU_REQUIRE_ERROR_COUNT(1, result);
GenericError* ge = get<GenericError>(result.errors[0]);
REQUIRE(ge);
CHECK_EQ("Type 'boolean' cannot be compared with relational operator <", ge->message);
}
TEST_CASE_FIXTURE(Fixture, "error_on_invalid_operand_types_to_relational_operators2")
{
CheckResult result = check(R"(
local a: number | string = ""
local b: number | string = 1
local foo = a < b
)");
LUAU_REQUIRE_ERROR_COUNT(1, result);
GenericError* ge = get<GenericError>(result.errors[0]);
REQUIRE(ge);
CHECK_EQ("Type 'number | string' cannot be compared with relational operator <", ge->message);
}
TEST_CASE_FIXTURE(Fixture, "cli_38355_recursive_union")
{
CheckResult result = check(R"(
--!strict
local _
_ += _ and _ or _ and _ or _ and _
)");
LUAU_REQUIRE_ERROR_COUNT(1, result);
CHECK_EQ("Type contains a self-recursive construct that cannot be resolved", toString(result.errors[0]));
}
TEST_CASE_FIXTURE(Fixture, "UnknownGlobalCompoundAssign")
{
// In non-strict mode, global definition is still allowed
{
CheckResult result = check(R"(
--!nonstrict
a = a + 1
print(a)
)");
LUAU_REQUIRE_ERROR_COUNT(1, result);
CHECK_EQ(toString(result.errors[0]), "Unknown global 'a'");
}
// In strict mode we no longer generate two errors from lhs
{
CheckResult result = check(R"(
--!strict
a += 1
print(a)
)");
LUAU_REQUIRE_ERRORS(result);
CHECK_EQ(toString(result.errors[0]), "Unknown global 'a'");
}
// In non-strict mode, compound assignment is not a definition, it's a modification
{
CheckResult result = check(R"(
--!nonstrict
a += 1
print(a)
)");
LUAU_REQUIRE_ERROR_COUNT(2, result);
CHECK_EQ(toString(result.errors[0]), "Unknown global 'a'");
}
}
TEST_CASE_FIXTURE(Fixture, "strip_nil_from_lhs_or_operator")
{
CheckResult result = check(R"(
--!strict
local a: number? = nil
local b: number = a or 1
)");
LUAU_REQUIRE_NO_ERRORS(result);
}
TEST_CASE_FIXTURE(Fixture, "strip_nil_from_lhs_or_operator2")
{
CheckResult result = check(R"(
--!nonstrict
local a: number? = nil
local b: number = a or 1
)");
LUAU_REQUIRE_NO_ERRORS(result);
}
TEST_CASE_FIXTURE(Fixture, "dont_strip_nil_from_rhs_or_operator")
{
CheckResult result = check(R"(
--!strict
local a: number? = nil
local b: number = 1 or a
)");
LUAU_REQUIRE_ERROR_COUNT(1, result);
TypeMismatch* tm = get<TypeMismatch>(result.errors[0]);
REQUIRE(tm);
CHECK_EQ(typeChecker.numberType, tm->wantedType);
CHECK_EQ("number?", toString(tm->givenType));
}
TEST_CASE_FIXTURE(Fixture, "operator_eq_verifies_types_do_intersect")
{
CheckResult result = check(R"(
type Array<T> = { [number]: T }
type Fiber = { id: number }
type null = {}
local fiberStack: Array<Fiber | null> = {}
local index = 0
local function f(fiber: Fiber)
local a = fiber ~= fiberStack[index]
local b = fiberStack[index] ~= fiber
end
return f
)");
LUAU_REQUIRE_NO_ERRORS(result);
}
TEST_CASE_FIXTURE(Fixture, "operator_eq_operands_are_not_subtypes_of_each_other_but_has_overlap")
{
ScopedFastFlag sff1{"LuauEqConstraint", true};
CheckResult result = check(R"(
local function f(a: string | number, b: boolean | number)
return a == b
end
)");
// This doesn't produce any errors but for the wrong reasons.
// This unit test serves as a reminder to not try and unify the operands on `==`/`~=`.
LUAU_REQUIRE_NO_ERRORS(result);
}
TEST_CASE_FIXTURE(Fixture, "refine_and_or")
{
CheckResult result = check(R"(
local t: {x: number?}? = {x = nil}
local u = t and t.x or 5
)");
LUAU_REQUIRE_NO_ERRORS(result);
CHECK_EQ("number", toString(requireType("u")));
}
TEST_SUITE_END();

View file

@ -0,0 +1,100 @@
// This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details
#include "Luau/AstQuery.h"
#include "Luau/BuiltinDefinitions.h"
#include "Luau/Scope.h"
#include "Luau/TypeInfer.h"
#include "Luau/TypeVar.h"
#include "Luau/VisitTypeVar.h"
#include "Fixture.h"
#include "doctest.h"
using namespace Luau;
TEST_SUITE_BEGIN("TypeInferPrimitives");
TEST_CASE_FIXTURE(Fixture, "cannot_call_primitives")
{
CheckResult result = check("local foo = 5 foo()");
LUAU_REQUIRE_ERROR_COUNT(1, result);
REQUIRE(get<CannotCallNonFunction>(result.errors[0]) != nullptr);
}
TEST_CASE_FIXTURE(Fixture, "string_length")
{
CheckResult result = check(R"(
local s = "Hello, World!"
local t = #s
)");
LUAU_REQUIRE_NO_ERRORS(result);
CHECK_EQ("number", toString(requireType("t")));
}
TEST_CASE_FIXTURE(Fixture, "string_index")
{
CheckResult result = check(R"(
local s = "Hello, World!"
local t = s[4]
)");
LUAU_REQUIRE_ERROR_COUNT(1, result);
NotATable* nat = get<NotATable>(result.errors[0]);
REQUIRE(nat);
CHECK_EQ("string", toString(nat->ty));
CHECK_EQ("*unknown*", toString(requireType("t")));
}
TEST_CASE_FIXTURE(Fixture, "string_method")
{
CheckResult result = check(R"(
local p = ("tacos"):len()
)");
CHECK_EQ(0, result.errors.size());
CHECK_EQ(*requireType("p"), *typeChecker.numberType);
}
TEST_CASE_FIXTURE(Fixture, "string_function_indirect")
{
CheckResult result = check(R"(
local s:string
local l = s.lower
local p = l(s)
)");
CHECK_EQ(0, result.errors.size());
CHECK_EQ(*requireType("p"), *typeChecker.stringType);
}
TEST_CASE_FIXTURE(Fixture, "string_function_other")
{
CheckResult result = check(R"(
local s:string
local p = s:match("foo")
)");
CHECK_EQ(0, result.errors.size());
CHECK_EQ(toString(requireType("p")), "string?");
}
TEST_CASE_FIXTURE(Fixture, "CheckMethodsOfNumber")
{
ScopedFastFlag sff{"LuauErrorRecoveryType", true};
CheckResult result = check(R"(
local x: number = 9999
function x:y(z: number)
local s: string = z
end
)");
LUAU_REQUIRE_ERROR_COUNT(2, result);
}
TEST_SUITE_END();

View file

@ -1298,4 +1298,22 @@ TEST_CASE_FIXTURE(RefinementClassFixture, "x_is_not_instance_or_else_not_part")
CHECK_EQ("Part", toString(requireTypeAtPosition({5, 28}))); CHECK_EQ("Part", toString(requireTypeAtPosition({5, 28})));
} }
TEST_CASE_FIXTURE(Fixture, "typeguard_doesnt_leak_to_elseif")
{
const std::string code = R"(
function f(a)
if type(a) == "boolean" then
local a1 = a
elseif a.fn() then
local a2 = a
else
local a3 = a
end
end
)";
CheckResult result = check(code);
LUAU_REQUIRE_NO_ERRORS(result);
dumpErrors(result);
}
TEST_SUITE_END(); TEST_SUITE_END();

View file

@ -5,6 +5,8 @@
#include "doctest.h" #include "doctest.h"
#include "Luau/BuiltinDefinitions.h" #include "Luau/BuiltinDefinitions.h"
LUAU_FASTFLAG(BetterDiagnosticCodesInStudio)
using namespace Luau; using namespace Luau;
TEST_SUITE_BEGIN("TypeSingletons"); TEST_SUITE_BEGIN("TypeSingletons");
@ -353,7 +355,14 @@ TEST_CASE_FIXTURE(Fixture, "table_properties_alias_or_parens_is_indexer")
)"); )");
LUAU_REQUIRE_ERROR_COUNT(1, result); LUAU_REQUIRE_ERROR_COUNT(1, result);
CHECK_EQ("Syntax error: Cannot have more than one table indexer", toString(result.errors[0])); if (FFlag::BetterDiagnosticCodesInStudio)
{
CHECK_EQ("Cannot have more than one table indexer", toString(result.errors[0]));
}
else
{
CHECK_EQ("Syntax error: Cannot have more than one table indexer", toString(result.errors[0]));
}
} }
TEST_CASE_FIXTURE(Fixture, "table_properties_type_error_escapes") TEST_CASE_FIXTURE(Fixture, "table_properties_type_error_escapes")
@ -445,7 +454,7 @@ TEST_CASE_FIXTURE(Fixture, "widen_the_supertype_if_it_is_free_and_subtype_has_si
{"LuauSingletonTypes", true}, {"LuauSingletonTypes", true},
{"LuauEqConstraint", true}, {"LuauEqConstraint", true},
{"LuauDiscriminableUnions2", true}, {"LuauDiscriminableUnions2", true},
{"LuauWidenIfSupertypeIsFree", true}, {"LuauWidenIfSupertypeIsFree2", true},
{"LuauWeakEqConstraint", false}, {"LuauWeakEqConstraint", false},
}; };
@ -472,9 +481,9 @@ TEST_CASE_FIXTURE(Fixture, "return_type_of_f_is_not_widened")
{"LuauSingletonTypes", true}, {"LuauSingletonTypes", true},
{"LuauDiscriminableUnions2", true}, {"LuauDiscriminableUnions2", true},
{"LuauEqConstraint", true}, {"LuauEqConstraint", true},
{"LuauWidenIfSupertypeIsFree", true}, {"LuauWidenIfSupertypeIsFree2", true},
{"LuauWeakEqConstraint", false}, {"LuauWeakEqConstraint", false},
{"LuauDoNotAccidentallyDependOnPointerOrdering", true} {"LuauDoNotAccidentallyDependOnPointerOrdering", true},
}; };
CheckResult result = check(R"( CheckResult result = check(R"(
@ -497,7 +506,7 @@ TEST_CASE_FIXTURE(Fixture, "widening_happens_almost_everywhere")
ScopedFastFlag sff[]{ ScopedFastFlag sff[]{
{"LuauParseSingletonTypes", true}, {"LuauParseSingletonTypes", true},
{"LuauSingletonTypes", true}, {"LuauSingletonTypes", true},
{"LuauWidenIfSupertypeIsFree", true}, {"LuauWidenIfSupertypeIsFree2", true},
}; };
CheckResult result = check(R"( CheckResult result = check(R"(
@ -515,7 +524,7 @@ TEST_CASE_FIXTURE(Fixture, "widening_happens_almost_everywhere_except_for_tables
{"LuauParseSingletonTypes", true}, {"LuauParseSingletonTypes", true},
{"LuauSingletonTypes", true}, {"LuauSingletonTypes", true},
{"LuauDiscriminableUnions2", true}, {"LuauDiscriminableUnions2", true},
{"LuauWidenIfSupertypeIsFree", true}, {"LuauWidenIfSupertypeIsFree2", true},
}; };
CheckResult result = check(R"( CheckResult result = check(R"(
@ -544,7 +553,7 @@ TEST_CASE_FIXTURE(Fixture, "table_insert_with_a_singleton_argument")
ScopedFastFlag sff[]{ ScopedFastFlag sff[]{
{"LuauParseSingletonTypes", true}, {"LuauParseSingletonTypes", true},
{"LuauSingletonTypes", true}, {"LuauSingletonTypes", true},
{"LuauWidenIfSupertypeIsFree", true}, {"LuauWidenIfSupertypeIsFree2", true},
}; };
CheckResult result = check(R"( CheckResult result = check(R"(
@ -565,4 +574,97 @@ TEST_CASE_FIXTURE(Fixture, "table_insert_with_a_singleton_argument")
CHECK_EQ("{string}", toString(requireType("t"))); CHECK_EQ("{string}", toString(requireType("t")));
} }
TEST_CASE_FIXTURE(Fixture, "functions_are_not_to_be_widened")
{
ScopedFastFlag sff[]{
{"LuauParseSingletonTypes", true},
{"LuauSingletonTypes", true},
{"LuauWidenIfSupertypeIsFree2", true},
};
CheckResult result = check(R"(
local function foo(my_enum: "A" | "B") end
)");
LUAU_REQUIRE_NO_ERRORS(result);
CHECK_EQ(R"(("A" | "B") -> ())", toString(requireType("foo")));
}
TEST_CASE_FIXTURE(Fixture, "indexing_on_string_singletons")
{
ScopedFastFlag sff[]{
{"LuauDiscriminableUnions2", true},
{"LuauSingletonTypes", true},
};
CheckResult result = check(R"(
local a: string = "hi"
if a == "hi" then
local x = a:byte()
end
)");
LUAU_REQUIRE_NO_ERRORS(result);
CHECK_EQ(R"("hi")", toString(requireTypeAtPosition({3, 22})));
}
TEST_CASE_FIXTURE(Fixture, "indexing_on_union_of_string_singletons")
{
ScopedFastFlag sff[]{
{"LuauDiscriminableUnions2", true},
{"LuauSingletonTypes", true},
};
CheckResult result = check(R"(
local a: string = "hi"
if a == "hi" or a == "bye" then
local x = a:byte()
end
)");
LUAU_REQUIRE_NO_ERRORS(result);
CHECK_EQ(R"("bye" | "hi")", toString(requireTypeAtPosition({3, 22})));
}
TEST_CASE_FIXTURE(Fixture, "taking_the_length_of_string_singleton")
{
ScopedFastFlag sff[]{
{"LuauDiscriminableUnions2", true},
{"LuauSingletonTypes", true},
};
CheckResult result = check(R"(
local a: string = "hi"
if a == "hi" then
local x = #a
end
)");
LUAU_REQUIRE_NO_ERRORS(result);
CHECK_EQ(R"("hi")", toString(requireTypeAtPosition({3, 23})));
}
TEST_CASE_FIXTURE(Fixture, "taking_the_length_of_union_of_string_singleton")
{
ScopedFastFlag sff[]{
{"LuauDiscriminableUnions2", true},
{"LuauSingletonTypes", true},
};
CheckResult result = check(R"(
local a: string = "hi"
if a == "hi" or a == "bye" then
local x = #a
end
)");
LUAU_REQUIRE_NO_ERRORS(result);
CHECK_EQ(R"("bye" | "hi")", toString(requireTypeAtPosition({3, 23})));
}
TEST_SUITE_END(); TEST_SUITE_END();

View file

@ -2384,4 +2384,504 @@ _ = (_.cos)
LUAU_REQUIRE_ERRORS(result); LUAU_REQUIRE_ERRORS(result);
} }
TEST_CASE_FIXTURE(Fixture, "cannot_call_tables")
{
CheckResult result = check("local foo = {} foo()");
LUAU_REQUIRE_ERROR_COUNT(1, result);
CHECK(get<CannotCallNonFunction>(result.errors[0]) != nullptr);
}
TEST_CASE_FIXTURE(Fixture, "table_length")
{
CheckResult result = check(R"(
local t = {}
local s = #t
)");
LUAU_REQUIRE_NO_ERRORS(result);
CHECK(nullptr != get<TableTypeVar>(requireType("t")));
CHECK_EQ(*typeChecker.numberType, *requireType("s"));
}
TEST_CASE_FIXTURE(Fixture, "nil_assign_doesnt_hit_indexer")
{
CheckResult result = check("local a = {} a[0] = 7 a[0] = nil");
LUAU_REQUIRE_ERROR_COUNT(0, result);
}
TEST_CASE_FIXTURE(Fixture, "wrong_assign_does_hit_indexer")
{
CheckResult result = check("local a = {} a[0] = 7 a[0] = 't'");
LUAU_REQUIRE_ERROR_COUNT(1, result);
CHECK_EQ(result.errors[0], (TypeError{Location{Position{0, 30}, Position{0, 33}}, TypeMismatch{
typeChecker.numberType,
typeChecker.stringType,
}}));
}
TEST_CASE_FIXTURE(Fixture, "nil_assign_doesnt_hit_no_indexer")
{
CheckResult result = check("local a = {a=1, b=2} a['a'] = nil");
LUAU_REQUIRE_ERROR_COUNT(1, result);
CHECK_EQ(result.errors[0], (TypeError{Location{Position{0, 30}, Position{0, 33}}, TypeMismatch{
typeChecker.numberType,
typeChecker.nilType,
}}));
}
TEST_CASE_FIXTURE(Fixture, "free_rhs_table_can_also_be_bound")
{
check(R"(
local o
local v = o:i()
function g(u)
v = u
end
o:f(g)
o:h()
o:h()
)");
}
TEST_CASE_FIXTURE(Fixture, "table_unifies_into_map")
{
CheckResult result = check(R"(
local Instance: any
local UDim2: any
function Create(instanceType)
return function(data)
local obj = Instance.new(instanceType)
for k, v in pairs(data) do
if type(k) == 'number' then
--v.Parent = obj
else
obj[k] = v
end
end
return obj
end
end
local topbarShadow = Create'ImageLabel'{
Name = "TopBarShadow";
Size = UDim2.new(1, 0, 0, 3);
Position = UDim2.new(0, 0, 1, 0);
Image = "rbxasset://textures/ui/TopBar/dropshadow.png";
BackgroundTransparency = 1;
Active = false;
Visible = false;
};
)");
LUAU_REQUIRE_NO_ERRORS(result);
}
TEST_CASE_FIXTURE(Fixture, "tables_get_names_from_their_locals")
{
CheckResult result = check(R"(
local T = {}
)");
LUAU_REQUIRE_NO_ERRORS(result);
CHECK_EQ("T", toString(requireType("T")));
}
TEST_CASE_FIXTURE(Fixture, "generalize_table_argument")
{
CheckResult result = check(R"(
function foo(arr)
local work = {}
for i = 1, #arr do
work[i] = arr[i]
end
return arr
end
)");
LUAU_REQUIRE_NO_ERRORS(result);
dumpErrors(result);
const FunctionTypeVar* fooType = get<FunctionTypeVar>(requireType("foo"));
REQUIRE(fooType);
std::optional<TypeId> fooArg1 = first(fooType->argTypes);
REQUIRE(fooArg1);
const TableTypeVar* fooArg1Table = get<TableTypeVar>(*fooArg1);
REQUIRE(fooArg1Table);
CHECK_EQ(fooArg1Table->state, TableState::Generic);
}
/*
* This test case exposed an oversight in the treatment of free tables.
* Free tables, like free TypeVars, need to record the scope depth where they were created so that
* we do not erroneously let-generalize them when they are used in a nested lambda.
*
* For more information about let-generalization, see <http://okmij.org/ftp/ML/generalization.html>
*
* The important idea here is that the return type of Counter.new is a table with some metatable.
* That metatable *must* be the same TypeVar as the type of Counter. If it is a copy (produced by
* the generalization process), then it loses the knowledge that its metatable will have an :incr()
* method.
*/
TEST_CASE_FIXTURE(Fixture, "dont_quantify_table_that_belongs_to_outer_scope")
{
CheckResult result = check(R"(
local Counter = {}
Counter.__index = Counter
function Counter.new()
local self = setmetatable({count=0}, Counter)
return self
end
function Counter:incr()
self.count = 1
return self.count
end
local self = Counter.new()
print(self:incr())
)");
LUAU_REQUIRE_NO_ERRORS(result);
TableTypeVar* counterType = getMutable<TableTypeVar>(requireType("Counter"));
REQUIRE(counterType);
const FunctionTypeVar* newType = get<FunctionTypeVar>(follow(counterType->props["new"].type));
REQUIRE(newType);
std::optional<TypeId> newRetType = *first(newType->retType);
REQUIRE(newRetType);
const MetatableTypeVar* newRet = get<MetatableTypeVar>(follow(*newRetType));
REQUIRE(newRet);
const TableTypeVar* newRetMeta = get<TableTypeVar>(newRet->metatable);
REQUIRE(newRetMeta);
CHECK(newRetMeta->props.count("incr"));
CHECK_EQ(follow(newRet->metatable), follow(requireType("Counter")));
}
// TODO: CLI-39624
TEST_CASE_FIXTURE(Fixture, "instantiate_tables_at_scope_level")
{
CheckResult result = check(R"(
--!strict
local Option = {}
Option.__index = Option
function Option.Is(obj)
return (type(obj) == "table" and getmetatable(obj) == Option)
end
return Option
)");
LUAU_REQUIRE_NO_ERRORS(result);
}
TEST_CASE_FIXTURE(Fixture, "inferring_crazy_table_should_also_be_quick")
{
CheckResult result = check(R"(
--!strict
function f(U)
U(w:s(an):c()():c():U(s):c():c():U(s):c():U(s):cU()):c():U(s):c():U(s):c():c():U(s):c():U(s):cU()
end
)");
ModulePtr module = getMainModule();
CHECK_GE(100, module->internalTypes.typeVars.size());
}
TEST_CASE_FIXTURE(Fixture, "MixedPropertiesAndIndexers")
{
CheckResult result = check(R"(
local x = {}
x.a = "a"
x[0] = true
x.b = 37
)");
LUAU_REQUIRE_NO_ERRORS(result);
}
TEST_CASE_FIXTURE(Fixture, "setmetatable_cant_be_used_to_mutate_global_types")
{
{
Fixture fix;
// inherit env from parent fixture checker
fix.typeChecker.globalScope = typeChecker.globalScope;
fix.check(R"(
--!nonstrict
type MT = typeof(setmetatable)
function wtf(arg: {MT}): typeof(table)
arg = wtf(arg)
end
)");
}
// validate sharedEnv post-typecheck; valuable for debugging some typeck crashes but slows fuzzing down
// note: it's important for typeck to be destroyed at this point!
{
for (auto& p : typeChecker.globalScope->bindings)
{
toString(p.second.typeId); // toString walks the entire type, making sure ASAN catches access to destroyed type arenas
}
}
}
TEST_CASE_FIXTURE(Fixture, "evil_table_unification")
{
// this code re-infers the type of _ while processing fields of _, which can cause use-after-free
check(R"(
--!nonstrict
_ = ...
_:table(_,string)[_:gsub(_,...,n0)],_,_:gsub(_,string)[""],_:split(_,...,table)._,n0 = nil
do end
)");
}
TEST_CASE_FIXTURE(Fixture, "dont_crash_when_setmetatable_does_not_produce_a_metatabletypevar")
{
CheckResult result = check("local x = setmetatable({})");
LUAU_REQUIRE_ERROR_COUNT(1, result);
}
TEST_CASE_FIXTURE(Fixture, "instantiate_table_cloning")
{
CheckResult result = check(R"(
--!nonstrict
local l0:any,l61:t0<t32> = _,math
while _ do
_()
end
function _():t0<t0>
end
type t0<t32> = any
)");
std::optional<TypeId> ty = requireType("math");
REQUIRE(ty);
const TableTypeVar* ttv = get<TableTypeVar>(*ty);
REQUIRE(ttv);
CHECK(ttv->instantiatedTypeParams.empty());
}
TEST_CASE_FIXTURE(Fixture, "instantiate_table_cloning_2")
{
ScopedFastFlag sff{"LuauOnlyMutateInstantiatedTables", true};
CheckResult result = check(R"(
type X<T> = T
type K = X<typeof(math)>
)");
LUAU_REQUIRE_NO_ERRORS(result);
std::optional<TypeId> ty = requireType("math");
REQUIRE(ty);
const TableTypeVar* ttv = get<TableTypeVar>(*ty);
REQUIRE(ttv);
CHECK(ttv->instantiatedTypeParams.empty());
}
TEST_CASE_FIXTURE(Fixture, "instantiate_table_cloning_3")
{
ScopedFastFlag sff{"LuauOnlyMutateInstantiatedTables", true};
CheckResult result = check(R"(
type X<T> = T
local a = {}
a.x = 4
local b: X<typeof(a)>
a.y = 5
local c: X<typeof(a)>
c = b
)");
LUAU_REQUIRE_NO_ERRORS(result);
std::optional<TypeId> ty = requireType("a");
REQUIRE(ty);
const TableTypeVar* ttv = get<TableTypeVar>(*ty);
REQUIRE(ttv);
CHECK(ttv->instantiatedTypeParams.empty());
}
TEST_CASE_FIXTURE(Fixture, "table_indexing_error_location")
{
CheckResult result = check(R"(
local foo = {42}
local bar: number?
local baz = foo[bar]
)");
LUAU_REQUIRE_ERROR_COUNT(1, result);
CHECK_EQ(result.errors[0].location, Location{Position{3, 16}, Position{3, 19}});
}
TEST_CASE_FIXTURE(Fixture, "table_simple_call")
{
CheckResult result = check(R"(
local a = setmetatable({ x = 2 }, {
__call = function(self)
return (self.x :: number) * 2 -- should work without annotation in the future
end
})
local b = a()
local c = a(2) -- too many arguments
)");
LUAU_REQUIRE_ERROR_COUNT(1, result);
CHECK_EQ("Argument count mismatch. Function expects 1 argument, but 2 are specified", toString(result.errors[0]));
}
TEST_CASE_FIXTURE(Fixture, "access_index_metamethod_that_returns_variadic")
{
CheckResult result = check(R"(
type Foo = {x: string}
local t = {}
setmetatable(t, {
__index = function(x: string): ...Foo
return {x = x}
end
})
local foo = t.bar
)");
LUAU_REQUIRE_NO_ERRORS(result);
ToStringOptions o;
o.exhaustive = true;
CHECK_EQ("{| x: string |}", toString(requireType("foo"), o));
}
TEST_CASE_FIXTURE(Fixture, "dont_invalidate_the_properties_iterator_of_free_table_when_rolled_back")
{
fileResolver.source["Module/Backend/Types"] = R"(
export type Fiber = {
return_: Fiber?
}
return {}
)";
fileResolver.source["Module/Backend"] = R"(
local Types = require(script.Types)
type Fiber = Types.Fiber
type ReactRenderer = { findFiberByHostInstance: () -> Fiber? }
local function attach(renderer): ()
local function getPrimaryFiber(fiber)
local alternate = fiber.alternate
return fiber
end
local function getFiberIDForNative()
local fiber = renderer.findFiberByHostInstance()
fiber = fiber.return_
return getPrimaryFiber(fiber)
end
end
function culprit(renderer: ReactRenderer): ()
attach(renderer)
end
return culprit
)";
CheckResult result = frontend.check("Module/Backend");
}
TEST_CASE_FIXTURE(Fixture, "checked_prop_too_early")
{
CheckResult result = check(R"(
local t: {x: number?}? = {x = nil}
local u = t.x and t or 5
)");
LUAU_REQUIRE_ERROR_COUNT(1, result);
CHECK_EQ("Value of type '{| x: number? |}?' could be nil", toString(result.errors[0]));
CHECK_EQ("number | {| x: number? |}", toString(requireType("u")));
}
TEST_CASE_FIXTURE(Fixture, "accidentally_checked_prop_in_opposite_branch")
{
CheckResult result = check(R"(
local t: {x: number?}? = {x = nil}
local u = t and t.x == 5 or t.x == 31337
)");
LUAU_REQUIRE_ERROR_COUNT(1, result);
CHECK_EQ("Value of type '{| x: number? |}?' could be nil", toString(result.errors[0]));
CHECK_EQ("boolean", toString(requireType("u")));
}
/*
* We had an issue where part of the type of pairs() was an unsealed table.
* This test depends on FFlagDebugLuauFreezeArena to trigger it.
*/
TEST_CASE_FIXTURE(Fixture, "pairs_parameters_are_not_unsealed_tables")
{
check(R"(
function _(l0:{n0:any})
_ = pairs
end
)");
}
TEST_CASE_FIXTURE(Fixture, "table_function_check_use_after_free")
{
CheckResult result = check(R"(
local t = {}
function t.x(value)
for k,v in pairs(t) do end
end
)");
LUAU_REQUIRE_NO_ERRORS(result);
}
/*
* When we add new properties to an unsealed table, we should do a level check and promote the property type to be at
* the level of the table.
*/
TEST_CASE_FIXTURE(Fixture, "inferred_properties_of_a_table_should_start_with_the_same_TypeLevel_of_that_table")
{
CheckResult result = check(R"(
--!strict
local T = {}
local function f(prop)
T[1] = {
prop = prop,
}
end
local function g()
local l = T[1].prop
end
)");
LUAU_REQUIRE_NO_ERRORS(result);
}
TEST_SUITE_END(); TEST_SUITE_END();

File diff suppressed because it is too large Load diff

View file

@ -895,4 +895,87 @@ caused by:
Type 'boolean' could not be converted into 'string')"); Type 'boolean' could not be converted into 'string')");
} }
// TODO: File a Jira about this
/*
TEST_CASE_FIXTURE(Fixture, "unifying_vararg_pack_with_fixed_length_pack_produces_fixed_length_pack")
{
CheckResult result = check(R"(
function a(x) return 1 end
a(...)
)");
LUAU_REQUIRE_NO_ERRORS(result);
REQUIRE(bool(getMainModule()->getModuleScope()->varargPack));
TypePackId varargPack = *getMainModule()->getModuleScope()->varargPack;
auto iter = begin(varargPack);
auto endIter = end(varargPack);
CHECK(iter != endIter);
++iter;
CHECK(iter == endIter);
CHECK(!iter.tail());
}
*/
TEST_CASE_FIXTURE(Fixture, "dont_ice_if_a_TypePack_is_an_error")
{
CheckResult result = check(R"(
--!strict
function f(s)
print(s)
return f
end
f("foo")("bar")
)");
}
TEST_CASE_FIXTURE(Fixture, "cyclic_type_packs")
{
// this has a risk of creating cyclic type packs, causing infinite loops / OOMs
check(R"(
--!nonstrict
_ += _(_,...)
repeat
_ += _(...)
until ... + _
)");
check(R"(
--!nonstrict
_ += _(_(...,...),_(...))
repeat
until _
)");
}
TEST_CASE_FIXTURE(Fixture, "detect_cyclic_typepacks")
{
CheckResult result = check(R"(
type ( ... ) ( ) ;
( ... ) ( - - ... ) ( - ... )
type = ( ... ) ;
( ... ) ( ) ( ... ) ;
( ... ) ""
)");
CHECK_LE(0, result.errors.size());
}
TEST_CASE_FIXTURE(Fixture, "detect_cyclic_typepacks2")
{
CheckResult result = check(R"(
function _(l0:((typeof((pcall)))|((((t0)->())|(typeof(-67108864)))|(any)))|(any),...):(((typeof(0))|(any))|(any),typeof(-67108864),any)
xpcall(_,_,_)
_(_,_,_)
end
)");
CHECK_LE(0, result.errors.size());
}
TEST_SUITE_END(); TEST_SUITE_END();

View file

@ -496,4 +496,20 @@ caused by:
None of the union options are compatible. For example: Table type 'a' not compatible with type 'X' because the former is missing field 'x')"); None of the union options are compatible. For example: Table type 'a' not compatible with type 'X' because the former is missing field 'x')");
} }
// We had a bug where a cyclic union caused a stack overflow.
// ex type U = number | U
TEST_CASE_FIXTURE(Fixture, "dont_allow_cyclic_unions_to_be_inferred")
{
CheckResult result = check(R"(
--!strict
function f(a, b)
a:g(b or {})
a:g(b)
end
)");
LUAU_REQUIRE_NO_ERRORS(result);
}
TEST_SUITE_END(); TEST_SUITE_END();

View file

@ -833,6 +833,17 @@ assert((function()
return sum return sum
end)() == 105) end)() == 105)
-- shrinking array part
assert((function()
local t = table.create(100, 42)
for i=1,90 do t[i] = nil end
t[101] = 42
local sum = 0
for _,v in ipairs(t) do sum += v end
for _,v in pairs(t) do sum += v end
return sum
end)() == 462)
-- upvalues: recursive capture -- upvalues: recursive capture
assert((function() local function fact(n) return n < 1 and 1 or n * fact(n-1) end return fact(5) end)() == 120) assert((function() local function fact(n) return n < 1 and 1 or n * fact(n-1) end return fact(5) end)() == 120)
@ -881,6 +892,14 @@ end)() == "6,8,10")
-- typeof == type in absence of custom userdata -- typeof == type in absence of custom userdata
assert(concat(typeof(5), typeof(nil), typeof({}), typeof(newproxy())) == "number,nil,table,userdata") assert(concat(typeof(5), typeof(nil), typeof({}), typeof(newproxy())) == "number,nil,table,userdata")
-- type/typeof/newproxy interaction with metatables: __type doesn't work intentionally to avoid spoofing
assert((function()
local ud = newproxy(true)
getmetatable(ud).__type = "number"
return concat(type(ud),typeof(ud))
end)() == "userdata,userdata")
testgetfenv() -- DONT MOVE THIS LINE testgetfenv() -- DONT MOVE THIS LINE
return 'OK' return 'OK'

View file

@ -3,14 +3,14 @@ print "testing debugger" -- note, this file can't run in isolation from C tests
local a = 5 local a = 5
function foo(b) function foo(b, ...)
print("in foo", b) print("in foo", b)
a = 6 a = 6
end end
breakpoint(8) breakpoint(8)
foo(50) foo(50, 42)
breakpoint(16) -- next line breakpoint(16) -- next line
print("here") print("here")

View file

@ -305,4 +305,6 @@ assert(ecall(function() return "a" + "b" end) == "attempt to perform arithmetic
assert(ecall(function() return 1 > nil end) == "attempt to compare nil < number") -- note reversed order (by design) assert(ecall(function() return 1 > nil end) == "attempt to compare nil < number") -- note reversed order (by design)
assert(ecall(function() return "a" <= 5 end) == "attempt to compare string <= number") assert(ecall(function() return "a" <= 5 end) == "attempt to compare string <= number")
assert(ecall(function() local t = {} setmetatable(t, { __newindex = function(t,i,v) end }) t[nil] = 2 end) == "table index is nil")
return('OK') return('OK')

View file

@ -0,0 +1,11 @@
-- This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details
print("testing interrupts")
function foo()
for i=1,10 do end
return
end
foo()
return "OK"

View file

@ -1,2 +0,0 @@
type synthetic add -x "^Luau::Variant<.+>$" -l LuauVisualize.LuauVariantSyntheticChildrenProvider
type summary add -x "^Luau::Variant<.+>$" -l LuauVisualize.luau_variant_summary

View file

@ -0,0 +1,2 @@
type synthetic add -x "^Luau::Variant<.+>$" -l lldb_formatters.LuauVariantSyntheticChildrenProvider
type summary add -x "^Luau::Variant<.+>$" -l lldb_formatters.luau_variant_summary