mirror of
https://github.com/luau-lang/luau.git
synced 2024-11-15 06:15:44 +08:00
Sync to upstream/release/519 (#422)
This commit is contained in:
parent
4fdead5e3c
commit
362428f8b4
@ -107,6 +107,7 @@ struct FunctionDoesNotTakeSelf
|
||||
|
||||
struct FunctionRequiresSelf
|
||||
{
|
||||
// TODO: Delete with LuauAnyInIsOptionalIsOptional
|
||||
int requiredExtraNils = 0;
|
||||
|
||||
bool operator==(const FunctionRequiresSelf& rhs) const;
|
||||
|
@ -86,6 +86,8 @@ private:
|
||||
void tryUnifyIndexer(const TableIndexer& subIndexer, const TableIndexer& superIndexer);
|
||||
|
||||
TypeId widen(TypeId ty);
|
||||
TypePackId widen(TypePackId tp);
|
||||
|
||||
TypeId deeplyOptional(TypeId ty, std::unordered_map<TypeId, TypeId> seen = {});
|
||||
|
||||
void cacheResult(TypeId subTy, TypeId superTy);
|
||||
|
@ -14,6 +14,7 @@
|
||||
#include <utility>
|
||||
|
||||
LUAU_FASTFLAGVARIABLE(LuauIfElseExprFixCompletionIssue, false);
|
||||
LUAU_FASTFLAG(LuauSelfCallAutocompleteFix)
|
||||
|
||||
static const std::unordered_set<std::string> kStatementStartingKeywords = {
|
||||
"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;
|
||||
}
|
||||
|
||||
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)
|
||||
{
|
||||
ty = follow(ty);
|
||||
|
||||
auto canUnify = [&typeArena](TypeId subTy, TypeId superTy) {
|
||||
LUAU_ASSERT(!FFlag::LuauSelfCallAutocompleteFix);
|
||||
|
||||
InternalErrorReporter iceReporter;
|
||||
UnifierSharedState unifierState(&iceReporter);
|
||||
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);
|
||||
|
||||
auto checkFunctionType = [&canUnify, &expectedType](const FunctionTypeVar* ftv) {
|
||||
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)
|
||||
auto checkFunctionType = [typeArena, &canUnify, &expectedType](const FunctionTypeVar* ftv) {
|
||||
if (FFlag::LuauSelfCallAutocompleteFix)
|
||||
{
|
||||
if (const VariadicTypePack* vtp = get<VariadicTypePack>(follow(*retTail)); vtp && canUnify(vtp->ty, expectedType))
|
||||
return true;
|
||||
}
|
||||
if (std::optional<TypeId> firstRetTy = first(ftv->retType))
|
||||
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
|
||||
@ -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
|
||||
@ -291,16 +316,22 @@ enum class PropIndexType
|
||||
Key,
|
||||
};
|
||||
|
||||
static void autocompleteProps(const Module& module, TypeArena* typeArena, TypeId ty, PropIndexType indexType, const std::vector<AstNode*>& nodes,
|
||||
AutocompleteEntryMap& result, std::unordered_set<TypeId>& seen, std::optional<const ClassTypeVar*> containingClass = std::nullopt)
|
||||
static void autocompleteProps(const Module& module, TypeArena* typeArena, TypeId rootTy, TypeId ty, PropIndexType indexType,
|
||||
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);
|
||||
|
||||
if (seen.count(ty))
|
||||
return;
|
||||
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)
|
||||
return false;
|
||||
|
||||
@ -331,6 +362,48 @@ static void autocompleteProps(const Module& module, TypeArena* typeArena, TypeId
|
||||
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) {
|
||||
for (const auto& [name, prop] : props)
|
||||
@ -349,7 +422,7 @@ static void autocompleteProps(const Module& module, TypeArena* typeArena, TypeId
|
||||
AutocompleteEntryKind::Property,
|
||||
type,
|
||||
prop.deprecated,
|
||||
isWrongIndexer(type),
|
||||
FFlag::LuauSelfCallAutocompleteFix ? isWrongIndexer(type) : isWrongIndexer_DEPRECATED(type),
|
||||
typeCorrect,
|
||||
containingClass,
|
||||
&prop,
|
||||
@ -361,34 +434,60 @@ static void autocompleteProps(const Module& module, TypeArena* typeArena, TypeId
|
||||
}
|
||||
};
|
||||
|
||||
if (auto cls = get<ClassTypeVar>(ty))
|
||||
{
|
||||
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 fillMetatableProps = [&](const TableTypeVar* mtable) {
|
||||
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, followed, indexType, nodes, result, seen);
|
||||
{
|
||||
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, *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;
|
||||
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)
|
||||
result.insert(pair);
|
||||
@ -423,14 +522,17 @@ static void autocompleteProps(const Module& module, TypeArena* typeArena, TypeId
|
||||
if (iter == endIter)
|
||||
return;
|
||||
|
||||
autocompleteProps(module, typeArena, *iter, indexType, nodes, result, seen);
|
||||
autocompleteProps(module, typeArena, rootTy, *iter, indexType, nodes, result, seen);
|
||||
|
||||
++iter;
|
||||
|
||||
while (iter != endIter)
|
||||
{
|
||||
AutocompleteEntryMap inner;
|
||||
std::unordered_set<TypeId> innerSeen = seen;
|
||||
std::unordered_set<TypeId> innerSeen;
|
||||
|
||||
if (!FFlag::LuauSelfCallAutocompleteFix)
|
||||
innerSeen = seen;
|
||||
|
||||
if (isNil(*iter))
|
||||
{
|
||||
@ -438,7 +540,7 @@ static void autocompleteProps(const Module& module, TypeArena* typeArena, TypeId
|
||||
continue;
|
||||
}
|
||||
|
||||
autocompleteProps(module, typeArena, *iter, indexType, nodes, inner, innerSeen);
|
||||
autocompleteProps(module, typeArena, rootTy, *iter, indexType, nodes, inner, innerSeen);
|
||||
|
||||
std::unordered_set<std::string> toRemove;
|
||||
|
||||
@ -455,6 +557,18 @@ static void autocompleteProps(const Module& module, TypeArena* typeArena, TypeId
|
||||
++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(
|
||||
@ -482,7 +596,7 @@ static void autocompleteProps(
|
||||
const Module& module, TypeArena* typeArena, TypeId ty, PropIndexType indexType, const std::vector<AstNode*>& nodes, AutocompleteEntryMap& result)
|
||||
{
|
||||
std::unordered_set<TypeId> seen;
|
||||
autocompleteProps(module, typeArena, ty, indexType, nodes, result, seen);
|
||||
autocompleteProps(module, typeArena, ty, ty, indexType, nodes, result, seen);
|
||||
}
|
||||
|
||||
AutocompleteEntryMap autocompleteProps(
|
||||
@ -1352,7 +1466,7 @@ static AutocompleteResult autocomplete(const SourceModule& sourceModule, const M
|
||||
TypeId ty = follow(*it);
|
||||
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),
|
||||
finder.ancestry};
|
||||
else
|
||||
|
@ -95,104 +95,104 @@ declare os: {
|
||||
|
||||
declare function require(target: any): any
|
||||
|
||||
declare function getfenv(target: any?): { [string]: any }
|
||||
declare function getfenv(target: any): { [string]: any }
|
||||
|
||||
declare _G: any
|
||||
declare _VERSION: string
|
||||
|
||||
declare function gcinfo(): number
|
||||
|
||||
declare function print<T...>(...: T...)
|
||||
declare function print<T...>(...: T...)
|
||||
|
||||
declare function type<T>(value: T): string
|
||||
declare function typeof<T>(value: T): string
|
||||
|
||||
-- `assert` has a magic function attached that will give more detailed type information
|
||||
declare function assert<T>(value: T, errorMessage: string?): T
|
||||
declare function type<T>(value: T): string
|
||||
declare function typeof<T>(value: T): string
|
||||
|
||||
declare function error<T>(message: T, level: number?)
|
||||
-- `assert` has a magic function attached that will give more detailed type information
|
||||
declare function assert<T>(value: T, errorMessage: string?): T
|
||||
|
||||
declare function tostring<T>(value: T): string
|
||||
declare function tonumber<T>(value: T, radix: number?): number?
|
||||
declare function error<T>(message: T, level: number?)
|
||||
|
||||
declare function rawequal<T1, T2>(a: T1, b: T2): boolean
|
||||
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 tostring<T>(value: T): string
|
||||
declare function tonumber<T>(value: T, radix: number?): number?
|
||||
|
||||
declare function setfenv<T..., R...>(target: number | (T...) -> R..., env: {[string]: any}): ((T...) -> R...)?
|
||||
declare function rawequal<T1, T2>(a: T1, b: T2): boolean
|
||||
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 ipairs<V>(tab: {V}): (({V}, number) -> (number, V), {V}, number)
|
||||
declare function setfenv<T..., R...>(target: number | (T...) -> R..., env: {[string]: any}): ((T...) -> R...)?
|
||||
|
||||
declare function pcall<A..., R...>(f: (A...) -> R..., ...: A...): (boolean, R...)
|
||||
declare function ipairs<V>(tab: {V}): (({V}, number) -> (number, V), {V}, number)
|
||||
|
||||
-- FIXME: The actual type of `xpcall` is:
|
||||
-- <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...).
|
||||
declare function xpcall<E, A..., R1..., R2...>(f: (A...) -> R1..., err: (E) -> R2..., ...: A...): (boolean, R1...)
|
||||
declare function pcall<A..., R...>(f: (A...) -> R..., ...: A...): (boolean, R...)
|
||||
|
||||
-- `select` has a magic function attached to provide more detailed type information
|
||||
declare function select<A...>(i: string | number, ...: A...): ...any
|
||||
-- FIXME: The actual type of `xpcall` is:
|
||||
-- <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...).
|
||||
declare function xpcall<E, A..., R1..., R2...>(f: (A...) -> R1..., err: (E) -> R2..., ...: A...): (boolean, R1...)
|
||||
|
||||
-- FIXME: This type is not entirely correct - `loadstring` returns a function or
|
||||
-- (nil, string).
|
||||
declare function loadstring<A...>(src: string, chunkname: string?): (((A...) -> any)?, string?)
|
||||
-- `select` has a magic function attached to provide more detailed type information
|
||||
declare function select<A...>(i: string | number, ...: A...): ...any
|
||||
|
||||
declare function newproxy(mt: boolean?): any
|
||||
-- FIXME: This type is not entirely correct - `loadstring` returns a function or
|
||||
-- (nil, string).
|
||||
declare function loadstring<A...>(src: string, chunkname: string?): (((A...) -> any)?, string?)
|
||||
|
||||
declare coroutine: {
|
||||
create: <A..., R...>((A...) -> R...) -> thread,
|
||||
resume: <A..., R...>(thread, A...) -> (boolean, R...),
|
||||
running: () -> thread,
|
||||
status: (thread) -> string,
|
||||
-- FIXME: This technically returns a function, but we can't represent this yet.
|
||||
wrap: <A..., R...>((A...) -> R...) -> any,
|
||||
yield: <A..., R...>(A...) -> R...,
|
||||
isyieldable: () -> boolean,
|
||||
close: (thread) -> (boolean, any?)
|
||||
}
|
||||
declare function newproxy(mt: boolean?): any
|
||||
|
||||
declare table: {
|
||||
concat: <V>({V}, string?, number?, number?) -> string,
|
||||
insert: (<V>({V}, V) -> ()) & (<V>({V}, number, V) -> ()),
|
||||
maxn: <V>({V}) -> number,
|
||||
remove: <V>({V}, number?) -> V?,
|
||||
sort: <V>({V}, ((V, V) -> boolean)?) -> (),
|
||||
create: <V>(number, V?) -> {V},
|
||||
find: <V>({V}, V, number?) -> number?,
|
||||
declare coroutine: {
|
||||
create: <A..., R...>((A...) -> R...) -> thread,
|
||||
resume: <A..., R...>(thread, A...) -> (boolean, R...),
|
||||
running: () -> thread,
|
||||
status: (thread) -> string,
|
||||
-- FIXME: This technically returns a function, but we can't represent this yet.
|
||||
wrap: <A..., R...>((A...) -> R...) -> any,
|
||||
yield: <A..., R...>(A...) -> R...,
|
||||
isyieldable: () -> boolean,
|
||||
close: (thread) -> (boolean, any)
|
||||
}
|
||||
|
||||
unpack: <V>({V}, number?, number?) -> ...V,
|
||||
pack: <V>(...V) -> { n: number, [number]: V },
|
||||
declare table: {
|
||||
concat: <V>({V}, string?, number?, number?) -> string,
|
||||
insert: (<V>({V}, V) -> ()) & (<V>({V}, number, V) -> ()),
|
||||
maxn: <V>({V}) -> number,
|
||||
remove: <V>({V}, number?) -> V?,
|
||||
sort: <V>({V}, ((V, V) -> boolean)?) -> (),
|
||||
create: <V>(number, V?) -> {V},
|
||||
find: <V>({V}, V, number?) -> number?,
|
||||
|
||||
getn: <V>({V}) -> number,
|
||||
foreach: <K, V>({[K]: V}, (K, V) -> ()) -> (),
|
||||
foreachi: <V>({V}, (number, V) -> ()) -> (),
|
||||
unpack: <V>({V}, number?, number?) -> ...V,
|
||||
pack: <V>(...V) -> { n: number, [number]: V },
|
||||
|
||||
move: <V>({V}, number, number, number, {V}?) -> {V},
|
||||
clear: <K, V>({[K]: V}) -> (),
|
||||
getn: <V>({V}) -> number,
|
||||
foreach: <K, V>({[K]: V}, (K, V) -> ()) -> (),
|
||||
foreachi: <V>({V}, (number, V) -> ()) -> (),
|
||||
|
||||
isfrozen: <K, V>({[K]: V}) -> boolean,
|
||||
}
|
||||
move: <V>({V}, number, number, number, {V}?) -> {V},
|
||||
clear: <K, V>({[K]: V}) -> (),
|
||||
|
||||
declare debug: {
|
||||
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),
|
||||
}
|
||||
isfrozen: <K, V>({[K]: V}) -> boolean,
|
||||
}
|
||||
|
||||
declare utf8: {
|
||||
char: (number, ...number) -> string,
|
||||
charpattern: string,
|
||||
codes: (string) -> ((string, number) -> (number, number), string, number),
|
||||
-- FIXME
|
||||
codepoint: (string, number?, number?) -> (number, ...number),
|
||||
len: (string, number?, number?) -> (number?, number?),
|
||||
offset: (string, number?, number?) -> number,
|
||||
nfdnormalize: (string) -> string,
|
||||
nfcnormalize: (string) -> string,
|
||||
graphemes: (string, number?, number?) -> (() -> (number, number)),
|
||||
}
|
||||
declare debug: {
|
||||
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),
|
||||
}
|
||||
|
||||
-- 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 utf8: {
|
||||
char: (number, ...number) -> string,
|
||||
charpattern: string,
|
||||
codes: (string) -> ((string, number) -> (number, number), string, number),
|
||||
-- FIXME
|
||||
codepoint: (string, number?, number?) -> (number, ...number),
|
||||
len: (string, number?, number?) -> (number?, number?),
|
||||
offset: (string, number?, number?) -> number,
|
||||
nfdnormalize: (string) -> string,
|
||||
nfcnormalize: (string) -> string,
|
||||
graphemes: (string, number?, number?) -> (() -> (number, number)),
|
||||
}
|
||||
|
||||
-- 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
|
||||
|
||||
)BUILTIN_SRC";
|
||||
|
||||
|
@ -7,6 +7,8 @@
|
||||
|
||||
#include <stdexcept>
|
||||
|
||||
LUAU_FASTFLAGVARIABLE(BetterDiagnosticCodesInStudio, false);
|
||||
|
||||
static std::string wrongNumberOfArgsString(size_t expectedCount, size_t actualCount, const char* argPrefix = nullptr, bool isVariadic = false)
|
||||
{
|
||||
std::string s = "expects ";
|
||||
@ -223,7 +225,14 @@ struct ErrorConverter
|
||||
|
||||
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
|
||||
|
@ -14,6 +14,7 @@
|
||||
LUAU_FASTFLAGVARIABLE(DebugLuauFreezeArena, false)
|
||||
LUAU_FASTFLAGVARIABLE(DebugLuauTrackOwningArena, false) // Remove with FFlagLuauImmutableTypes
|
||||
LUAU_FASTINTVARIABLE(LuauTypeCloneRecursionLimit, 300)
|
||||
LUAU_FASTFLAGVARIABLE(LuauCloneDeclaredGlobals, false)
|
||||
LUAU_FASTFLAG(LuauImmutableTypes)
|
||||
|
||||
namespace Luau
|
||||
@ -536,6 +537,12 @@ bool Module::clonePublicInterface()
|
||||
if (get<GenericTypeVar>(follow(ty)))
|
||||
*asMutable(ty) = AnyTypeVar{};
|
||||
|
||||
if (FFlag::LuauCloneDeclaredGlobals)
|
||||
{
|
||||
for (auto& [name, ty] : declaredGlobals)
|
||||
ty = clone(ty, interfaceTypes, seenTypes, seenTypePacks, cloneState);
|
||||
}
|
||||
|
||||
freeze(internalTypes);
|
||||
freeze(interfaceTypes);
|
||||
|
||||
|
@ -29,22 +29,24 @@ LUAU_FASTFLAGVARIABLE(LuauRecursiveTypeParameterRestriction, false)
|
||||
LUAU_FASTFLAGVARIABLE(LuauGenericFunctionsDontCacheTypeParams, false)
|
||||
LUAU_FASTFLAGVARIABLE(LuauImmutableTypes, false)
|
||||
LUAU_FASTFLAGVARIABLE(LuauSealExports, false)
|
||||
LUAU_FASTFLAGVARIABLE(LuauSelfCallAutocompleteFix, false)
|
||||
LUAU_FASTFLAGVARIABLE(LuauSingletonTypes, false)
|
||||
LUAU_FASTFLAGVARIABLE(LuauDiscriminableUnions2, false)
|
||||
LUAU_FASTFLAGVARIABLE(LuauExpectedTypesOfProperties, false)
|
||||
LUAU_FASTFLAGVARIABLE(LuauErrorRecoveryType, false)
|
||||
LUAU_FASTFLAGVARIABLE(LuauOnlyMutateInstantiatedTables, false)
|
||||
LUAU_FASTFLAGVARIABLE(LuauPropertiesGetExpectedType, false)
|
||||
LUAU_FASTFLAGVARIABLE(LuauStatFunctionSimplify, false)
|
||||
LUAU_FASTFLAGVARIABLE(LuauStatFunctionSimplify2, false)
|
||||
LUAU_FASTFLAGVARIABLE(LuauUnsealedTableLiteral, false)
|
||||
LUAU_FASTFLAGVARIABLE(LuauTwoPassAliasDefinitionFix, false)
|
||||
LUAU_FASTFLAGVARIABLE(LuauAssertStripsFalsyTypes, false)
|
||||
LUAU_FASTFLAGVARIABLE(LuauReturnAnyInsteadOfICE, false) // Eventually removed as false.
|
||||
LUAU_FASTFLAG(LuauWidenIfSupertypeIsFree)
|
||||
LUAU_FASTFLAG(LuauWidenIfSupertypeIsFree2)
|
||||
LUAU_FASTFLAGVARIABLE(LuauDoNotTryToReduce, false)
|
||||
LUAU_FASTFLAGVARIABLE(LuauDoNotAccidentallyDependOnPointerOrdering, false)
|
||||
LUAU_FASTFLAGVARIABLE(LuauFixArgumentCountMismatchAmountWithGenericTypes, false)
|
||||
LUAU_FASTFLAGVARIABLE(LuauFixIncorrectLineNumberDuplicateType, false)
|
||||
LUAU_FASTFLAG(LuauAnyInIsOptionalIsOptional)
|
||||
|
||||
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};
|
||||
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;
|
||||
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}});
|
||||
}
|
||||
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);
|
||||
|
||||
@ -1134,7 +1139,7 @@ void TypeChecker::check(const ScopePtr& scope, TypeId ty, const ScopePtr& funSco
|
||||
if (ttv && ttv->state != TableState::Sealed)
|
||||
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>());
|
||||
|
||||
@ -1144,7 +1149,7 @@ void TypeChecker::check(const ScopePtr& scope, TypeId ty, const ScopePtr& funSco
|
||||
}
|
||||
else if (function.func->self)
|
||||
{
|
||||
LUAU_ASSERT(!FFlag::LuauStatFunctionSimplify);
|
||||
LUAU_ASSERT(!FFlag::LuauStatFunctionSimplify2);
|
||||
|
||||
AstExprIndexName* indexName = function.name->as<AstExprIndexName>();
|
||||
if (!indexName)
|
||||
@ -1183,7 +1188,7 @@ void TypeChecker::check(const ScopePtr& scope, TypeId ty, const ScopePtr& funSco
|
||||
}
|
||||
else
|
||||
{
|
||||
LUAU_ASSERT(!FFlag::LuauStatFunctionSimplify);
|
||||
LUAU_ASSERT(!FFlag::LuauStatFunctionSimplify2);
|
||||
|
||||
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->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))
|
||||
{
|
||||
bool hasNil = false;
|
||||
|
||||
for (TypeId option : utv)
|
||||
if (FFlag::LuauAnyInIsOptionalIsOptional)
|
||||
{
|
||||
if (isNil(option))
|
||||
{
|
||||
hasNil = true;
|
||||
break;
|
||||
}
|
||||
if (!std::any_of(begin(utv), end(utv), isNil))
|
||||
return ty;
|
||||
}
|
||||
else
|
||||
{
|
||||
bool hasNil = false;
|
||||
|
||||
if (!hasNil)
|
||||
return ty;
|
||||
for (TypeId option : utv)
|
||||
{
|
||||
if (isNil(option))
|
||||
{
|
||||
hasNil = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!hasNil)
|
||||
return ty;
|
||||
}
|
||||
|
||||
std::vector<TypeId> result;
|
||||
|
||||
@ -1916,14 +1932,34 @@ std::optional<TypeId> TypeChecker::tryStripUnionFromNil(TypeId ty)
|
||||
|
||||
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});
|
||||
return follow(*strippedUnion);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (isOptional(ty))
|
||||
{
|
||||
if (std::optional<TypeId> strippedUnion = tryStripUnionFromNil(follow(ty)))
|
||||
{
|
||||
reportError(location, OptionalValueAccess{ty});
|
||||
return follow(*strippedUnion);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return ty;
|
||||
}
|
||||
@ -2935,9 +2971,25 @@ TypeId TypeChecker::checkFunctionName(const ScopePtr& scope, AstExpr& funName, T
|
||||
return errorRecoveryType(scope);
|
||||
}
|
||||
|
||||
// Cannot extend sealed table, but we dont report an error here because it will be reported during AstStatFunction check
|
||||
if (lhsType->persistent || ttv->state == TableState::Sealed)
|
||||
return errorRecoveryType(scope);
|
||||
if (FFlag::LuauStatFunctionSimplify2)
|
||||
{
|
||||
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;
|
||||
|
||||
@ -3393,7 +3445,7 @@ void TypeChecker::checkArgumentList(
|
||||
else if (state.log.getMutable<ErrorTypeVar>(t))
|
||||
{
|
||||
} // ok
|
||||
else if (isNonstrictMode() && state.log.getMutable<AnyTypeVar>(t))
|
||||
else if (!FFlag::LuauAnyInIsOptionalIsOptional && isNonstrictMode() && state.log.getMutable<AnyTypeVar>(t))
|
||||
{
|
||||
} // ok
|
||||
else
|
||||
@ -3467,7 +3519,11 @@ void TypeChecker::checkArgumentList(
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
else if (state.log.getMutable<FreeTypePack>(tail))
|
||||
@ -3542,6 +3598,23 @@ ExprResult<TypePackId> TypeChecker::checkExprPack(const ScopePtr& scope, const A
|
||||
|
||||
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.
|
||||
// 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.
|
||||
@ -3550,8 +3623,6 @@ ExprResult<TypePackId> TypeChecker::checkExprPack(const ScopePtr& scope, const A
|
||||
|
||||
std::vector<TypeId> overloads = flattenIntersection(actualFunctionType);
|
||||
|
||||
TypePackId retPack = freshTypePack(scope->level);
|
||||
|
||||
std::vector<std::optional<TypeId>> expectedTypes = getExpectedTypesForCall(overloads, expr.args.size, expr.self);
|
||||
|
||||
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
|
||||
// unify it with a monomorphic function.
|
||||
TypeId r = addType(FunctionTypeVar(scope->level, argPack, retPack));
|
||||
if (FFlag::LuauWidenIfSupertypeIsFree)
|
||||
if (FFlag::LuauWidenIfSupertypeIsFree2)
|
||||
{
|
||||
UnifierOptions options;
|
||||
options.isFunctionCall = true;
|
||||
@ -3772,7 +3843,7 @@ std::optional<ExprResult<TypePackId>> TypeChecker::checkCallOverload(const Scope
|
||||
{
|
||||
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
|
||||
// the function is declared with colon notation AND we use dot notation, warn.
|
||||
|
@ -26,6 +26,7 @@ LUAU_FASTINT(LuauTypeInferRecursionLimit)
|
||||
LUAU_FASTFLAG(LuauErrorRecoveryType)
|
||||
LUAU_FASTFLAG(LuauSubtypingAddOptPropsToUnsealedTables)
|
||||
LUAU_FASTFLAG(LuauDiscriminableUnions2)
|
||||
LUAU_FASTFLAGVARIABLE(LuauAnyInIsOptionalIsOptional, false)
|
||||
|
||||
namespace Luau
|
||||
{
|
||||
@ -201,11 +202,16 @@ bool isOptional(TypeId ty)
|
||||
if (isNil(ty))
|
||||
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)
|
||||
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)
|
||||
|
@ -20,11 +20,13 @@ LUAU_FASTFLAGVARIABLE(LuauTableSubtypingVariance2, false);
|
||||
LUAU_FASTFLAG(LuauSingletonTypes)
|
||||
LUAU_FASTFLAG(LuauErrorRecoveryType);
|
||||
LUAU_FASTFLAGVARIABLE(LuauSubtypingAddOptPropsToUnsealedTables, false)
|
||||
LUAU_FASTFLAGVARIABLE(LuauWidenIfSupertypeIsFree, false)
|
||||
LUAU_FASTFLAGVARIABLE(LuauWidenIfSupertypeIsFree2, false)
|
||||
LUAU_FASTFLAGVARIABLE(LuauDifferentOrderOfUnificationDoesntMatter, false)
|
||||
LUAU_FASTFLAGVARIABLE(LuauTxnLogSeesTypePacks2, false)
|
||||
LUAU_FASTFLAGVARIABLE(LuauTxnLogCheckForInvalidation, false)
|
||||
LUAU_FASTFLAGVARIABLE(LuauTxnLogRefreshFunctionPointers, false)
|
||||
LUAU_FASTFLAGVARIABLE(LuauTxnLogDontRetryForIndexers, false)
|
||||
LUAU_FASTFLAG(LuauAnyInIsOptionalIsOptional)
|
||||
|
||||
namespace Luau
|
||||
{
|
||||
@ -272,12 +274,6 @@ TypePackId Widen::clean(TypePackId)
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
@ -990,7 +986,7 @@ void Unifier::tryUnify_(TypePackId subTp, TypePackId superTp, bool isFunctionCal
|
||||
|
||||
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))
|
||||
@ -1107,7 +1103,7 @@ void Unifier::tryUnify_(TypePackId subTp, TypePackId superTp, bool isFunctionCal
|
||||
}
|
||||
|
||||
// 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();
|
||||
continue;
|
||||
@ -1280,6 +1276,13 @@ void Unifier::tryUnifyFunctions(TypeId subTy, TypeId superTy, bool isFunctionCal
|
||||
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 (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);
|
||||
|
||||
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)
|
||||
missingProperties.push_back(propName);
|
||||
if (subIter == subTable->props.end() && (!FFlag::LuauSubtypingAddOptPropsToUnsealedTables || subTable->state == TableState::Unsealed) && !isOptional(superProp.type) && !isAny)
|
||||
missingProperties.push_back(propName);
|
||||
}
|
||||
}
|
||||
|
||||
if (!missingProperties.empty())
|
||||
@ -1378,9 +1389,17 @@ void Unifier::tryUnifyTables(TypeId subTy, TypeId superTy, bool isIntersection)
|
||||
{
|
||||
auto superIter = superTable->props.find(propName);
|
||||
|
||||
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 (FFlag::LuauAnyInIsOptionalIsOptional)
|
||||
{
|
||||
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())
|
||||
@ -1424,6 +1443,12 @@ void Unifier::tryUnifyTables(TypeId subTy, TypeId superTy, bool isIntersection)
|
||||
if (innerState.errors.empty())
|
||||
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))))
|
||||
// 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`.
|
||||
@ -1497,6 +1522,9 @@ void Unifier::tryUnifyTables(TypeId subTy, TypeId superTy, bool isIntersection)
|
||||
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))))
|
||||
{
|
||||
}
|
||||
@ -1618,7 +1646,7 @@ void Unifier::tryUnifyTables(TypeId subTy, TypeId superTy, bool isIntersection)
|
||||
|
||||
TypeId Unifier::widen(TypeId ty)
|
||||
{
|
||||
if (!FFlag::LuauWidenIfSupertypeIsFree)
|
||||
if (!FFlag::LuauWidenIfSupertypeIsFree2)
|
||||
return ty;
|
||||
|
||||
Widen widen{types};
|
||||
@ -1627,10 +1655,21 @@ TypeId Unifier::widen(TypeId 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)
|
||||
{
|
||||
ty = follow(ty);
|
||||
if (get<AnyTypeVar>(ty))
|
||||
if (!FFlag::LuauAnyInIsOptionalIsOptional && get<AnyTypeVar>(ty))
|
||||
return ty;
|
||||
else if (isOptional(ty))
|
||||
return ty;
|
||||
@ -1744,7 +1783,10 @@ void Unifier::tryUnifyFreeTable(TypeId subTy, TypeId superTy)
|
||||
{
|
||||
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
|
||||
|
@ -11,18 +11,11 @@
|
||||
LUAU_FASTINTVARIABLE(LuauRecursionLimit, 1000)
|
||||
LUAU_FASTINTVARIABLE(LuauParseErrorLimit, 100)
|
||||
LUAU_FASTFLAGVARIABLE(LuauParseSingletonTypes, false)
|
||||
LUAU_FASTFLAGVARIABLE(LuauParseAllHotComments, false)
|
||||
LUAU_FASTFLAGVARIABLE(LuauTableFieldFunctionDebugname, false)
|
||||
|
||||
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)
|
||||
: location(location)
|
||||
, message(message)
|
||||
@ -146,54 +139,13 @@ ParseResult Parser::parse(const char* buffer, size_t bufferSize, AstNameTable& n
|
||||
{
|
||||
LUAU_TIMETRACE_SCOPE("Parser::parse", "Parser");
|
||||
|
||||
Parser p(buffer, bufferSize, names, allocator, FFlag::LuauParseAllHotComments ? options : ParseOptions());
|
||||
Parser p(buffer, bufferSize, names, allocator, options);
|
||||
|
||||
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)};
|
||||
}
|
||||
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)};
|
||||
}
|
||||
return ParseResult{root, std::move(p.hotcomments), std::move(p.parseErrors), std::move(p.commentLocations)};
|
||||
}
|
||||
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[Lexeme::Type::Eof] = 1;
|
||||
|
||||
if (FFlag::LuauParseAllHotComments)
|
||||
lexer.setSkipComments(true);
|
||||
// required for lookahead() to work across a comment boundary and for nextLexeme() to work when captureComments is false
|
||||
lexer.setSkipComments(true);
|
||||
|
||||
// read first lexeme
|
||||
// read first lexeme (any hot comments get .header = true)
|
||||
LUAU_ASSERT(hotcommentHeader);
|
||||
nextLexeme();
|
||||
|
||||
// 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 (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();
|
||||
commentLocations.push_back(Comment{lexeme.type, lexeme.location});
|
||||
const char* text = lexeme.data;
|
||||
|
||||
// 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;
|
||||
unsigned int end = lexeme.length;
|
||||
while (end > 0 && isSpace(text[end - 1]))
|
||||
--end;
|
||||
|
||||
// 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 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;
|
||||
hotcomments.push_back({hotcommentHeader, lexeme.location, std::string(text + 1, text + end)});
|
||||
}
|
||||
|
||||
type = lexer.next(/* skipComments= */ false).type;
|
||||
}
|
||||
}
|
||||
else
|
||||
|
@ -4,8 +4,6 @@
|
||||
#include "Luau/Bytecode.h"
|
||||
#include "Luau/Compiler.h"
|
||||
|
||||
LUAU_FASTFLAGVARIABLE(LuauCompileSelectBuiltin2, false)
|
||||
|
||||
namespace Luau
|
||||
{
|
||||
namespace Compile
|
||||
@ -64,7 +62,7 @@ int getBuiltinFunctionId(const Builtin& builtin, const CompileOptions& options)
|
||||
if (builtin.isGlobal("unpack"))
|
||||
return LBF_TABLE_UNPACK;
|
||||
|
||||
if (FFlag::LuauCompileSelectBuiltin2 && builtin.isGlobal("select"))
|
||||
if (builtin.isGlobal("select"))
|
||||
return LBF_SELECT_VARARG;
|
||||
|
||||
if (builtin.object == "math")
|
||||
|
@ -15,8 +15,6 @@
|
||||
#include <bitset>
|
||||
#include <math.h>
|
||||
|
||||
LUAU_FASTFLAG(LuauCompileSelectBuiltin2)
|
||||
|
||||
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)
|
||||
{
|
||||
LUAU_ASSERT(FFlag::LuauCompileSelectBuiltin2);
|
||||
LUAU_ASSERT(targetCount == 1);
|
||||
LUAU_ASSERT(!expr->self);
|
||||
LUAU_ASSERT(expr->args.size == 2 && expr->args.data[1]->is<AstExprVarargs>());
|
||||
@ -407,7 +404,6 @@ struct Compiler
|
||||
|
||||
if (bfid == LBF_SELECT_VARARG)
|
||||
{
|
||||
LUAU_ASSERT(FFlag::LuauCompileSelectBuiltin2);
|
||||
// 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
|
||||
if (multRet == false && targetCount == 1 && expr->args.size == 2 && expr->args.data[1]->is<AstExprVarargs>())
|
||||
|
@ -232,11 +232,18 @@ if(TARGET Luau.UnitTest)
|
||||
tests/Transpiler.test.cpp
|
||||
tests/TypeInfer.aliases.test.cpp
|
||||
tests/TypeInfer.annotations.test.cpp
|
||||
tests/TypeInfer.anyerror.test.cpp
|
||||
tests/TypeInfer.builtins.test.cpp
|
||||
tests/TypeInfer.classes.test.cpp
|
||||
tests/TypeInfer.definitions.test.cpp
|
||||
tests/TypeInfer.functions.test.cpp
|
||||
tests/TypeInfer.generics.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.refinements.test.cpp
|
||||
tests/TypeInfer.singletons.test.cpp
|
||||
|
@ -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 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_pushlightuserdata(lua_State* L, void* p);
|
||||
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)
|
||||
*/
|
||||
@ -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 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 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);
|
||||
|
||||
/*
|
||||
** 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
|
||||
*/
|
||||
|
@ -35,8 +35,8 @@ const char* luau_ident = "$Luau: Copyright (C) 2019-2022 Roblox Corporation $\n"
|
||||
|
||||
static Table* getcurrenv(lua_State* L)
|
||||
{
|
||||
if (L->ci == L->base_ci) /* no enclosing function? */
|
||||
return L->gt; /* use global table as environment */
|
||||
if (L->ci == L->base_ci) /* no enclosing function? */
|
||||
return L->gt; /* use global table as environment */
|
||||
else
|
||||
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)
|
||||
{
|
||||
api_check(L, unsigned(tag) < LUA_UTAG_LIMIT);
|
||||
api_check(L, unsigned(tag) < LUA_UTAG_LIMIT || tag == UTAG_PROXY);
|
||||
luaC_checkGC(L);
|
||||
luaC_checkthreadsleep(L);
|
||||
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;
|
||||
}
|
||||
|
||||
LUA_API void lua_clonefunction(lua_State* L, int idx)
|
||||
void lua_clonefunction(lua_State* L, int idx)
|
||||
{
|
||||
StkId p = index2addr(L, idx);
|
||||
api_check(L, isLfunction(p));
|
||||
@ -1333,3 +1333,15 @@ lua_Callbacks* lua_callbacks(lua_State* L)
|
||||
{
|
||||
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];
|
||||
}
|
||||
|
@ -5,6 +5,7 @@
|
||||
#include "lstate.h"
|
||||
#include "lapi.h"
|
||||
#include "ldo.h"
|
||||
#include "ludata.h"
|
||||
|
||||
#include <ctype.h>
|
||||
#include <stdio.h>
|
||||
@ -190,6 +191,7 @@ static int luaB_type(lua_State* L)
|
||||
luaL_checkany(L, 1);
|
||||
if (DFFlag::LuauMorePreciseLuaLTypeName)
|
||||
{
|
||||
/* resulting name doesn't differentiate between userdata types */
|
||||
lua_pushstring(L, lua_typename(L, lua_type(L, 1)));
|
||||
}
|
||||
else
|
||||
@ -202,8 +204,16 @@ static int luaB_type(lua_State* L)
|
||||
static int luaB_typeof(lua_State* L)
|
||||
{
|
||||
luaL_checkany(L, 1);
|
||||
const TValue* obj = luaA_toobject(L, 1);
|
||||
lua_pushstring(L, luaT_objtypename(L, obj));
|
||||
if (DFFlag::LuauMorePreciseLuaLTypeName)
|
||||
{
|
||||
/* 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;
|
||||
}
|
||||
|
||||
@ -412,7 +422,7 @@ static int luaB_newproxy(lua_State* L)
|
||||
|
||||
bool needsmt = lua_toboolean(L, 1);
|
||||
|
||||
lua_newuserdata(L, 0);
|
||||
lua_newuserdatatagged(L, 0, UTAG_PROXY);
|
||||
|
||||
if (needsmt)
|
||||
{
|
||||
|
@ -9,9 +9,9 @@
|
||||
/*
|
||||
** 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_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
|
||||
|
@ -98,7 +98,7 @@
|
||||
*/
|
||||
#if defined(__APPLE__)
|
||||
#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)
|
||||
#else
|
||||
// Android somehow uses a similar ABI to MSVC, *not* to iOS...
|
||||
|
@ -53,7 +53,7 @@ void luaS_resize(lua_State* L, int newsize)
|
||||
{
|
||||
TString* p = tb->hash[i];
|
||||
while (p)
|
||||
{ /* for each node in the list */
|
||||
{ /* for each node in the list */
|
||||
TString* next = p->next; /* save next */
|
||||
unsigned int h = p->hash;
|
||||
int h1 = lmod(h, newsize); /* new position */
|
||||
|
@ -24,6 +24,8 @@
|
||||
|
||||
#include <string.h>
|
||||
|
||||
LUAU_FASTFLAGVARIABLE(LuauTableRehashRework, false)
|
||||
|
||||
// max size of both array and hash part is 2^MAXBITS
|
||||
#define MAXBITS 26
|
||||
#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 */
|
||||
}
|
||||
|
||||
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)
|
||||
{
|
||||
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++)
|
||||
{
|
||||
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 */
|
||||
luaM_reallocarray(L, t->array, oldasize, nasize, TValue, t->memcat);
|
||||
}
|
||||
/* re-insert elements from hash part */
|
||||
for (int i = twoto(oldhsize) - 1; i >= 0; i--)
|
||||
if (FFlag::LuauTableRehashRework)
|
||||
{
|
||||
LuaNode* old = nold + i;
|
||||
if (!ttisnil(gval(old)))
|
||||
for (int i = twoto(oldhsize) - 1; i >= 0; i--)
|
||||
{
|
||||
TValue ok;
|
||||
getnodekey(L, &ok, old);
|
||||
setobjt2t(L, luaH_set(L, t, &ok), gval(old));
|
||||
LuaNode* old = nold + i;
|
||||
if (!ttisnil(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)
|
||||
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)
|
||||
{ /* cannot find a free place? */
|
||||
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);
|
||||
TValue mk;
|
||||
|
@ -4,6 +4,7 @@
|
||||
|
||||
#include "lstate.h"
|
||||
#include "lstring.h"
|
||||
#include "ludata.h"
|
||||
#include "ltable.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)
|
||||
{
|
||||
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]);
|
||||
|
||||
|
@ -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)
|
||||
{
|
||||
LUAU_ASSERT(u->tag < LUA_UTAG_LIMIT || u->tag == UTAG_IDTOR);
|
||||
|
||||
void (*dtor)(void*) = nullptr;
|
||||
if (u->tag == UTAG_IDTOR)
|
||||
memcpy(&dtor, &u->data + u->len - sizeof(dtor), sizeof(dtor));
|
||||
else if (u->tag)
|
||||
if (u->tag < LUA_UTAG_LIMIT)
|
||||
dtor = L->global->udatagc[u->tag];
|
||||
else if (u->tag == UTAG_IDTOR)
|
||||
memcpy(&dtor, &u->data + u->len - sizeof(dtor), sizeof(dtor));
|
||||
|
||||
if (dtor)
|
||||
dtor(u->data);
|
||||
|
@ -7,6 +7,9 @@
|
||||
/* special tag value is used for user data with inline dtors */
|
||||
#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)
|
||||
|
||||
LUAI_FUNC Udata* luaU_newudata(lua_State* L, size_t s, int tag);
|
||||
|
@ -77,7 +77,7 @@
|
||||
if (LUAU_UNLIKELY(!!interrupt)) \
|
||||
{ /* the interrupt hook is called right before we advance pc */ \
|
||||
VM_PROTECT(L->ci->savedpc++; interrupt(L, -1)); \
|
||||
if (L->status != 0) \
|
||||
if (L->status != 0) \
|
||||
{ \
|
||||
L->ci->savedpc--; \
|
||||
goto exit; \
|
||||
|
@ -2755,4 +2755,166 @@ local abc = b@1
|
||||
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();
|
||||
|
@ -2819,8 +2819,6 @@ RETURN R1 -1
|
||||
|
||||
TEST_CASE("FastcallSelect")
|
||||
{
|
||||
ScopedFastFlag sff("LuauCompileSelectBuiltin2", true);
|
||||
|
||||
// select(_, ...) compiles to a builtin call
|
||||
CHECK_EQ("\n" + compileFunction0("return (select('#', ...))"), R"(
|
||||
LOADK R1 K0
|
||||
|
@ -569,6 +569,11 @@ TEST_CASE("Debugger")
|
||||
CHECK(lua_tointeger(L, -1) == 50);
|
||||
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
|
||||
const char* l = lua_getlocal(L, 0, 1);
|
||||
REQUIRE(l);
|
||||
@ -652,31 +657,6 @@ TEST_CASE("SameHash")
|
||||
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")
|
||||
{
|
||||
static int dtorhits = 0;
|
||||
@ -969,7 +949,7 @@ TEST_CASE("StringConversion")
|
||||
TEST_CASE("GCDump")
|
||||
{
|
||||
// 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);
|
||||
lua_State* L = globalState.get();
|
||||
@ -1015,4 +995,114 @@ TEST_CASE("GCDump")
|
||||
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();
|
||||
|
@ -1667,8 +1667,6 @@ _ = (math.random() < 0.5 and false) or 42 -- currently ignored
|
||||
|
||||
TEST_CASE_FIXTURE(Fixture, "WrongComment")
|
||||
{
|
||||
ScopedFastFlag sff("LuauParseAllHotComments", true);
|
||||
|
||||
LintResult result = lint(R"(
|
||||
--!strict
|
||||
--!struct
|
||||
|
@ -248,10 +248,12 @@ TEST_CASE_FIXTURE(Fixture, "clone_seal_free_tables")
|
||||
|
||||
TEST_CASE_FIXTURE(Fixture, "clone_self_property")
|
||||
{
|
||||
ScopedFastFlag sff{"LuauAnyInIsOptionalIsOptional", true};
|
||||
|
||||
fileResolver.source["Module/A"] = R"(
|
||||
--!nonstrict
|
||||
local a = {}
|
||||
function a:foo(x)
|
||||
function a:foo(x: number)
|
||||
return -x;
|
||||
end
|
||||
return a;
|
||||
@ -267,10 +269,10 @@ TEST_CASE_FIXTURE(Fixture, "clone_self_property")
|
||||
)";
|
||||
|
||||
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 "
|
||||
"dot or pass 1 extra nil to suppress this warning");
|
||||
LUAU_REQUIRE_ERROR_COUNT(1, result);
|
||||
|
||||
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")
|
||||
|
@ -126,6 +126,8 @@ TEST_CASE_FIXTURE(Fixture, "parameters_having_type_any_are_optional")
|
||||
|
||||
TEST_CASE_FIXTURE(Fixture, "local_tables_are_not_any")
|
||||
{
|
||||
ScopedFastFlag sff{"LuauAnyInIsOptionalIsOptional", true};
|
||||
|
||||
CheckResult result = check(R"(
|
||||
--!nonstrict
|
||||
local T = {}
|
||||
@ -136,31 +138,25 @@ TEST_CASE_FIXTURE(Fixture, "local_tables_are_not_any")
|
||||
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) {
|
||||
return get<FunctionDoesNotTakeSelf>(e);
|
||||
}));
|
||||
CHECK(std::any_of(result.errors.begin(), result.errors.end(), [](const TypeError& e) {
|
||||
return get<FunctionRequiresSelf>(e);
|
||||
}));
|
||||
CHECK_EQ("This function does not take self. Did you mean to use a dot instead of a colon?", toString(result.errors[0]));
|
||||
}
|
||||
|
||||
TEST_CASE_FIXTURE(Fixture, "offer_a_hint_if_you_use_a_dot_instead_of_a_colon")
|
||||
{
|
||||
ScopedFastFlag sff{"LuauAnyInIsOptionalIsOptional", true};
|
||||
|
||||
CheckResult result = check(R"(
|
||||
--!nonstrict
|
||||
local T = {}
|
||||
function T:method() end
|
||||
T.method()
|
||||
function T:method(x: number) end
|
||||
T.method(5)
|
||||
)");
|
||||
|
||||
LUAU_REQUIRE_ERROR_COUNT(1, result);
|
||||
|
||||
auto e = get<FunctionRequiresSelf>(result.errors[0]);
|
||||
REQUIRE(e != nullptr);
|
||||
|
||||
REQUIRE_EQ(1, e->requiredExtraNils);
|
||||
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, "table_props_are_any")
|
||||
|
@ -676,4 +676,25 @@ TEST_CASE_FIXTURE(Fixture, "forward_declared_alias_is_not_clobbered_by_prior_uni
|
||||
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();
|
||||
|
335
tests/TypeInfer.anyerror.test.cpp
Normal file
335
tests/TypeInfer.anyerror.test.cpp
Normal 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();
|
@ -871,6 +871,7 @@ TEST_CASE_FIXTURE(Fixture, "assert_removes_falsy_types")
|
||||
ScopedFastFlag sff[]{
|
||||
{"LuauAssertStripsFalsyTypes", true},
|
||||
{"LuauDiscriminableUnions2", true},
|
||||
{"LuauWidenIfSupertypeIsFree2", true},
|
||||
};
|
||||
|
||||
CheckResult result = check(R"(
|
||||
@ -879,6 +880,26 @@ TEST_CASE_FIXTURE(Fixture, "assert_removes_falsy_types")
|
||||
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);
|
||||
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}}));
|
||||
}
|
||||
|
||||
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();
|
||||
|
@ -293,4 +293,22 @@ TEST_CASE_FIXTURE(Fixture, "documentation_symbols_dont_attach_to_persistent_type
|
||||
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();
|
||||
|
1338
tests/TypeInfer.functions.test.cpp
Normal file
1338
tests/TypeInfer.functions.test.cpp
Normal file
File diff suppressed because it is too large
Load Diff
@ -1,6 +1,9 @@
|
||||
// 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/TypeVar.h"
|
||||
#include "Luau/Scope.h"
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
#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)");
|
||||
}
|
||||
|
||||
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();
|
||||
|
@ -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)");
|
||||
}
|
||||
|
||||
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();
|
||||
|
473
tests/TypeInfer.loops.test.cpp
Normal file
473
tests/TypeInfer.loops.test.cpp
Normal 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();
|
310
tests/TypeInfer.modules.test.cpp
Normal file
310
tests/TypeInfer.modules.test.cpp
Normal 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();
|
275
tests/TypeInfer.oop.test.cpp
Normal file
275
tests/TypeInfer.oop.test.cpp
Normal 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();
|
759
tests/TypeInfer.operators.test.cpp
Normal file
759
tests/TypeInfer.operators.test.cpp
Normal 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();
|
100
tests/TypeInfer.primitives.test.cpp
Normal file
100
tests/TypeInfer.primitives.test.cpp
Normal 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();
|
@ -1298,4 +1298,22 @@ TEST_CASE_FIXTURE(RefinementClassFixture, "x_is_not_instance_or_else_not_part")
|
||||
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();
|
||||
|
@ -5,6 +5,8 @@
|
||||
#include "doctest.h"
|
||||
#include "Luau/BuiltinDefinitions.h"
|
||||
|
||||
LUAU_FASTFLAG(BetterDiagnosticCodesInStudio)
|
||||
|
||||
using namespace Luau;
|
||||
|
||||
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);
|
||||
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")
|
||||
@ -445,7 +454,7 @@ TEST_CASE_FIXTURE(Fixture, "widen_the_supertype_if_it_is_free_and_subtype_has_si
|
||||
{"LuauSingletonTypes", true},
|
||||
{"LuauEqConstraint", true},
|
||||
{"LuauDiscriminableUnions2", true},
|
||||
{"LuauWidenIfSupertypeIsFree", true},
|
||||
{"LuauWidenIfSupertypeIsFree2", true},
|
||||
{"LuauWeakEqConstraint", false},
|
||||
};
|
||||
|
||||
@ -472,9 +481,9 @@ TEST_CASE_FIXTURE(Fixture, "return_type_of_f_is_not_widened")
|
||||
{"LuauSingletonTypes", true},
|
||||
{"LuauDiscriminableUnions2", true},
|
||||
{"LuauEqConstraint", true},
|
||||
{"LuauWidenIfSupertypeIsFree", true},
|
||||
{"LuauWidenIfSupertypeIsFree2", true},
|
||||
{"LuauWeakEqConstraint", false},
|
||||
{"LuauDoNotAccidentallyDependOnPointerOrdering", true}
|
||||
{"LuauDoNotAccidentallyDependOnPointerOrdering", true},
|
||||
};
|
||||
|
||||
CheckResult result = check(R"(
|
||||
@ -497,7 +506,7 @@ TEST_CASE_FIXTURE(Fixture, "widening_happens_almost_everywhere")
|
||||
ScopedFastFlag sff[]{
|
||||
{"LuauParseSingletonTypes", true},
|
||||
{"LuauSingletonTypes", true},
|
||||
{"LuauWidenIfSupertypeIsFree", true},
|
||||
{"LuauWidenIfSupertypeIsFree2", true},
|
||||
};
|
||||
|
||||
CheckResult result = check(R"(
|
||||
@ -515,7 +524,7 @@ TEST_CASE_FIXTURE(Fixture, "widening_happens_almost_everywhere_except_for_tables
|
||||
{"LuauParseSingletonTypes", true},
|
||||
{"LuauSingletonTypes", true},
|
||||
{"LuauDiscriminableUnions2", true},
|
||||
{"LuauWidenIfSupertypeIsFree", true},
|
||||
{"LuauWidenIfSupertypeIsFree2", true},
|
||||
};
|
||||
|
||||
CheckResult result = check(R"(
|
||||
@ -544,7 +553,7 @@ TEST_CASE_FIXTURE(Fixture, "table_insert_with_a_singleton_argument")
|
||||
ScopedFastFlag sff[]{
|
||||
{"LuauParseSingletonTypes", true},
|
||||
{"LuauSingletonTypes", true},
|
||||
{"LuauWidenIfSupertypeIsFree", true},
|
||||
{"LuauWidenIfSupertypeIsFree2", true},
|
||||
};
|
||||
|
||||
CheckResult result = check(R"(
|
||||
@ -565,4 +574,97 @@ TEST_CASE_FIXTURE(Fixture, "table_insert_with_a_singleton_argument")
|
||||
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();
|
||||
|
@ -2384,4 +2384,504 @@ _ = (_.cos)
|
||||
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();
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -895,4 +895,87 @@ caused by:
|
||||
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();
|
||||
|
@ -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')");
|
||||
}
|
||||
|
||||
// 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();
|
||||
|
@ -833,6 +833,17 @@ assert((function()
|
||||
return sum
|
||||
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
|
||||
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
|
||||
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
|
||||
|
||||
return 'OK'
|
||||
|
@ -3,14 +3,14 @@ print "testing debugger" -- note, this file can't run in isolation from C tests
|
||||
|
||||
local a = 5
|
||||
|
||||
function foo(b)
|
||||
function foo(b, ...)
|
||||
print("in foo", b)
|
||||
a = 6
|
||||
end
|
||||
|
||||
breakpoint(8)
|
||||
|
||||
foo(50)
|
||||
foo(50, 42)
|
||||
|
||||
breakpoint(16) -- next line
|
||||
print("here")
|
||||
|
@ -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 "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')
|
||||
|
11
tests/conformance/interrupt.lua
Normal file
11
tests/conformance/interrupt.lua
Normal 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"
|
@ -1,2 +0,0 @@
|
||||
type synthetic add -x "^Luau::Variant<.+>$" -l LuauVisualize.LuauVariantSyntheticChildrenProvider
|
||||
type summary add -x "^Luau::Variant<.+>$" -l LuauVisualize.luau_variant_summary
|
2
tools/lldb_formatters.lldb
Normal file
2
tools/lldb_formatters.lldb
Normal 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
|
Loading…
Reference in New Issue
Block a user