Sync to upstream/release/608 (#1145)

# Old Solver:

- Fix a bug in the old solver where a user could use the keyword
`typeof` as the name of a type alias.
- Fix stringification of scientific notation to omit a trailing decimal
place when not followed by a digit e.g. `1.e+20` -> `1e+20`
# New Solver
- Continuing work on the New non-strict mode
- Introduce `keyof` and `rawkeyof` type function for acquiring the type
of all keys in a table or class
(https://github.com/luau-lang/rfcs/pull/16)

---
Co-authored-by: Aaron Weiss <aaronweiss@roblox.com>
Co-authored-by: Alexander McCord <amccord@roblox.com>
Co-authored-by: Andy Friesen <afriesen@roblox.com>
Co-authored-by: Aviral Goel <agoel@roblox.com>
Co-authored-by: Lily Brown <lbrown@roblox.com>
Co-authored-by: Vyacheslav Egorov <vegorov@roblox.com>
Co-authored-by: Vighnesh Vijay <vvijay@roblox.com>

---------

Co-authored-by: Aaron Weiss <aaronweiss@roblox.com>
Co-authored-by: Alexander McCord <amccord@roblox.com>
Co-authored-by: Andy Friesen <afriesen@roblox.com>
Co-authored-by: Aviral Goel <agoel@roblox.com>
Co-authored-by: David Cope <dcope@roblox.com>
Co-authored-by: Lily Brown <lbrown@roblox.com>
Co-authored-by: Vyacheslav Egorov <vegorov@roblox.com>
This commit is contained in:
Vighnesh-V 2024-01-12 14:25:27 -08:00 committed by GitHub
parent c0f5538947
commit f31232d301
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
75 changed files with 3132 additions and 826 deletions

View File

@ -85,6 +85,8 @@ struct TarjanNode
// https://en.wikipedia.org/wiki/Tarjan%27s_strongly_connected_components_algorithm
struct Tarjan
{
Tarjan();
// Vertices (types and type packs) are indexed, using pre-order traversal.
DenseHashMap<TypeId, int> typeToIndex{nullptr};
DenseHashMap<TypePackId, int> packToIndex{nullptr};

View File

@ -11,10 +11,9 @@
#include "Luau/Predicate.h"
#include "Luau/Unifiable.h"
#include "Luau/Variant.h"
#include "Luau/TypeFwd.h"
#include "Luau/VecDeque.h"
#include <atomic>
#include <deque>
#include <map>
#include <memory>
#include <optional>
@ -768,6 +767,7 @@ bool isThread(TypeId ty);
bool isBuffer(TypeId ty);
bool isOptional(TypeId ty);
bool isTableIntersection(TypeId ty);
bool isTableUnion(TypeId ty);
bool isOverloadedFunction(TypeId ty);
// True when string is a subtype of ty
@ -992,7 +992,7 @@ private:
// (T* t, size_t currentIndex)
using SavedIterInfo = std::pair<const T*, size_t>;
std::deque<SavedIterInfo> stack;
VecDeque<SavedIterInfo> stack;
DenseHashSet<const T*> seen{nullptr}; // Only needed to protect the iterator from hanging the thread.
void advance()

View File

@ -163,6 +163,8 @@ struct BuiltinTypeFamilies
TypeFamily eqFamily;
TypeFamily refineFamily;
TypeFamily keyofFamily;
TypeFamily rawkeyofFamily;
void addToScope(NotNull<TypeArena> arena, NotNull<Scope> scope) const;
};

View File

@ -16,7 +16,6 @@
LUAU_FASTFLAG(DebugLuauDeferredConstraintResolution);
LUAU_FASTFLAG(DebugLuauReadWriteProperties);
LUAU_FASTFLAG(LuauClipExtraHasEndProps);
LUAU_FASTFLAGVARIABLE(LuauAutocompleteDoEnd, false);
LUAU_FASTFLAGVARIABLE(LuauAutocompleteStringLiteralBounds, false);
static const std::unordered_set<std::string> kStatementStartingKeywords = {
@ -1093,11 +1092,8 @@ static AutocompleteEntryMap autocompleteStatement(
result.emplace("end", AutocompleteEntry{AutocompleteEntryKind::Keyword});
else if (AstExprFunction* exprFunction = (*it)->as<AstExprFunction>(); exprFunction && !exprFunction->body->hasEnd)
result.emplace("end", AutocompleteEntry{AutocompleteEntryKind::Keyword});
if (FFlag::LuauAutocompleteDoEnd)
{
if (AstStatBlock* exprBlock = (*it)->as<AstStatBlock>(); exprBlock && !exprBlock->hasEnd)
result.emplace("end", AutocompleteEntry{AutocompleteEntryKind::Keyword});
}
if (AstStatBlock* exprBlock = (*it)->as<AstStatBlock>(); exprBlock && !exprBlock->hasEnd)
result.emplace("end", AutocompleteEntry{AutocompleteEntryKind::Keyword});
}
}
else
@ -1114,11 +1110,8 @@ static AutocompleteEntryMap autocompleteStatement(
result.emplace("end", AutocompleteEntry{AutocompleteEntryKind::Keyword});
else if (AstExprFunction* exprFunction = (*it)->as<AstExprFunction>(); exprFunction && !exprFunction->DEPRECATED_hasEnd)
result.emplace("end", AutocompleteEntry{AutocompleteEntryKind::Keyword});
if (FFlag::LuauAutocompleteDoEnd)
{
if (AstStatBlock* exprBlock = (*it)->as<AstStatBlock>(); exprBlock && !exprBlock->hasEnd)
result.emplace("end", AutocompleteEntry{AutocompleteEntryKind::Keyword});
}
if (AstStatBlock* exprBlock = (*it)->as<AstStatBlock>(); exprBlock && !exprBlock->hasEnd)
result.emplace("end", AutocompleteEntry{AutocompleteEntryKind::Keyword});
}
}

View File

@ -23,7 +23,8 @@
* about a function that takes any number of values, but where each value must have some specific type.
*/
LUAU_FASTFLAG(DebugLuauDeferredConstraintResolution)
LUAU_FASTFLAG(DebugLuauDeferredConstraintResolution);
LUAU_FASTFLAGVARIABLE(LuauSetMetatableOnUnionsOfTables, false);
namespace Luau
{
@ -963,6 +964,18 @@ static std::optional<WithPredicate<TypePackId>> magicFunctionSetMetaTable(
else if (get<AnyType>(target) || get<ErrorType>(target) || isTableIntersection(target))
{
}
else if (FFlag::LuauSetMetatableOnUnionsOfTables && isTableUnion(target))
{
const UnionType* ut = get<UnionType>(target);
LUAU_ASSERT(ut);
std::vector<TypeId> resultParts;
for (TypeId ty : ut)
resultParts.push_back(arena.addType(MetatableType{ty, mt}));
return WithPredicate<TypePackId>{arena.addTypePack({arena.addType(UnionType{std::move(resultParts)})})};
}
else
{
typechecker.reportError(TypeError{expr.location, GenericError{"setmetatable should take a table"}});

View File

@ -159,6 +159,15 @@ private:
TypeId target = arena->addType(ty->ty);
asMutable(target)->documentationSymbol = ty->documentationSymbol;
if (auto generic = getMutable<GenericType>(target))
generic->scope = nullptr;
else if (auto free = getMutable<FreeType>(target))
free->scope = nullptr;
else if (auto fn = getMutable<FunctionType>(target))
fn->scope = nullptr;
else if (auto table = getMutable<TableType>(target))
table->scope = nullptr;
(*types)[ty] = target;
queue.push_back(target);
return target;
@ -175,6 +184,11 @@ private:
TypePackId target = arena->addTypePack(tp->ty);
if (auto generic = getMutable<GenericTypePack>(target))
generic->scope = nullptr;
else if (auto free = getMutable<FreeTypePack>(target))
free->scope = nullptr;
(*packs)[tp] = target;
queue.push_back(target);
return target;
@ -563,10 +577,12 @@ struct TypePackCloner
{
defaultClone(t);
}
void operator()(const GenericTypePack& t)
{
defaultClone(t);
}
void operator()(const ErrorTypePack& t)
{
defaultClone(t);
@ -633,7 +649,7 @@ void TypeCloner::operator()(const FreeType& t)
{
if (FFlag::DebugLuauDeferredConstraintResolution)
{
FreeType ft{t.scope, clone(t.lowerBound, dest, cloneState), clone(t.upperBound, dest, cloneState)};
FreeType ft{nullptr, clone(t.lowerBound, dest, cloneState), clone(t.upperBound, dest, cloneState)};
TypeId res = dest.addType(ft);
seenTypes[typeId] = res;
}

View File

@ -504,6 +504,11 @@ ControlFlow ConstraintGenerator::visitBlockWithoutChildScope(const ScopePtr& sco
continue;
}
// A type alias might have no name if the code is syntactically
// illegal. We mustn't prepopulate anything in this case.
if (alias->name == kParseNameError || alias->name == "typeof")
continue;
ScopePtr defnScope = childScope(alias, scope);
TypeId initialType = arena->addType(BlockedType{});
@ -1084,6 +1089,15 @@ static bool occursCheck(TypeId needle, TypeId haystack)
ControlFlow ConstraintGenerator::visit(const ScopePtr& scope, AstStatTypeAlias* alias)
{
if (alias->name == kParseNameError)
return ControlFlow::None;
if (alias->name == "typeof")
{
reportError(alias->location, GenericError{"Type aliases cannot be named typeof"});
return ControlFlow::None;
}
ScopePtr* defnScope = astTypeAliasDefiningScopes.find(alias);
std::unordered_map<Name, TypeFun>* typeBindings;
@ -1516,10 +1530,24 @@ InferencePack ConstraintGenerator::checkPack(const ScopePtr& scope, AstExprCall*
LUAU_ASSERT(target);
LUAU_ASSERT(mt);
target = follow(target);
AstExpr* targetExpr = call->args.data[0];
MetatableType mtv{target, mt};
TypeId resultTy = arena->addType(mtv);
TypeId resultTy = nullptr;
if (isTableUnion(target))
{
const UnionType* targetUnion = get<UnionType>(target);
std::vector<TypeId> newParts;
for (TypeId ty : targetUnion)
newParts.push_back(arena->addType(MetatableType{ty, mt}));
resultTy = arena->addType(UnionType{std::move(newParts)});
}
else
resultTy = arena->addType(MetatableType{target, mt});
if (AstExprLocal* targetLocal = targetExpr->as<AstExprLocal>())
{

View File

@ -16,6 +16,7 @@
#include "Luau/TypeFamily.h"
#include "Luau/TypeUtils.h"
#include "Luau/Unifier2.h"
#include "Luau/VecDeque.h"
#include "Luau/VisitType.h"
#include <algorithm>
#include <utility>
@ -450,10 +451,10 @@ struct TypeAndLocation
struct FreeTypeSearcher : TypeOnceVisitor
{
std::deque<TypeAndLocation>* result;
VecDeque<TypeAndLocation>* result;
Location location;
FreeTypeSearcher(std::deque<TypeAndLocation>* result, Location location)
FreeTypeSearcher(VecDeque<TypeAndLocation>* result, Location location)
: result(result)
, location(location)
{
@ -484,7 +485,7 @@ void ConstraintSolver::finalizeModule()
Unifier2 u2{NotNull{arena}, builtinTypes, rootScope, NotNull{&iceReporter}};
std::deque<TypeAndLocation> queue;
VecDeque<TypeAndLocation> queue;
for (auto& [name, binding] : rootScope->bindings)
queue.push_back({binding.typeId, binding.location});

View File

@ -4,7 +4,6 @@
#include "Luau/Common.h"
#include <algorithm>
#include <deque>
namespace Luau
{

View File

@ -1,8 +1,8 @@
// This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details
#include "Luau/BuiltinDefinitions.h"
LUAU_FASTFLAGVARIABLE(LuauBufferDefinitions, false)
LUAU_FASTFLAGVARIABLE(LuauBufferTypeck, false)
LUAU_FASTFLAGVARIABLE(LuauCheckedEmbeddedDefinitions, false);
namespace Luau
{
@ -263,14 +263,238 @@ declare function unpack<V>(tab: {V}, i: number?, j: number?): ...V
)BUILTIN_SRC";
static const std::string kBuiltinDefinitionLuaSrcChecked = R"BUILTIN_SRC(
declare bit32: {
band: @checked (...number) -> number,
bor: @checked (...number) -> number,
bxor: @checked (...number) -> number,
btest: @checked (number, ...number) -> boolean,
rrotate: @checked (x: number, disp: number) -> number,
lrotate: @checked (x: number, disp: number) -> number,
lshift: @checked (x: number, disp: number) -> number,
arshift: @checked (x: number, disp: number) -> number,
rshift: @checked (x: number, disp: number) -> number,
bnot: @checked (x: number) -> number,
extract: @checked (n: number, field: number, width: number?) -> number,
replace: @checked (n: number, v: number, field: number, width: number?) -> number,
countlz: @checked (n: number) -> number,
countrz: @checked (n: number) -> number,
byteswap: @checked (n: number) -> number,
}
declare math: {
frexp: @checked (n: number) -> (number, number),
ldexp: @checked (s: number, e: number) -> number,
fmod: @checked (x: number, y: number) -> number,
modf: @checked (n: number) -> (number, number),
pow: @checked (x: number, y: number) -> number,
exp: @checked (n: number) -> number,
ceil: @checked (n: number) -> number,
floor: @checked (n: number) -> number,
abs: @checked (n: number) -> number,
sqrt: @checked (n: number) -> number,
log: @checked (n: number, base: number?) -> number,
log10: @checked (n: number) -> number,
rad: @checked (n: number) -> number,
deg: @checked (n: number) -> number,
sin: @checked (n: number) -> number,
cos: @checked (n: number) -> number,
tan: @checked (n: number) -> number,
sinh: @checked (n: number) -> number,
cosh: @checked (n: number) -> number,
tanh: @checked (n: number) -> number,
atan: @checked (n: number) -> number,
acos: @checked (n: number) -> number,
asin: @checked (n: number) -> number,
atan2: @checked (y: number, x: number) -> number,
min: @checked (number, ...number) -> number,
max: @checked (number, ...number) -> number,
pi: number,
huge: number,
randomseed: @checked (seed: number) -> (),
random: @checked (number?, number?) -> number,
sign: @checked (n: number) -> number,
clamp: @checked (n: number, min: number, max: number) -> number,
noise: @checked (x: number, y: number?, z: number?) -> number,
round: @checked (n: number) -> number,
}
type DateTypeArg = {
year: number,
month: number,
day: number,
hour: number?,
min: number?,
sec: number?,
isdst: boolean?,
}
type DateTypeResult = {
year: number,
month: number,
wday: number,
yday: number,
day: number,
hour: number,
min: number,
sec: number,
isdst: boolean,
}
declare os: {
time: @checked (time: DateTypeArg?) -> number,
date: ((formatString: "*t" | "!*t", time: number?) -> DateTypeResult) & ((formatString: string?, time: number?) -> string),
difftime: @checked (t2: DateTypeResult | number, t1: DateTypeResult | number) -> number,
clock: () -> number,
}
declare function @checked require(target: any): any
declare function @checked getfenv(target: any): { [string]: any }
declare _G: any
declare _VERSION: string
declare function gcinfo(): number
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 error<T>(message: T, level: number?): never
declare function tostring<T>(value: T): string
declare function tonumber<T>(value: T, radix: number?): 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 rawlen<K, V>(obj: {[K]: V} | string): number
declare function setfenv<T..., R...>(target: number | (T...) -> R..., env: {[string]: any}): ((T...) -> R...)?
declare function ipairs<V>(tab: {V}): (({V}, number) -> (number?, V), {V}, number)
declare function pcall<A..., R...>(f: (A...) -> R..., ...: A...): (boolean, R...)
-- 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...)
-- `select` has a magic function attached to provide more detailed type information
declare function select<A...>(i: string | number, ...: A...): ...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 function @checked newproxy(mt: boolean?): any
declare coroutine: {
create: <A..., R...>(f: (A...) -> R...) -> thread,
resume: <A..., R...>(co: thread, A...) -> (boolean, R...),
running: () -> thread,
status: @checked (co: thread) -> "dead" | "running" | "normal" | "suspended",
wrap: <A..., R...>(f: (A...) -> R...) -> ((A...) -> R...),
yield: <A..., R...>(A...) -> R...,
isyieldable: () -> boolean,
close: @checked (co: thread) -> (boolean, any)
}
declare table: {
concat: <V>(t: {V}, sep: string?, i: number?, j: number?) -> string,
insert: (<V>(t: {V}, value: V) -> ()) & (<V>(t: {V}, pos: number, value: V) -> ()),
maxn: <V>(t: {V}) -> number,
remove: <V>(t: {V}, number?) -> V?,
sort: <V>(t: {V}, comp: ((V, V) -> boolean)?) -> (),
create: <V>(count: number, value: V?) -> {V},
find: <V>(haystack: {V}, needle: V, init: number?) -> number?,
unpack: <V>(list: {V}, i: number?, j: number?) -> ...V,
pack: <V>(...V) -> { n: number, [number]: V },
getn: <V>(t: {V}) -> number,
foreach: <K, V>(t: {[K]: V}, f: (K, V) -> ()) -> (),
foreachi: <V>({V}, (number, V) -> ()) -> (),
move: <V>(src: {V}, a: number, b: number, t: number, dst: {V}?) -> {V},
clear: <K, V>(table: {[K]: V}) -> (),
isfrozen: <K, V>(t: {[K]: V}) -> boolean,
}
declare debug: {
info: (<R...>(thread: thread, level: number, options: string) -> R...) & (<R...>(level: number, options: string) -> R...) & (<A..., R1..., R2...>(func: (A...) -> R1..., options: string) -> R2...),
traceback: ((message: string?, level: number?) -> string) & ((thread: thread, message: string?, level: number?) -> string),
}
declare utf8: {
char: @checked (...number) -> string,
charpattern: string,
codes: @checked (str: string) -> ((string, number) -> (number, number), string, number),
codepoint: @checked (str: string, i: number?, j: number?) -> ...number,
len: @checked (s: string, i: number?, j: number?) -> (number?, number?),
offset: @checked (s: string, n: number?, i: 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
--- Buffer API
declare buffer: {
create: @checked (size: number) -> buffer,
fromstring: @checked (str: string) -> buffer,
tostring: @checked (b: buffer) -> string,
len: @checked (b: buffer) -> number,
copy: @checked (target: buffer, targetOffset: number, source: buffer, sourceOffset: number?, count: number?) -> (),
fill: @checked (b: buffer, offset: number, value: number, count: number?) -> (),
readi8: @checked (b: buffer, offset: number) -> number,
readu8: @checked (b: buffer, offset: number) -> number,
readi16: @checked (b: buffer, offset: number) -> number,
readu16: @checked (b: buffer, offset: number) -> number,
readi32: @checked (b: buffer, offset: number) -> number,
readu32: @checked (b: buffer, offset: number) -> number,
readf32: @checked (b: buffer, offset: number) -> number,
readf64: @checked (b: buffer, offset: number) -> number,
writei8: @checked (b: buffer, offset: number, value: number) -> (),
writeu8: @checked (b: buffer, offset: number, value: number) -> (),
writei16: @checked (b: buffer, offset: number, value: number) -> (),
writeu16: @checked (b: buffer, offset: number, value: number) -> (),
writei32: @checked (b: buffer, offset: number, value: number) -> (),
writeu32: @checked (b: buffer, offset: number, value: number) -> (),
writef32: @checked (b: buffer, offset: number, value: number) -> (),
writef64: @checked (b: buffer, offset: number, value: number) -> (),
readstring: @checked (b: buffer, offset: number, count: number) -> string,
writestring: @checked (b: buffer, offset: number, value: string, count: number?) -> (),
}
)BUILTIN_SRC";
std::string getBuiltinDefinitionSource()
{
std::string result = kBuiltinDefinitionLuaSrc;
if (FFlag::LuauBufferTypeck)
result = kBuiltinDefinitionBufferSrc + result;
else if (FFlag::LuauBufferDefinitions)
else
result = kBuiltinDefinitionBufferSrc_DEPRECATED + result;
// Annotates each non generic function as checked
if (FFlag::LuauCheckedEmbeddedDefinitions)
result = kBuiltinDefinitionLuaSrcChecked;
return result;
}

View File

@ -35,8 +35,6 @@ LUAU_FASTFLAGVARIABLE(LuauKnowsTheDataModel3, false)
LUAU_FASTFLAGVARIABLE(DebugLuauDeferredConstraintResolution, false)
LUAU_FASTFLAGVARIABLE(DebugLuauLogSolverToJson, false)
LUAU_FASTFLAGVARIABLE(DebugLuauReadWriteProperties, false)
LUAU_FASTFLAGVARIABLE(CorrectEarlyReturnInMarkDirty, false)
LUAU_FASTFLAGVARIABLE(LuauDefinitionFileSetModuleName, false)
LUAU_FASTFLAGVARIABLE(LuauRethrowSingleModuleIce, false)
namespace Luau
@ -165,11 +163,9 @@ LoadDefinitionFileResult Frontend::loadDefinitionFile(GlobalTypes& globals, Scop
LUAU_TIMETRACE_SCOPE("loadDefinitionFile", "Frontend");
Luau::SourceModule sourceModule;
if (FFlag::LuauDefinitionFileSetModuleName)
{
sourceModule.name = packageName;
sourceModule.humanReadableName = packageName;
}
sourceModule.name = packageName;
sourceModule.humanReadableName = packageName;
Luau::ParseResult parseResult = parseSourceForModule(source, sourceModule, captureComments);
if (parseResult.errors.size() > 0)
return LoadDefinitionFileResult{false, parseResult, sourceModule, nullptr};
@ -1116,16 +1112,8 @@ bool Frontend::isDirty(const ModuleName& name, bool forAutocomplete) const
*/
void Frontend::markDirty(const ModuleName& name, std::vector<ModuleName>* markedDirty)
{
if (FFlag::CorrectEarlyReturnInMarkDirty)
{
if (sourceNodes.count(name) == 0)
return;
}
else
{
if (!moduleResolver.getModule(name) && !moduleResolverForAutocomplete.getModule(name))
return;
}
if (sourceNodes.count(name) == 0)
return;
std::unordered_map<ModuleName, std::vector<ModuleName>> reverseDeps;
for (const auto& module : sourceNodes)

View File

@ -2,7 +2,6 @@
#include "Luau/GlobalTypes.h"
LUAU_FASTFLAG(LuauInitializeStringMetatableInGlobalTypes)
LUAU_FASTFLAG(LuauBufferTypeck)
namespace Luau
@ -24,14 +23,11 @@ GlobalTypes::GlobalTypes(NotNull<BuiltinTypes> builtinTypes)
globalScope->addBuiltinTypeBinding("unknown", TypeFun{{}, builtinTypes->unknownType});
globalScope->addBuiltinTypeBinding("never", TypeFun{{}, builtinTypes->neverType});
if (FFlag::LuauInitializeStringMetatableInGlobalTypes)
{
unfreeze(*builtinTypes->arena);
TypeId stringMetatableTy = makeStringMetatable(builtinTypes);
asMutable(builtinTypes->stringType)->ty.emplace<PrimitiveType>(PrimitiveType::String, stringMetatableTy);
persist(stringMetatableTy);
freeze(*builtinTypes->arena);
}
unfreeze(*builtinTypes->arena);
TypeId stringMetatableTy = makeStringMetatable(builtinTypes);
asMutable(builtinTypes->stringType)->ty.emplace<PrimitiveType>(PrimitiveType::String, stringMetatableTy);
persist(stringMetatableTy);
freeze(*builtinTypes->arena);
}
} // namespace Luau

View File

@ -10,6 +10,8 @@
LUAU_FASTINTVARIABLE(LuauTarjanChildLimit, 10000)
LUAU_FASTFLAG(DebugLuauReadWriteProperties)
LUAU_FASTFLAGVARIABLE(LuauPreallocateTarjanVectors, false);
LUAU_FASTINTVARIABLE(LuauTarjanPreallocationSize, 256);
namespace Luau
{
@ -146,6 +148,18 @@ static TypeId shallowClone(TypeId ty, TypeArena& dest, const TxnLog* log, bool a
return resTy;
}
Tarjan::Tarjan()
{
if (FFlag::LuauPreallocateTarjanVectors)
{
nodes.reserve(FInt::LuauTarjanPreallocationSize);
stack.reserve(FInt::LuauTarjanPreallocationSize);
edgesTy.reserve(FInt::LuauTarjanPreallocationSize);
edgesTp.reserve(FInt::LuauTarjanPreallocationSize);
worklist.reserve(FInt::LuauTarjanPreallocationSize);
}
}
void Tarjan::visitChildren(TypeId ty, int index)
{
LUAU_ASSERT(ty == log->follow(ty));

View File

@ -545,11 +545,7 @@ SubtypingResult Subtyping::isCovariantWith(SubtypingEnvironment& env, TypePackId
// Match head types pairwise
for (size_t i = 0; i < headSize; ++i)
{
results.push_back(isCovariantWith(env, subHead[i], superHead[i]).withBothComponent(TypePath::Index{i}));
if (!results.back().isSubtype)
return results.back();
}
// Handle mismatched head sizes
@ -599,7 +595,10 @@ SubtypingResult Subtyping::isCovariantWith(SubtypingEnvironment& env, TypePackId
unexpected(*subTail);
}
else
return {false};
{
results.push_back({false});
return SubtypingResult::all(results);
}
}
else if (subHead.size() > superHead.size())
{
@ -664,7 +663,7 @@ SubtypingResult Subtyping::isCovariantWith(SubtypingEnvironment& env, TypePackId
bool ok = bindGeneric(env, *subTail, *superTail);
results.push_back(SubtypingResult{ok}.withBothComponent(TypePath::PackField::Tail));
}
else if (get2<VariadicTypePack, GenericTypePack>(*subTail, *superTail))
else if (auto p = get2<VariadicTypePack, GenericTypePack>(*subTail, *superTail))
{
if (variance == Variance::Contravariant)
{
@ -678,9 +677,17 @@ SubtypingResult Subtyping::isCovariantWith(SubtypingEnvironment& env, TypePackId
results.push_back(SubtypingResult{false}.withBothComponent(TypePath::PackField::Tail));
}
}
else if (get2<GenericTypePack, VariadicTypePack>(*subTail, *superTail))
else if (auto p = get2<GenericTypePack, VariadicTypePack>(*subTail, *superTail))
{
if (variance == Variance::Contravariant)
if (TypeId t = follow(p.second->ty); get<AnyType>(t) || get<UnknownType>(t))
{
// Extra magic rule:
// T... <: ...any
// T... <: ...unknown
//
// See https://github.com/luau-lang/luau/issues/767
}
else if (variance == Variance::Contravariant)
{
// (...number) -> number </: <A...>(A...) -> number
results.push_back(SubtypingResult{false}.withBothComponent(TypePath::PackField::Tail));
@ -747,6 +754,8 @@ SubtypingResult Subtyping::isCovariantWith(SubtypingEnvironment& env, TypePackId
template<typename SubTy, typename SuperTy>
SubtypingResult Subtyping::isContravariantWith(SubtypingEnvironment& env, SubTy&& subTy, SuperTy&& superTy)
{
VarianceFlipper vf{&variance};
SubtypingResult result = isCovariantWith(env, superTy, subTy);
if (result.reasoning.empty())
result.reasoning.insert(SubtypingReasoning{TypePath::kEmpty, TypePath::kEmpty, SubtypingVariance::Contravariant});
@ -778,6 +787,7 @@ template<typename SubTy, typename SuperTy>
SubtypingResult Subtyping::isInvariantWith(SubtypingEnvironment& env, SubTy&& subTy, SuperTy&& superTy)
{
SubtypingResult result = isCovariantWith(env, subTy, superTy).andAlso(isContravariantWith(env, subTy, superTy));
if (result.reasoning.empty())
result.reasoning.insert(SubtypingReasoning{TypePath::kEmpty, TypePath::kEmpty, SubtypingVariance::Invariant});
else
@ -842,11 +852,20 @@ SubtypingResult Subtyping::isInvariantWith(SubtypingEnvironment& env, const TryP
SubtypingResult Subtyping::isCovariantWith(SubtypingEnvironment& env, TypeId subTy, const UnionType* superUnion)
{
// As per TAPL: T <: A | B iff T <: A || T <: B
std::vector<SubtypingResult> subtypings;
size_t i = 0;
for (TypeId ty : superUnion)
subtypings.push_back(isCovariantWith(env, subTy, ty).withSuperComponent(TypePath::Index{i++}));
return SubtypingResult::any(subtypings);
{
SubtypingResult next = isCovariantWith(env, subTy, ty);
if (next.isSubtype)
return SubtypingResult{true};
}
/*
* TODO: Is it possible here to use the context produced by the above
* isCovariantWith() calls to produce a richer, more helpful result in the
* case that the subtyping relation does not hold?
*/
return SubtypingResult{false};
}
SubtypingResult Subtyping::isCovariantWith(SubtypingEnvironment& env, const UnionType* subUnion, TypeId superTy)
@ -1173,7 +1192,6 @@ SubtypingResult Subtyping::isCovariantWith(SubtypingEnvironment& env, const Func
{
SubtypingResult result;
{
VarianceFlipper vf{&variance};
result.orElse(isContravariantWith(env, subFunction->argTypes, superFunction->argTypes).withBothComponent(TypePath::PackField::Arguments));
}

View File

@ -11,6 +11,7 @@
#include "Luau/ToString.h"
#include "Luau/TypeInfer.h"
#include "Luau/TypePack.h"
#include "Luau/VecDeque.h"
#include "Luau/VisitType.h"
#include <algorithm>
@ -26,7 +27,6 @@ LUAU_FASTINTVARIABLE(LuauTableTypeMaximumStringifierLength, 0)
LUAU_FASTINT(LuauTypeInferRecursionLimit)
LUAU_FASTFLAG(LuauInstantiateInSubtyping)
LUAU_FASTFLAG(DebugLuauReadWriteProperties)
LUAU_FASTFLAGVARIABLE(LuauInitializeStringMetatableInGlobalTypes, false)
LUAU_FASTFLAG(LuauBufferTypeck)
namespace Luau
@ -129,7 +129,7 @@ std::vector<TypeId> flattenIntersection(TypeId ty)
return {ty};
std::unordered_set<TypeId> seen;
std::deque<TypeId> queue{ty};
VecDeque<TypeId> queue{ty};
std::vector<TypeId> result;
@ -248,6 +248,15 @@ bool isTableIntersection(TypeId ty)
return std::all_of(parts.begin(), parts.end(), getTableType);
}
bool isTableUnion(TypeId ty)
{
const UnionType* ut = get<UnionType>(follow(ty));
if (!ut)
return false;
return std::all_of(begin(ut), end(ut), getTableType);
}
bool isOverloadedFunction(TypeId ty)
{
if (!get<IntersectionType>(follow(ty)))
@ -955,13 +964,6 @@ BuiltinTypes::BuiltinTypes()
, uninhabitableTypePack(arena->addTypePack(TypePackVar{TypePack{{neverType}, neverTypePack}, /*persistent*/ true}))
, errorTypePack(arena->addTypePack(TypePackVar{Unifiable::Error{}, /*persistent*/ true}))
{
if (!FFlag::LuauInitializeStringMetatableInGlobalTypes)
{
TypeId stringMetatable = makeStringMetatable(NotNull{this});
asMutable(stringType)->ty = PrimitiveType{PrimitiveType::String, stringMetatable};
persist(stringMetatable);
}
freeze(*arena);
}
@ -999,7 +1001,7 @@ TypePackId BuiltinTypes::errorRecoveryTypePack(TypePackId guess) const
void persist(TypeId ty)
{
std::deque<TypeId> queue{ty};
VecDeque<TypeId> queue{ty};
while (!queue.empty())
{

View File

@ -1351,111 +1351,131 @@ struct TypeChecker2
ErrorVec argumentErrors;
// Reminder: Functions have parameters. You provide arguments.
auto paramIter = begin(fn->argTypes);
size_t argOffset = 0;
TypeId prospectiveFunction = arena->addType(FunctionType{arena->addTypePack(*args), builtinTypes->anyTypePack});
SubtypingResult sr = subtyping->isSubtype(fnTy, prospectiveFunction);
while (paramIter != end(fn->argTypes))
if (sr.isSubtype)
return {Analysis::Ok, {}};
if (1 == sr.reasoning.size())
{
if (argOffset >= args->head.size())
break;
const SubtypingReasoning& reason = *sr.reasoning.begin();
TypeId paramTy = *paramIter;
TypeId argTy = args->head[argOffset];
AstExpr* argLoc = argExprs->at(argOffset >= argExprs->size() ? argExprs->size() - 1 : argOffset);
const TypePath::Path justArguments{TypePath::PackField::Arguments};
if (auto errors = testIsSubtype(argLoc->location, argTy, paramTy))
if (reason.subPath == justArguments && reason.superPath == justArguments)
{
// Since we're stopping right here, we need to decide if this is a nonviable overload or if there is an arity mismatch.
// If it's a nonviable overload, then we need to keep going to get all type errors.
auto [minParams, optMaxParams] = getParameterExtents(TxnLog::empty(), fn->argTypes);
if (args->head.size() < minParams)
return {ArityMismatch, *errors};
else
argumentErrors.insert(argumentErrors.end(), errors->begin(), errors->end());
}
// If the subtype test failed only due to an arity mismatch,
// it is still possible that this function call is okay.
// Subtype testing does not know anything about optional
// function arguments.
//
// This can only happen if the actual function call has a
// finite set of arguments which is too short for the
// function being called. If all of those unsatisfied
// function arguments are options, then this function call
// is ok.
++paramIter;
++argOffset;
}
const size_t firstUnsatisfiedArgument = argExprs->size();
const auto [requiredHead, _requiredTail] = flatten(fn->argTypes);
while (argOffset < args->head.size())
{
// If we can iterate over the head of arguments, then we have exhausted the head of the parameters.
LUAU_ASSERT(paramIter == end(fn->argTypes));
// If too many arguments were supplied, this overload
// definitely does not match.
if (args->head.size() > requiredHead.size())
{
auto [minParams, optMaxParams] = getParameterExtents(TxnLog::empty(), fn->argTypes);
TypeError error{fnExpr->location, CountMismatch{minParams, optMaxParams, args->head.size(), CountMismatch::Arg, false}};
AstExpr* argExpr = argExprs->at(argOffset >= argExprs->size() ? argExprs->size() - 1 : argOffset);
return {Analysis::ArityMismatch, {error}};
}
if (!paramIter.tail())
{
auto [minParams, optMaxParams] = getParameterExtents(TxnLog::empty(), fn->argTypes);
TypeError error{argExpr->location, CountMismatch{minParams, optMaxParams, args->head.size(), CountMismatch::Arg, false}};
return {ArityMismatch, {error}};
}
else if (auto vtp = get<VariadicTypePack>(follow(paramIter.tail())))
{
if (auto errors = testIsSubtype(argExpr->location, args->head[argOffset], vtp->ty))
argumentErrors.insert(argumentErrors.end(), errors->begin(), errors->end());
}
else if (get<GenericTypePack>(follow(paramIter.tail())))
argumentErrors.push_back(TypeError{argExpr->location, TypePackMismatch{fn->argTypes, arena->addTypePack(*args)}});
// If any of the unsatisfied arguments are not supertypes of
// nil, then this overload does not match.
for (size_t i = firstUnsatisfiedArgument; i < requiredHead.size(); ++i)
{
if (!subtyping->isSubtype(builtinTypes->nilType, requiredHead[i]).isSubtype)
{
auto [minParams, optMaxParams] = getParameterExtents(TxnLog::empty(), fn->argTypes);
TypeError error{fnExpr->location, CountMismatch{minParams, optMaxParams, args->head.size(), CountMismatch::Arg, false}};
++argOffset;
}
return {Analysis::ArityMismatch, {error}};
}
}
while (paramIter != end(fn->argTypes))
{
// If we can iterate over parameters, then we have exhausted the head of the arguments.
LUAU_ASSERT(argOffset == args->head.size());
// It may have a tail, however, so check that.
if (auto vtp = get<VariadicTypePack>(follow(args->tail)))
{
AstExpr* argExpr = argExprs->at(argExprs->size() - 1);
if (auto errors = testIsSubtype(argExpr->location, vtp->ty, *paramIter))
argumentErrors.insert(argumentErrors.end(), errors->begin(), errors->end());
}
else if (!isOptional(*paramIter))
{
AstExpr* argExpr = argExprs->empty() ? fnExpr : argExprs->at(argExprs->size() - 1);
// It is ok to have excess parameters as long as they are all optional.
auto [minParams, optMaxParams] = getParameterExtents(TxnLog::empty(), fn->argTypes);
TypeError error{argExpr->location, CountMismatch{minParams, optMaxParams, args->head.size(), CountMismatch::Arg, false}};
return {ArityMismatch, {error}};
}
++paramIter;
}
// We hit the end of the heads for both parameters and arguments, so check their tails.
LUAU_ASSERT(paramIter == end(fn->argTypes));
LUAU_ASSERT(argOffset == args->head.size());
const Location argLoc = argExprs->empty() ? Location{} // TODO
: argExprs->at(argExprs->size() - 1)->location;
if (paramIter.tail() && args->tail)
{
if (auto errors = testIsSubtype(argLoc, *args->tail, *paramIter.tail()))
argumentErrors.insert(argumentErrors.end(), errors->begin(), errors->end());
}
else if (paramIter.tail())
{
const TypePackId paramTail = follow(*paramIter.tail());
if (get<GenericTypePack>(paramTail))
{
argumentErrors.push_back(TypeError{argLoc, TypePackMismatch{fn->argTypes, arena->addTypePack(*args)}});
}
else if (get<VariadicTypePack>(paramTail))
{
// Nothing. This is ok.
return {Analysis::Ok, {}};
}
}
return {argumentErrors.empty() ? Ok : OverloadIsNonviable, argumentErrors};
ErrorVec errors;
if (!sr.isErrorSuppressing)
{
for (const SubtypingReasoning& reason : sr.reasoning)
{
/* The return type of our prospective function is always
* any... so any subtype failures here can only arise from
* argument type mismatches.
*/
Location argLocation;
if (const Luau::TypePath::Index* pathIndexComponent = get_if<Luau::TypePath::Index>(&reason.superPath.components.at(1)))
{
size_t nthArgument = pathIndexComponent->index;
argLocation = argExprs->at(nthArgument)->location;
std::optional<TypeId> failedSubTy = traverseForType(fnTy, reason.subPath, builtinTypes);
std::optional<TypeId> failedSuperTy = traverseForType(prospectiveFunction, reason.superPath, builtinTypes);
if (failedSubTy && failedSuperTy)
{
// TODO extract location from the SubtypingResult path and argExprs
switch (reason.variance)
{
case SubtypingVariance::Covariant:
case SubtypingVariance::Contravariant:
errors.emplace_back(argLocation, TypeMismatch{*failedSubTy, *failedSuperTy, TypeMismatch::CovariantContext});
break;
case SubtypingVariance::Invariant:
errors.emplace_back(argLocation, TypeMismatch{*failedSubTy, *failedSuperTy, TypeMismatch::InvariantContext});
break;
default:
LUAU_ASSERT(0);
break;
}
}
}
std::optional<TypePackId> failedSubPack = traverseForPack(fnTy, reason.subPath, builtinTypes);
std::optional<TypePackId> failedSuperPack = traverseForPack(prospectiveFunction, reason.superPath, builtinTypes);
if (failedSubPack && failedSuperPack)
{
LUAU_ASSERT(!argExprs->empty());
argLocation = argExprs->at(argExprs->size() - 1)->location;
// TODO extract location from the SubtypingResult path and argExprs
switch (reason.variance)
{
case SubtypingVariance::Covariant:
errors.emplace_back(argLocation, TypePackMismatch{*failedSubPack, *failedSuperPack});
break;
case SubtypingVariance::Contravariant:
errors.emplace_back(argLocation, TypePackMismatch{*failedSuperPack, *failedSubPack});
break;
case SubtypingVariance::Invariant:
errors.emplace_back(argLocation, TypePackMismatch{*failedSubPack, *failedSuperPack});
break;
default:
LUAU_ASSERT(0);
break;
}
}
}
}
return {Analysis::OverloadIsNonviable, std::move(errors)};
}
size_t indexof(Analysis analysis)
@ -1494,7 +1514,6 @@ struct TypeChecker2
arityMismatches.emplace_back(ty, std::move(errors));
break;
case OverloadIsNonviable:
LUAU_ASSERT(!errors.empty());
nonviableOverloads.emplace_back(ty, std::move(errors));
break;
}

View File

@ -14,6 +14,7 @@
#include "Luau/TypeCheckLimits.h"
#include "Luau/TypeUtils.h"
#include "Luau/Unifier2.h"
#include "Luau/VecDeque.h"
#include "Luau/VisitType.h"
LUAU_DYNAMIC_FASTINTVARIABLE(LuauTypeFamilyGraphReductionMaximumSteps, 1'000'000);
@ -23,8 +24,8 @@ namespace Luau
struct InstanceCollector : TypeOnceVisitor
{
std::deque<TypeId> tys;
std::deque<TypePackId> tps;
VecDeque<TypeId> tys;
VecDeque<TypePackId> tps;
bool visit(TypeId ty, const TypeFamilyInstanceType&) override
{
@ -60,8 +61,8 @@ struct FamilyReducer
{
TypeFamilyContext ctx;
std::deque<TypeId> queuedTys;
std::deque<TypePackId> queuedTps;
VecDeque<TypeId> queuedTys;
VecDeque<TypePackId> queuedTps;
DenseHashSet<const void*> irreducible{nullptr};
FamilyGraphReductionResult result;
bool force = false;
@ -69,7 +70,7 @@ struct FamilyReducer
// Local to the constraint being reduced.
Location location;
FamilyReducer(std::deque<TypeId> queuedTys, std::deque<TypePackId> queuedTps, Location location, TypeFamilyContext ctx, bool force = false)
FamilyReducer(VecDeque<TypeId> queuedTys, VecDeque<TypePackId> queuedTps, Location location, TypeFamilyContext ctx, bool force = false)
: ctx(ctx)
, queuedTys(std::move(queuedTys))
, queuedTps(std::move(queuedTps))
@ -258,7 +259,7 @@ struct FamilyReducer
};
static FamilyGraphReductionResult reduceFamiliesInternal(
std::deque<TypeId> queuedTys, std::deque<TypePackId> queuedTps, Location location, TypeFamilyContext ctx, bool force)
VecDeque<TypeId> queuedTys, VecDeque<TypePackId> queuedTps, Location location, TypeFamilyContext ctx, bool force)
{
FamilyReducer reducer{std::move(queuedTys), std::move(queuedTps), location, ctx, force};
int iterationCount = 0;
@ -1051,6 +1052,188 @@ TypeFamilyReductionResult<TypeId> refineFamilyFn(const std::vector<TypeId>& type
return {resultTy, false, {}, {}};
}
// computes the keys of `ty` into `result`
// `isRaw` parameter indicates whether or not we should follow __index metamethods
// returns `false` if `result` should be ignored because the answer is "all strings"
bool computeKeysOf(TypeId ty, DenseHashSet<std::string>& result, DenseHashSet<TypeId>& seen, bool isRaw, NotNull<TypeFamilyContext> ctx)
{
// if the type is the top table type, the answer is just "all strings"
if (get<PrimitiveType>(ty))
return false;
// if we've already seen this type, we can do nothing
if (seen.contains(ty))
return true;
seen.insert(ty);
// if we have a particular table type, we can insert the keys
if (auto tableTy = get<TableType>(ty))
{
for (auto [key, _] : tableTy->props)
result.insert(key);
return true;
}
// otherwise, we have a metatable to deal with
if (auto metatableTy = get<MetatableType>(ty))
{
bool res = true;
if (!isRaw)
{
// findMetatableEntry demands the ability to emit errors, so we must give it
// the necessary state to do that, even if we intend to just eat the errors.
ErrorVec dummy;
std::optional<TypeId> mmType = findMetatableEntry(ctx->builtins, dummy, ty, "__index", Location{});
if (mmType)
res = res && computeKeysOf(*mmType, result, seen, isRaw, ctx);
}
res = res && computeKeysOf(metatableTy->table, result, seen, isRaw, ctx);
return res;
}
// this should not be reachable since the type should be a valid tables part from normalization.
LUAU_ASSERT(false);
return false;
}
TypeFamilyReductionResult<TypeId> keyofFamilyImpl(const std::vector<TypeId>& typeParams, const std::vector<TypePackId>& packParams, NotNull<TypeFamilyContext> ctx, bool isRaw)
{
if (typeParams.size() != 1 || !packParams.empty())
{
ctx->ice->ice("keyof type family: encountered a type family instance without the required argument structure");
LUAU_ASSERT(false);
}
TypeId operandTy = follow(typeParams.at(0));
const NormalizedType* normTy = ctx->normalizer->normalize(operandTy);
// if the operand failed to normalize, we can't reduce, but know nothing about inhabitance.
if (!normTy)
return {std::nullopt, false, {}, {}};
// if we don't have either just tables or just classes, we've got nothing to get keys of (at least until a future version perhaps adds classes as well)
if (normTy->hasTables() == normTy->hasClasses())
return {std::nullopt, true, {}, {}};
// this is sort of atrocious, but we're trying to reject any type that has not normalized to a table or a union of tables.
if (normTy->hasTops() || normTy->hasBooleans() || normTy->hasErrors() || normTy->hasNils() || normTy->hasNumbers() || normTy->hasStrings() ||
normTy->hasThreads() || normTy->hasBuffers() || normTy->hasFunctions() || normTy->hasTyvars())
return {std::nullopt, true, {}, {}};
// we're going to collect the keys in here
DenseHashSet<std::string> keys{{}};
// computing the keys for classes
if (normTy->hasClasses())
{
LUAU_ASSERT(!normTy->hasTables());
auto classesIter = normTy->classes.ordering.begin();
auto classesIterEnd = normTy->classes.ordering.end();
LUAU_ASSERT(classesIter != classesIterEnd); // should be guaranteed by the `hasClasses` check
auto classTy = get<ClassType>(*classesIter);
if (!classTy)
{
LUAU_ASSERT(false); // this should not be possible according to normalization's spec
return {std::nullopt, true, {}, {}};
}
for (auto [key, _] : classTy->props)
keys.insert(key);
// we need to check that if there are multiple classes, they have the same set of keys
while (++classesIter != classesIterEnd)
{
auto classTy = get<ClassType>(*classesIter);
if (!classTy)
{
LUAU_ASSERT(false); // this should not be possible according to normalization's spec
return {std::nullopt, true, {}, {}};
}
for (auto [key, _] : classTy->props)
{
// we will refuse to reduce if the keys are not exactly the same
if (!keys.contains(key))
return {std::nullopt, true, {}, {}};
}
}
}
// computing the keys for tables
if (normTy->hasTables())
{
LUAU_ASSERT(!normTy->hasClasses());
// seen set for key computation for tables
DenseHashSet<TypeId> seen{{}};
auto tablesIter = normTy->tables.begin();
LUAU_ASSERT(tablesIter != normTy->tables.end()); // should be guaranteed by the `hasTables` check earlier
// collect all the properties from the first table type
if (!computeKeysOf(*tablesIter, keys, seen, isRaw, ctx))
return {ctx->builtins->stringType, false, {}, {}}; // if it failed, we have the top table type!
// we need to check that if there are multiple tables, they have the same set of keys
while (++tablesIter != normTy->tables.end())
{
seen.clear(); // we'll reuse the same seen set
DenseHashSet<std::string> localKeys{{}};
// the type family is irreducible if there's _also_ the top table type in here
if (!computeKeysOf(*tablesIter, localKeys, seen, isRaw, ctx))
return {std::nullopt, true, {}, {}};
// the type family is irreducible if the key sets are not equal.
if (localKeys != keys)
return {std::nullopt, true, {}, {}};
}
}
// if the set of keys is empty, `keyof<T>` is `never`
if (keys.empty())
return {ctx->builtins->neverType, false, {}, {}};
// everything is validated, we need only construct our big union of singletons now!
std::vector<TypeId> singletons;
singletons.reserve(keys.size());
for (std::string key : keys)
singletons.push_back(ctx->arena->addType(SingletonType{StringSingleton{key}}));
return {ctx->arena->addType(UnionType{singletons}), false, {}, {}};
}
TypeFamilyReductionResult<TypeId> keyofFamilyFn(const std::vector<TypeId>& typeParams, const std::vector<TypePackId>& packParams, NotNull<TypeFamilyContext> ctx)
{
if (typeParams.size() != 1 || !packParams.empty())
{
ctx->ice->ice("keyof type family: encountered a type family instance without the required argument structure");
LUAU_ASSERT(false);
}
return keyofFamilyImpl(typeParams, packParams, ctx, /* isRaw */ false);
}
TypeFamilyReductionResult<TypeId> rawkeyofFamilyFn(const std::vector<TypeId>& typeParams, const std::vector<TypePackId>& packParams, NotNull<TypeFamilyContext> ctx)
{
if (typeParams.size() != 1 || !packParams.empty())
{
ctx->ice->ice("rawkeyof type family: encountered a type family instance without the required argument structure");
LUAU_ASSERT(false);
}
return keyofFamilyImpl(typeParams, packParams, ctx, /* isRaw */ true);
}
BuiltinTypeFamilies::BuiltinTypeFamilies()
: notFamily{"not", notFamilyFn}
, lenFamily{"len", lenFamilyFn}
@ -1069,6 +1252,8 @@ BuiltinTypeFamilies::BuiltinTypeFamilies()
, leFamily{"le", leFamilyFn}
, eqFamily{"eq", eqFamilyFn}
, refineFamily{"refine", refineFamilyFn}
, keyofFamily{"keyof", keyofFamilyFn}
, rawkeyofFamily{"rawkeyof", rawkeyofFamilyFn}
{
}
@ -1107,6 +1292,9 @@ void BuiltinTypeFamilies::addToScope(NotNull<TypeArena> arena, NotNull<Scope> sc
scope->exportedTypeBindings[ltFamily.name] = mkBinaryTypeFamily(&ltFamily);
scope->exportedTypeBindings[leFamily.name] = mkBinaryTypeFamily(&leFamily);
scope->exportedTypeBindings[eqFamily.name] = mkBinaryTypeFamily(&eqFamily);
scope->exportedTypeBindings[keyofFamily.name] = mkUnaryTypeFamily(&keyofFamily);
scope->exportedTypeBindings[rawkeyofFamily.name] = mkUnaryTypeFamily(&rawkeyofFamily);
}
} // namespace Luau

View File

@ -40,6 +40,7 @@ LUAU_FASTFLAGVARIABLE(LuauLoopControlFlowAnalysis, false)
LUAU_FASTFLAGVARIABLE(LuauAlwaysCommitInferencesOfFunctionCalls, false)
LUAU_FASTFLAG(LuauBufferTypeck)
LUAU_FASTFLAGVARIABLE(LuauRemoveBadRelationalOperatorWarning, false)
LUAU_FASTFLAGVARIABLE(LuauForbidAliasNamedTypeof, false)
namespace Luau
{
@ -668,7 +669,7 @@ LUAU_NOINLINE void TypeChecker::checkBlockTypeAliases(const ScopePtr& scope, std
{
if (const auto& typealias = stat->as<AstStatTypeAlias>())
{
if (typealias->name == kParseNameError)
if (typealias->name == kParseNameError || (FFlag::LuauForbidAliasNamedTypeof && typealias->name == "typeof"))
continue;
auto& bindings = typealias->exported ? scope->exportedTypeBindings : scope->privateTypeBindings;
@ -1536,6 +1537,12 @@ ControlFlow TypeChecker::check(const ScopePtr& scope, const AstStatTypeAlias& ty
if (name == kParseNameError)
return ControlFlow::None;
if (FFlag::LuauForbidAliasNamedTypeof && name == "typeof")
{
reportError(typealias.location, GenericError{"Type aliases cannot be named typeof"});
return ControlFlow::None;
}
std::optional<TypeFun> binding;
if (auto it = scope->exportedTypeBindings.find(name); it != scope->exportedTypeBindings.end())
binding = it->second;
@ -1649,7 +1656,9 @@ void TypeChecker::prototype(const ScopePtr& scope, const AstStatTypeAlias& typea
Name name = typealias.name.value;
// If the alias is missing a name, we can't do anything with it. Ignore it.
if (name == kParseNameError)
// Also, typeof is not a valid type alias name. We will report an error for
// this in check()
if (name == kParseNameError || (FFlag::LuauForbidAliasNamedTypeof && name == "typeof"))
return;
std::optional<TypeFun> binding;

View File

@ -142,56 +142,6 @@ struct CompileStats
Luau::CodeGen::LoweringStats lowerStats;
void serializeToJson(FILE* fp)
{
// use compact one-line formatting to reduce file length
fprintf(fp, "{\
\"lines\": %zu, \
\"bytecode\": %zu, \
\"bytecodeInstructionCount\": %zu, \
\"codegen\": %zu, \
\"readTime\": %f, \
\"miscTime\": %f, \
\"parseTime\": %f, \
\"compileTime\": %f, \
\"codegenTime\": %f, \
\"lowerStats\": {\
\"totalFunctions\": %u, \
\"skippedFunctions\": %u, \
\"spillsToSlot\": %d, \
\"spillsToRestore\": %d, \
\"maxSpillSlotsUsed\": %u, \
\"blocksPreOpt\": %u, \
\"blocksPostOpt\": %u, \
\"maxBlockInstructions\": %u, \
\"regAllocErrors\": %d, \
\"loweringErrors\": %d\
}, \
\"blockLinearizationStats\": {\
\"constPropInstructionCount\": %u, \
\"timeSeconds\": %f\
}",
lines, bytecode, bytecodeInstructionCount, codegen, readTime, miscTime, parseTime, compileTime, codegenTime, lowerStats.totalFunctions,
lowerStats.skippedFunctions, lowerStats.spillsToSlot, lowerStats.spillsToRestore, lowerStats.maxSpillSlotsUsed, lowerStats.blocksPreOpt,
lowerStats.blocksPostOpt, lowerStats.maxBlockInstructions, lowerStats.regAllocErrors, lowerStats.loweringErrors,
lowerStats.blockLinearizationStats.constPropInstructionCount, lowerStats.blockLinearizationStats.timeSeconds);
if (lowerStats.collectFunctionStats)
{
fprintf(fp, ", \"functions\": [");
auto functionCount = lowerStats.functions.size();
for (size_t i = 0; i < functionCount; ++i)
{
const Luau::CodeGen::FunctionStats& fstat = lowerStats.functions[i];
fprintf(fp, "{\"name\": \"%s\", \"line\": %d, \"bcodeCount\": %u, \"irCount\": %u, \"asmCount\": %u}", fstat.name.c_str(), fstat.line,
fstat.bcodeCount, fstat.irCount, fstat.asmCount);
if (i < functionCount - 1)
fprintf(fp, ", ");
}
fprintf(fp, "]");
}
fprintf(fp, "}");
}
CompileStats& operator+=(const CompileStats& that)
{
this->lines += that.lines;
@ -216,6 +166,121 @@ struct CompileStats
}
};
#define WRITE_NAME(INDENT, NAME) fprintf(fp, INDENT "\"" #NAME "\": ")
#define WRITE_PAIR(INDENT, NAME, FORMAT) fprintf(fp, INDENT "\"" #NAME "\": " FORMAT, stats.NAME)
#define WRITE_PAIR_STRING(INDENT, NAME, FORMAT) fprintf(fp, INDENT "\"" #NAME "\": " FORMAT, stats.NAME.c_str())
void serializeFunctionStats(FILE* fp, const Luau::CodeGen::FunctionStats& stats)
{
fprintf(fp, " {\n");
WRITE_PAIR_STRING(" ", name, "\"%s\",\n");
WRITE_PAIR(" ", line, "%d,\n");
WRITE_PAIR(" ", bcodeCount, "%u,\n");
WRITE_PAIR(" ", irCount, "%u,\n");
WRITE_PAIR(" ", asmCount, "%u,\n");
WRITE_PAIR(" ", asmSize, "%u,\n");
WRITE_NAME(" ", bytecodeSummary);
const size_t nestingLimit = stats.bytecodeSummary.size();
if (nestingLimit == 0)
fprintf(fp, "[]");
else
{
fprintf(fp, "[\n");
for (size_t i = 0; i < nestingLimit; ++i)
{
const std::vector<unsigned>& counts = stats.bytecodeSummary[i];
fprintf(fp, " [");
for (size_t j = 0; j < counts.size(); ++j)
{
fprintf(fp, "%u", counts[j]);
if (j < counts.size() - 1)
fprintf(fp, ", ");
}
fprintf(fp, "]");
if (i < stats.bytecodeSummary.size() - 1)
fprintf(fp, ",\n");
}
fprintf(fp, "\n ]");
}
fprintf(fp, "\n }");
}
void serializeBlockLinearizationStats(FILE* fp, const Luau::CodeGen::BlockLinearizationStats& stats)
{
fprintf(fp, "{\n");
WRITE_PAIR(" ", constPropInstructionCount, "%u,\n");
WRITE_PAIR(" ", timeSeconds, "%f\n");
fprintf(fp, " }");
}
void serializeLoweringStats(FILE* fp, const Luau::CodeGen::LoweringStats& stats)
{
fprintf(fp, "{\n");
WRITE_PAIR(" ", totalFunctions, "%u,\n");
WRITE_PAIR(" ", skippedFunctions, "%u,\n");
WRITE_PAIR(" ", spillsToSlot, "%d,\n");
WRITE_PAIR(" ", spillsToRestore, "%d,\n");
WRITE_PAIR(" ", maxSpillSlotsUsed, "%u,\n");
WRITE_PAIR(" ", blocksPreOpt, "%u,\n");
WRITE_PAIR(" ", blocksPostOpt, "%u,\n");
WRITE_PAIR(" ", maxBlockInstructions, "%u,\n");
WRITE_PAIR(" ", regAllocErrors, "%d,\n");
WRITE_PAIR(" ", loweringErrors, "%d,\n");
WRITE_NAME(" ", blockLinearizationStats);
serializeBlockLinearizationStats(fp, stats.blockLinearizationStats);
fprintf(fp, ",\n");
WRITE_NAME(" ", functions);
const size_t functionCount = stats.functions.size();
if (functionCount == 0)
fprintf(fp, "[]");
else
{
fprintf(fp, "[\n");
for (size_t i = 0; i < functionCount; ++i)
{
serializeFunctionStats(fp, stats.functions[i]);
if (i < functionCount - 1)
fprintf(fp, ",\n");
}
fprintf(fp, "\n ]");
}
fprintf(fp, "\n }");
}
void serializeCompileStats(FILE* fp, const CompileStats& stats)
{
fprintf(fp, "{\n");
WRITE_PAIR(" ", lines, "%zu,\n");
WRITE_PAIR(" ", bytecode, "%zu,\n");
WRITE_PAIR(" ", bytecodeInstructionCount, "%zu,\n");
WRITE_PAIR(" ", codegen, "%zu,\n");
WRITE_PAIR(" ", readTime, "%f,\n");
WRITE_PAIR(" ", miscTime, "%f,\n");
WRITE_PAIR(" ", parseTime, "%f,\n");
WRITE_PAIR(" ", compileTime, "%f,\n");
WRITE_PAIR(" ", codegenTime, "%f,\n");
WRITE_NAME(" ", lowerStats);
serializeLoweringStats(fp, stats.lowerStats);
fprintf(fp, "\n }");
}
#undef WRITE_NAME
#undef WRITE_PAIR
#undef WRITE_PAIR_STRING
static double recordDeltaTime(double& timer)
{
double now = Luau::TimeTrace::getClock();
@ -347,8 +412,9 @@ static void displayHelp(const char* argv0)
printf(" -g<n>: compile with debug level n (default 1, n should be between 0 and 2).\n");
printf(" --target=<target>: compile code for specific architecture (a64, x64, a64_nf, x64_ms).\n");
printf(" --timetrace: record compiler time tracing information into trace.json\n");
printf(" --record-stats=<granularity>: granularity of compilation stats (total, file, function).\n");
printf(" --bytecode-summary: Compute bytecode operation distribution.\n");
printf(" --stats-file=<filename>: file in which compilation stats will be recored (default 'stats.json').\n");
printf(" --record-stats=<granularity>: granularity of compilation stats recorded in stats.json (total, file, function).\n");
printf(" --vector-lib=<name>: name of the library providing vector type operations.\n");
printf(" --vector-ctor=<name>: name of the function constructing a vector value.\n");
printf(" --vector-type=<name>: name of the vector type.\n");
@ -394,6 +460,7 @@ int main(int argc, char** argv)
Luau::CodeGen::AssemblyOptions::Target assemblyTarget = Luau::CodeGen::AssemblyOptions::Host;
RecordStats recordStats = RecordStats::None;
std::string statsFile("stats.json");
bool bytecodeSummary = false;
for (int i = 1; i < argc; i++)
{
@ -456,10 +523,14 @@ int main(int argc, char** argv)
recordStats = RecordStats::Function;
else
{
fprintf(stderr, "Error: unknown 'granularity' for '--record-stats'\n");
fprintf(stderr, "Error: unknown 'granularity' for '--record-stats'.\n");
return 1;
}
}
else if (strncmp(argv[i], "--bytecode-summary", 18) == 0)
{
bytecodeSummary = true;
}
else if (strncmp(argv[i], "--stats-file=", 13) == 0)
{
statsFile = argv[i] + 13;
@ -498,6 +569,12 @@ int main(int argc, char** argv)
}
}
if (bytecodeSummary && (recordStats != RecordStats::Function))
{
fprintf(stderr, "'Error: Required '--record-stats=function' for '--bytecode-summary'.\n");
return 1;
}
#if !defined(LUAU_ENABLE_TIME_TRACE)
if (FFlag::DebugLuauTimeTracing)
{
@ -521,11 +598,12 @@ int main(int argc, char** argv)
fileStats.reserve(fileCount);
int failed = 0;
unsigned functionStats = (recordStats == RecordStats::Function ? Luau::CodeGen::FunctionStats_Enable : 0) |
(bytecodeSummary ? Luau::CodeGen::FunctionStats_BytecodeSummary : 0);
for (const std::string& path : files)
{
CompileStats fileStat = {};
fileStat.lowerStats.collectFunctionStats = (recordStats == RecordStats::Function);
fileStat.lowerStats.functionStatsFlags = functionStats;
failed += !compileFile(path.c_str(), compileFormat, assemblyTarget, fileStat);
stats += fileStat;
if (recordStats == RecordStats::File || recordStats == RecordStats::Function)
@ -561,7 +639,7 @@ int main(int argc, char** argv)
if (recordStats == RecordStats::Total)
{
stats.serializeToJson(fp);
serializeCompileStats(fp, stats);
}
else if (recordStats == RecordStats::File || recordStats == RecordStats::Function)
{
@ -569,8 +647,8 @@ int main(int argc, char** argv)
for (size_t i = 0; i < fileCount; ++i)
{
std::string escaped(escapeFilename(files[i]));
fprintf(fp, "\"%s\": ", escaped.c_str());
fileStats[i].serializeToJson(fp);
fprintf(fp, " \"%s\": ", escaped.c_str());
serializeCompileStats(fp, fileStats[i]);
fprintf(fp, i == (fileCount - 1) ? "\n" : ",\n");
}
fprintf(fp, "}");

View File

@ -180,6 +180,8 @@ public:
uint32_t getCodeSize() const;
unsigned getInstructionCount() const;
// Resulting data and code that need to be copied over one after the other
// The *end* of 'data' has to be aligned to 16 bytes, this will also align 'code'
std::vector<uint8_t> data;

View File

@ -187,6 +187,8 @@ public:
uint32_t getCodeSize() const;
unsigned getInstructionCount() const;
// Resulting data and code that need to be copied over one after the other
// The *end* of 'data' has to be aligned to 16 bytes, this will also align 'code'
std::vector<uint8_t> data;
@ -266,6 +268,8 @@ private:
uint8_t* codePos = nullptr;
uint8_t* codeEnd = nullptr;
unsigned instructionCount = 0;
};
} // namespace X64

View File

@ -102,6 +102,14 @@ struct BlockLinearizationStats
}
};
enum FunctionStatsFlags
{
// Enable stats collection per function
FunctionStats_Enable = 1 << 0,
// Compute function bytecode summary
FunctionStats_BytecodeSummary = 1 << 1,
};
struct FunctionStats
{
std::string name;
@ -109,6 +117,8 @@ struct FunctionStats
unsigned bcodeCount = 0;
unsigned irCount = 0;
unsigned asmCount = 0;
unsigned asmSize = 0;
std::vector<std::vector<unsigned>> bytecodeSummary;
};
struct LoweringStats
@ -127,7 +137,7 @@ struct LoweringStats
BlockLinearizationStats blockLinearizationStats;
bool collectFunctionStats = false;
unsigned functionStatsFlags = 0;
std::vector<FunctionStats> functions;
LoweringStats operator+(const LoweringStats& other) const
@ -150,7 +160,7 @@ struct LoweringStats
this->regAllocErrors += that.regAllocErrors;
this->loweringErrors += that.loweringErrors;
this->blockLinearizationStats += that.blockLinearizationStats;
if (this->collectFunctionStats)
if (this->functionStatsFlags & FunctionStats_Enable)
this->functions.insert(this->functions.end(), that.functions.begin(), that.functions.end());
return *this;
}

View File

@ -13,8 +13,6 @@
#include <stdint.h>
#include <string.h>
LUAU_FASTFLAG(LuauKeepVmapLinear2)
struct Proto;
namespace Luau
@ -89,6 +87,11 @@ enum class IrCmd : uint8_t
// B: tag
STORE_TAG,
// Store an integer into the extra field of the TValue
// A: Rn
// B: int
STORE_EXTRA,
// Store a pointer (*) into TValue
// A: Rn
// B: pointer
@ -964,7 +967,6 @@ struct IrFunction
// For each instruction, an operand that can be used to recompute the value
std::vector<IrOp> valueRestoreOps;
std::vector<uint32_t> validRestoreOpBlocks;
uint32_t validRestoreOpBlockIdx = 0;
Proto* proto = nullptr;
bool variadic = false;
@ -1108,37 +1110,21 @@ struct IrFunction
if (instIdx >= valueRestoreOps.size())
return {};
if (FFlag::LuauKeepVmapLinear2)
// When spilled, values can only reference restore operands in the current block chain
if (limitToCurrentBlock)
{
// When spilled, values can only reference restore operands in the current block chain
if (limitToCurrentBlock)
for (uint32_t blockIdx : validRestoreOpBlocks)
{
for (uint32_t blockIdx : validRestoreOpBlocks)
{
const IrBlock& block = blocks[blockIdx];
const IrBlock& block = blocks[blockIdx];
if (instIdx >= block.start && instIdx <= block.finish)
return valueRestoreOps[instIdx];
}
return {};
if (instIdx >= block.start && instIdx <= block.finish)
return valueRestoreOps[instIdx];
}
return valueRestoreOps[instIdx];
return {};
}
else
{
const IrBlock& block = blocks[validRestoreOpBlockIdx];
// When spilled, values can only reference restore operands in the current block
if (limitToCurrentBlock)
{
if (instIdx < block.start || instIdx > block.finish)
return {};
}
return valueRestoreOps[instIdx];
}
return valueRestoreOps[instIdx];
}
IrOp findRestoreOp(const IrInst& inst, bool limitToCurrentBlock) const

View File

@ -23,6 +23,7 @@ static void visitVmRegDefsUses(T& visitor, IrFunction& function, const IrInst& i
visitor.maybeUse(inst.a); // Argument can also be a VmConst
break;
case IrCmd::STORE_TAG:
case IrCmd::STORE_EXTRA:
case IrCmd::STORE_POINTER:
case IrCmd::STORE_DOUBLE:
case IrCmd::STORE_INT:

View File

@ -776,6 +776,11 @@ uint32_t AssemblyBuilderA64::getCodeSize() const
return uint32_t(codePos - code.data());
}
unsigned AssemblyBuilderA64::getInstructionCount() const
{
return unsigned(getCodeSize()) / 4;
}
bool AssemblyBuilderA64::isMaskSupported(uint32_t mask)
{
int lz = countlz(mask);

View File

@ -6,8 +6,6 @@
#include <stdarg.h>
#include <stdio.h>
LUAU_FASTFLAG(LuauCodeGenFixByteLower)
namespace Luau
{
namespace CodeGen
@ -1052,6 +1050,11 @@ uint32_t AssemblyBuilderX64::getCodeSize() const
return uint32_t(codePos - code.data());
}
unsigned AssemblyBuilderX64::getInstructionCount() const
{
return instructionCount;
}
void AssemblyBuilderX64::placeBinary(const char* name, OperandX64 lhs, OperandX64 rhs, uint8_t codeimm8, uint8_t codeimm, uint8_t codeimmImm8,
uint8_t code8rev, uint8_t coderev, uint8_t code8, uint8_t code, uint8_t opreg)
{
@ -1439,18 +1442,8 @@ void AssemblyBuilderX64::placeImm8(int32_t imm)
{
int8_t imm8 = int8_t(imm);
if (FFlag::LuauCodeGenFixByteLower)
{
LUAU_ASSERT(imm8 == imm);
place(imm8);
}
else
{
if (imm8 == imm)
place(imm8);
else
LUAU_ASSERT(!"Invalid immediate value");
}
LUAU_ASSERT(imm8 == imm);
place(imm8);
}
void AssemblyBuilderX64::placeImm16(int16_t imm)
@ -1503,6 +1496,8 @@ void AssemblyBuilderX64::commit()
{
LUAU_ASSERT(codePos <= codeEnd);
++instructionCount;
if (unsigned(codeEnd - codePos) < kMaxInstructionLength)
extend();
}

View File

@ -1,5 +1,6 @@
// This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details
#include "Luau/BytecodeSummary.h"
#include "Luau/BytecodeUtils.h"
#include "CodeGenLower.h"
#include "lua.h"
@ -36,11 +37,12 @@ FunctionBytecodeSummary FunctionBytecodeSummary::fromProto(Proto* proto, unsigne
FunctionBytecodeSummary summary(source, name, line, nestingLimit);
for (int i = 0; i < proto->sizecode; ++i)
for (int i = 0; i < proto->sizecode;)
{
Instruction insn = proto->code[i];
uint8_t op = LUAU_INSN_OP(insn);
summary.incCount(0, op);
i += Luau::getOpLength(LuauOpcode(op));
}
return summary;
@ -61,7 +63,8 @@ std::vector<FunctionBytecodeSummary> summarizeBytecode(lua_State* L, int idx, un
for (Proto* proto : protos)
{
summaries.push_back(FunctionBytecodeSummary::fromProto(proto, nestingLimit));
if (proto)
summaries.push_back(FunctionBytecodeSummary::fromProto(proto, nestingLimit));
}
return summaries;

View File

@ -1,6 +1,7 @@
// This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details
#include "Luau/CodeGen.h"
#include "Luau/BytecodeUtils.h"
#include "Luau/BytecodeSummary.h"
#include "CodeGenLower.h"
@ -93,7 +94,8 @@ static std::string getAssemblyImpl(AssemblyBuilder& build, const TValue* func, A
{
IrBuilder ir;
ir.buildFunctionIr(p);
unsigned asmCount = build.getCodeSize();
unsigned asmSize = build.getCodeSize();
unsigned asmCount = build.getInstructionCount();
if (options.includeAssembly || options.includeIr)
logFunctionHeader(build, p);
@ -103,6 +105,7 @@ static std::string getAssemblyImpl(AssemblyBuilder& build, const TValue* func, A
if (build.logText)
build.logAppend("; skipping (can't lower)\n");
asmSize = 0;
asmCount = 0;
if (stats)
@ -110,16 +113,25 @@ static std::string getAssemblyImpl(AssemblyBuilder& build, const TValue* func, A
}
else
{
asmCount = build.getCodeSize() - asmCount;
asmSize = build.getCodeSize() - asmSize;
asmCount = build.getInstructionCount() - asmCount;
}
if (stats && stats->collectFunctionStats)
if (stats && (stats->functionStatsFlags & FunctionStats_Enable))
{
const char* name = p->debugname ? getstr(p->debugname) : "";
int line = p->linedefined;
unsigned bcodeCount = getInstructionCount(p->code, p->sizecode);
unsigned irCount = unsigned(ir.function.instructions.size());
stats->functions.push_back({name, line, bcodeCount, irCount, asmCount});
FunctionStats functionStat;
functionStat.name = p->debugname ? getstr(p->debugname) : "";
functionStat.line = p->linedefined;
functionStat.bcodeCount = getInstructionCount(p->code, p->sizecode);
functionStat.irCount = unsigned(ir.function.instructions.size());
functionStat.asmSize = asmSize;
functionStat.asmCount = asmCount;
if (stats->functionStatsFlags & FunctionStats_BytecodeSummary)
{
FunctionBytecodeSummary summary(FunctionBytecodeSummary::fromProto(p, 0));
functionStat.bytecodeSummary.push_back(summary.getCounts(0));
}
stats->functions.push_back(std::move(functionStat));
}
if (build.logText)

View File

@ -26,7 +26,6 @@ LUAU_FASTFLAG(DebugCodegenSkipNumbering)
LUAU_FASTINT(CodegenHeuristicsInstructionLimit)
LUAU_FASTINT(CodegenHeuristicsBlockLimit)
LUAU_FASTINT(CodegenHeuristicsBlockInstructionLimit)
LUAU_FASTFLAG(LuauKeepVmapLinear2)
namespace Luau
{
@ -113,16 +112,8 @@ inline bool lowerImpl(AssemblyBuilder& build, IrLowering& lowering, IrFunction&
toStringDetailed(ctx, block, blockIndex, /* includeUseInfo */ true);
}
if (FFlag::LuauKeepVmapLinear2)
{
// Values can only reference restore operands in the current block chain
function.validRestoreOpBlocks.push_back(blockIndex);
}
else
{
// Values can only reference restore operands in the current block
function.validRestoreOpBlockIdx = blockIndex;
}
// Values can only reference restore operands in the current block chain
function.validRestoreOpBlocks.push_back(blockIndex);
build.setLabel(block.label);
@ -209,7 +200,7 @@ inline bool lowerImpl(AssemblyBuilder& build, IrLowering& lowering, IrFunction&
if (options.includeIr)
build.logAppend("#\n");
if (FFlag::LuauKeepVmapLinear2 && block.expectedNextBlock == ~0u)
if (block.expectedNextBlock == ~0u)
function.validRestoreOpBlocks.clear();
}

View File

@ -112,6 +112,11 @@ inline OperandX64 luauRegTag(int ri)
return dword[rBase + ri * sizeof(TValue) + offsetof(TValue, tt)];
}
inline OperandX64 luauRegExtra(int ri)
{
return dword[rBase + ri * sizeof(TValue) + offsetof(TValue, extra)];
}
inline OperandX64 luauRegValueInt(int ri)
{
return dword[rBase + ri * sizeof(TValue) + offsetof(TValue, value)];

View File

@ -400,8 +400,9 @@ void emitInstForGLoop(AssemblyBuilderX64& build, int ra, int aux, Label& loopRep
build.cmp(dword[elemPtr + offsetof(TValue, tt)], LUA_TNIL);
build.jcc(ConditionX64::Equal, skipArrayNil);
// setpvalue(ra + 2, reinterpret_cast<void*>(uintptr_t(index + 1)));
// setpvalue(ra + 2, reinterpret_cast<void*>(uintptr_t(index + 1)), LU_TAG_ITERATOR);
build.mov(luauRegValue(ra + 2), index);
// Extra should already be set to LU_TAG_ITERATOR
// Tag should already be set to lightuserdata
// setnvalue(ra + 3, double(index + 1));

View File

@ -13,8 +13,6 @@
#include <string.h>
LUAU_FASTFLAGVARIABLE(LuauCodegenBytecodeInfer, false)
namespace Luau
{
namespace CodeGen
@ -123,8 +121,7 @@ void IrBuilder::buildFunctionIr(Proto* proto)
rebuildBytecodeBasicBlocks(proto);
// Infer register tags in bytecode
if (FFlag::LuauCodegenBytecodeInfer)
analyzeBytecodeTypes(function);
analyzeBytecodeTypes(function);
function.bcMapping.resize(proto->sizecode, {~0u, ~0u});
@ -231,8 +228,7 @@ void IrBuilder::rebuildBytecodeBasicBlocks(Proto* proto)
}
}
if (FFlag::LuauCodegenBytecodeInfer)
buildBytecodeBlocks(function, jumpTargets);
buildBytecodeBlocks(function, jumpTargets);
}
void IrBuilder::translateInst(LuauOpcode op, const Instruction* pc, int i)

View File

@ -103,6 +103,8 @@ const char* getCmdName(IrCmd cmd)
return "GET_CLOSURE_UPVAL_ADDR";
case IrCmd::STORE_TAG:
return "STORE_TAG";
case IrCmd::STORE_EXTRA:
return "STORE_EXTRA";
case IrCmd::STORE_POINTER:
return "STORE_POINTER";
case IrCmd::STORE_DOUBLE:

View File

@ -416,6 +416,21 @@ void IrLoweringA64::lowerInst(IrInst& inst, uint32_t index, const IrBlock& next)
}
break;
}
case IrCmd::STORE_EXTRA:
{
AddressA64 addr = tempAddr(inst.a, offsetof(TValue, extra));
if (intOp(inst.b) == 0)
{
build.str(wzr, addr);
}
else
{
RegisterA64 temp = regs.allocTemp(KindA64::w);
build.mov(temp, intOp(inst.b));
build.str(temp, addr);
}
break;
}
case IrCmd::STORE_DOUBLE:
{
AddressA64 addr = tempAddr(inst.a, offsetof(TValue, value));

View File

@ -15,8 +15,6 @@
#include "lstate.h"
#include "lgc.h"
LUAU_FASTFLAG(LuauCodeGenFixByteLower)
namespace Luau
{
namespace CodeGen
@ -212,7 +210,9 @@ void IrLoweringX64::lowerInst(IrInst& inst, uint32_t index, const IrBlock& next)
build.mov(luauRegTag(vmRegOp(inst.a)), tagOp(inst.b));
}
else
{
LUAU_ASSERT(!"Unsupported instruction form");
}
break;
case IrCmd::STORE_POINTER:
{
@ -233,6 +233,19 @@ void IrLoweringX64::lowerInst(IrInst& inst, uint32_t index, const IrBlock& next)
}
break;
}
case IrCmd::STORE_EXTRA:
if (inst.b.kind == IrOpKind::Constant)
{
if (inst.a.kind == IrOpKind::Inst)
build.mov(dword[regOp(inst.a) + offsetof(TValue, extra)], intOp(inst.b));
else
build.mov(luauRegExtra(vmRegOp(inst.a)), intOp(inst.b));
}
else
{
LUAU_ASSERT(!"Unsupported instruction form");
}
break;
case IrCmd::STORE_DOUBLE:
{
OperandX64 valueLhs = inst.a.kind == IrOpKind::Inst ? qword[regOp(inst.a) + offsetof(TValue, value)] : luauRegValue(vmRegOp(inst.a));
@ -1803,18 +1816,9 @@ void IrLoweringX64::lowerInst(IrInst& inst, uint32_t index, const IrBlock& next)
case IrCmd::BUFFER_WRITEI8:
{
if (FFlag::LuauCodeGenFixByteLower)
{
OperandX64 value = inst.c.kind == IrOpKind::Inst ? byteReg(regOp(inst.c)) : OperandX64(int8_t(intOp(inst.c)));
OperandX64 value = inst.c.kind == IrOpKind::Inst ? byteReg(regOp(inst.c)) : OperandX64(int8_t(intOp(inst.c)));
build.mov(byte[bufferAddrOp(inst.a, inst.b)], value);
}
else
{
OperandX64 value = inst.c.kind == IrOpKind::Inst ? byteReg(regOp(inst.c)) : OperandX64(intOp(inst.c));
build.mov(byte[bufferAddrOp(inst.a, inst.b)], value);
}
build.mov(byte[bufferAddrOp(inst.a, inst.b)], value);
break;
}
@ -1832,18 +1836,9 @@ void IrLoweringX64::lowerInst(IrInst& inst, uint32_t index, const IrBlock& next)
case IrCmd::BUFFER_WRITEI16:
{
if (FFlag::LuauCodeGenFixByteLower)
{
OperandX64 value = inst.c.kind == IrOpKind::Inst ? wordReg(regOp(inst.c)) : OperandX64(int16_t(intOp(inst.c)));
OperandX64 value = inst.c.kind == IrOpKind::Inst ? wordReg(regOp(inst.c)) : OperandX64(int16_t(intOp(inst.c)));
build.mov(word[bufferAddrOp(inst.a, inst.b)], value);
}
else
{
OperandX64 value = inst.c.kind == IrOpKind::Inst ? wordReg(regOp(inst.c)) : OperandX64(intOp(inst.c));
build.mov(word[bufferAddrOp(inst.a, inst.b)], value);
}
build.mov(word[bufferAddrOp(inst.a, inst.b)], value);
break;
}

View File

@ -8,8 +8,6 @@
#include <math.h>
LUAU_FASTFLAGVARIABLE(LuauBufferTranslateIr, false)
// TODO: when nresults is less than our actual result count, we can skip computing/writing unused results
static const int kMinMaxUnrolledParams = 5;
@ -749,9 +747,6 @@ static void translateBufferArgsAndCheckBounds(IrBuilder& build, int nparams, int
static BuiltinImplResult translateBuiltinBufferRead(
IrBuilder& build, int nparams, int ra, int arg, IrOp args, int nresults, int pcpos, IrCmd readCmd, int size, IrCmd convCmd)
{
if (!FFlag::LuauBufferTranslateIr)
return {BuiltinImplType::None, -1};
if (nparams < 2 || nresults > 1)
return {BuiltinImplType::None, -1};
@ -768,9 +763,6 @@ static BuiltinImplResult translateBuiltinBufferRead(
static BuiltinImplResult translateBuiltinBufferWrite(
IrBuilder& build, int nparams, int ra, int arg, IrOp args, int nresults, int pcpos, IrCmd writeCmd, int size, IrCmd convCmd)
{
if (!FFlag::LuauBufferTranslateIr)
return {BuiltinImplType::None, -1};
if (nparams < 3 || nresults > 0)
return {BuiltinImplType::None, -1};

View File

@ -12,8 +12,7 @@
#include "lstate.h"
#include "ltm.h"
LUAU_FASTFLAGVARIABLE(LuauFullLoopLuserdata, false)
LUAU_FASTFLAGVARIABLE(LuauLoopInterruptFix, false)
LUAU_FASTFLAGVARIABLE(LuauCodegenLuData, false)
namespace Luau
{
@ -754,23 +753,11 @@ void translateInstForNLoop(IrBuilder& build, const Instruction* pc, int pcpos)
LUAU_ASSERT(!build.numericLoopStack.empty());
IrBuilder::LoopInfo loopInfo = build.numericLoopStack.back();
if (FFlag::LuauLoopInterruptFix)
{
// normally, the interrupt is placed at the beginning of the loop body by FORNPREP translation
// however, there are rare cases where FORNLOOP might not jump directly to the first loop instruction
// we detect this by checking the starting instruction of the loop body from loop information stack
if (repeatJumpTarget != loopInfo.startpc)
build.inst(IrCmd::INTERRUPT, build.constUint(pcpos));
}
else
{
// normally, the interrupt is placed at the beginning of the loop body by FORNPREP translation
// however, there are rare contrived cases where FORNLOOP ends up jumping to itself without an interrupt placed
// we detect this by checking if loopRepeat has any instructions (it should normally start with INTERRUPT) and emit a failsafe INTERRUPT if
// not
if (build.function.blockOp(loopRepeat).start == build.function.instructions.size())
build.inst(IrCmd::INTERRUPT, build.constUint(pcpos));
}
// normally, the interrupt is placed at the beginning of the loop body by FORNPREP translation
// however, there are rare cases where FORNLOOP might not jump directly to the first loop instruction
// we detect this by checking the starting instruction of the loop body from loop information stack
if (repeatJumpTarget != loopInfo.startpc)
build.inst(IrCmd::INTERRUPT, build.constUint(pcpos));
IrOp stepK = loopInfo.step;
@ -817,8 +804,12 @@ void translateInstForGPrepNext(IrBuilder& build, const Instruction* pc, int pcpo
build.inst(IrCmd::STORE_TAG, build.vmReg(ra), build.constTag(LUA_TNIL));
// setpvalue(ra + 2, reinterpret_cast<void*>(uintptr_t(0)));
build.inst(FFlag::LuauFullLoopLuserdata ? IrCmd::STORE_POINTER : IrCmd::STORE_INT, build.vmReg(ra + 2), build.constInt(0));
// setpvalue(ra + 2, reinterpret_cast<void*>(uintptr_t(0)), LU_TAG_ITERATOR);
build.inst(IrCmd::STORE_POINTER, build.vmReg(ra + 2), build.constInt(0));
if (FFlag::LuauCodegenLuData)
build.inst(IrCmd::STORE_EXTRA, build.vmReg(ra + 2), build.constInt(LU_TAG_ITERATOR));
build.inst(IrCmd::STORE_TAG, build.vmReg(ra + 2), build.constTag(LUA_TLIGHTUSERDATA));
build.inst(IrCmd::JUMP, target);
@ -849,8 +840,12 @@ void translateInstForGPrepInext(IrBuilder& build, const Instruction* pc, int pcp
build.inst(IrCmd::STORE_TAG, build.vmReg(ra), build.constTag(LUA_TNIL));
// setpvalue(ra + 2, reinterpret_cast<void*>(uintptr_t(0)));
build.inst(FFlag::LuauFullLoopLuserdata ? IrCmd::STORE_POINTER : IrCmd::STORE_INT, build.vmReg(ra + 2), build.constInt(0));
// setpvalue(ra + 2, reinterpret_cast<void*>(uintptr_t(0)), LU_TAG_ITERATOR);
build.inst(IrCmd::STORE_POINTER, build.vmReg(ra + 2), build.constInt(0));
if (FFlag::LuauCodegenLuData)
build.inst(IrCmd::STORE_EXTRA, build.vmReg(ra + 2), build.constInt(LU_TAG_ITERATOR));
build.inst(IrCmd::STORE_TAG, build.vmReg(ra + 2), build.constTag(LUA_TLIGHTUSERDATA));
build.inst(IrCmd::JUMP, target);

View File

@ -40,6 +40,7 @@ IrValueKind getCmdValueKind(IrCmd cmd)
case IrCmd::GET_CLOSURE_UPVAL_ADDR:
return IrValueKind::Pointer;
case IrCmd::STORE_TAG:
case IrCmd::STORE_EXTRA:
case IrCmd::STORE_POINTER:
case IrCmd::STORE_DOUBLE:
case IrCmd::STORE_INT:

View File

@ -28,6 +28,10 @@ void IrValueLocationTracking::beforeInstLowering(IrInst& inst)
// Tag update is a bit tricky, restore operations of values are not affected
invalidateRestoreOp(inst.a, /*skipValueInvalidation*/ true);
break;
case IrCmd::STORE_EXTRA:
// While extra field update doesn't invalidate some of the values, it can invalidate a vector type field
invalidateRestoreOp(inst.a, /*skipValueInvalidation*/ false);
break;
case IrCmd::STORE_POINTER:
case IrCmd::STORE_DOUBLE:
case IrCmd::STORE_INT:

View File

@ -17,8 +17,6 @@
LUAU_FASTINTVARIABLE(LuauCodeGenMinLinearBlockPath, 3)
LUAU_FASTINTVARIABLE(LuauCodeGenReuseSlotLimit, 64)
LUAU_FASTFLAGVARIABLE(DebugLuauAbortingChecks, false)
LUAU_FASTFLAGVARIABLE(LuauCodeGenFixByteLower, false)
LUAU_FASTFLAGVARIABLE(LuauKeepVmapLinear2, false)
LUAU_FASTFLAGVARIABLE(LuauReuseBufferChecks, false)
namespace Luau
@ -627,6 +625,8 @@ static void constPropInInst(ConstPropState& state, IrBuilder& build, IrFunction&
state.valueMap[state.versionedVmRegLoad(activeLoadCmd, source)] = activeLoadValue;
}
break;
case IrCmd::STORE_EXTRA:
break;
case IrCmd::STORE_POINTER:
if (inst.a.kind == IrOpKind::VmReg)
{
@ -1374,16 +1374,6 @@ static void constPropInBlock(IrBuilder& build, IrBlock& block, ConstPropState& s
constPropInInst(state, build, function, block, inst, index);
}
if (!FFlag::LuauKeepVmapLinear2)
{
// Value numbering and load/store propagation is not performed between blocks
state.invalidateValuePropagation();
// Same for table and buffer data propagation
state.invalidateHeapTableData();
state.invalidateHeapBufferData();
}
}
static void constPropInBlockChain(IrBuilder& build, std::vector<uint8_t>& visited, IrBlock* block, ConstPropState& state)
@ -1403,15 +1393,12 @@ static void constPropInBlockChain(IrBuilder& build, std::vector<uint8_t>& visite
constPropInBlock(build, *block, state);
if (FFlag::LuauKeepVmapLinear2)
{
// Value numbering and load/store propagation is not performed between blocks
state.invalidateValuePropagation();
// Value numbering and load/store propagation is not performed between blocks
state.invalidateValuePropagation();
// Same for table and buffer data propagation
state.invalidateHeapTableData();
state.invalidateHeapBufferData();
}
// Same for table and buffer data propagation
state.invalidateHeapTableData();
state.invalidateHeapBufferData();
// Blocks in a chain are guaranteed to follow each other
// We force that by giving all blocks the same sorting key, but consecutive chain keys

View File

@ -3,7 +3,7 @@
#include "Luau/Common.h"
#include <cstddef>
#include <stddef.h>
#include <functional>
#include <utility>
#include <type_traits>

View File

@ -0,0 +1,442 @@
// This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details
#pragma once
#include "Common.h"
#include "Luau/Common.h"
#include <algorithm>
#include <limits>
#include <memory>
#include <new>
#include <stdexcept>
namespace Luau
{
// `VecDeque` is a general double-ended implementation designed as a drop-in replacement for the
// standard library `std::deque`. It's backed by a growable ring buffer, rather than using the
// segmented queue design of `std::deque` which can degrade into a linked list in the worst case.
// The motivation for `VecDeque` as a replacement is to maintain the asymptotic complexity of
// `std::deque` while reducing overall allocations and promoting better usage of the cache. Its API
// is intended to be compatible with `std::deque` and `std::vector` as appropriate, and as such
// provides corresponding method definitions and supports the use of custom allocators.
//
// `VecDeque` offers pushing and popping from both ends with an amortized O(1) complexity. It also
// supports `std::vector`-style random-access in O(1). The implementation of buffer resizing uses
// a growth factor of 1.5x to enable better memory reuse after resizing, and reduce overall memory
// fragmentation when using the queue.
//
// Since `VecDeque` is a ring buffer, its elements are not necessarily contiguous in memory. To
// describe this, we refer to the two portions of the buffer as the `head` and the `tail`. The
// `head` is the initial portion of the queue that is on the range `[head, capacity)` and the tail
// is the (optionally) remaining portion on the range `[0, head + size - capacity)` whenever the
// `head + size` exceeds the capacity of the buffer.
//
// `VecDeque` does not currently support iteration since its primary focus is on providing
// double-ended queue functionality specifically, but it can be reasonably expanded to provide
// an iterator if we have a use-case for one in the future.
template<typename T, class Allocator = std::allocator<T>>
class VecDeque : Allocator
{
private:
static_assert(std::is_nothrow_move_constructible_v<T>);
static_assert(std::is_nothrow_move_assignable_v<T>);
T* buffer = nullptr; // the existing allocation we have backing this queue
size_t buffer_capacity = 0; // the size of our allocation
size_t head = 0; // the index of the head of the queue
size_t queue_size = 0; // the size of the queue
void destroyElements() noexcept
{
size_t head_size =
std::min(queue_size, capacity() - head); // how many elements are in the head portion (i.e. from the head to the end of the buffer)
size_t tail_size = queue_size - head_size; // how many elements are in the tail portion (i.e. any portion that wrapped to the front)
// we have to destroy every element in the head portion
for (size_t index = head; index < head + head_size; index++)
buffer[index].~T();
// and any in the tail portion, if one exists
for (size_t index = 0; index < tail_size; index++)
buffer[index].~T();
}
bool is_full()
{
return queue_size == capacity();
}
void grow()
{
size_t old_capacity = capacity();
// we use a growth factor of 1.5x (plus a constant) here in order to enable the
// previous memory to be reused after a certain number of calls to grow.
// see: https://github.com/facebook/folly/blob/main/folly/docs/FBVector.md#memory-handling
size_t new_capacity = (old_capacity > 0) ? old_capacity * 3 / 2 + 1 : 4;
// check that it's a legal allocation
if (new_capacity > max_size())
throw std::bad_array_new_length();
// allocate a new backing buffer
T* new_buffer = this->allocate(new_capacity);
// we should not be growing if the capacity is not the current size
LUAU_ASSERT(old_capacity == queue_size);
size_t head_size =
std::min(queue_size, old_capacity - head); // how many elements are in the head portion (i.e. from the head to the end of the buffer)
size_t tail_size = queue_size - head_size; // how many elements are in the tail portion (i.e. any portion that wrapped to the front)
// move the head into the new buffer
std::uninitialized_move(buffer + head, buffer + head + head_size, new_buffer);
// move the tail into the new buffer immediately after
if (head_size < queue_size)
std::uninitialized_move(buffer, buffer + tail_size, new_buffer + head_size);
// destroy the old elements
destroyElements();
// deallocate the old buffer
this->deallocate(buffer, old_capacity);
// set up the queue to be backed by the new buffer
buffer = new_buffer;
buffer_capacity = new_capacity;
head = 0;
}
size_t logicalToPhysical(size_t pos)
{
return (head + pos) % capacity();
}
public:
VecDeque() = default;
explicit VecDeque(const Allocator& alloc) noexcept
: Allocator{alloc}
{
}
VecDeque(const VecDeque& other)
: buffer(this->allocate(other.buffer_capacity))
, buffer_capacity(other.buffer_capacity)
, head(other.head)
, queue_size(other.queue_size)
{
// copy the contents of the other buffer to this one
std::uninitialized_copy(other.buffer, other.buffer + other.buffer_capacity, buffer);
}
VecDeque(const VecDeque& other, const Allocator& alloc)
: Allocator{alloc}
, buffer(this->allocate(other.buffer_capacity))
, buffer_capacity(other.buffer_capacity)
, head(other.head)
, queue_size(other.queue_size)
{
// copy the contents of the other buffer to this one
std::uninitialized_copy(other.buffer, other.buffer + other.buffer_capacity, buffer);
}
VecDeque(VecDeque&& other) noexcept
: buffer(std::exchange(other.buffer, nullptr))
, buffer_capacity(std::exchange(other.buffer_capacity, 0))
, head(std::exchange(other.head, 0))
, queue_size(std::exchange(other.queue_size, 0))
{
}
VecDeque(VecDeque&& other, const Allocator& alloc) noexcept
: Allocator{alloc}
, buffer(std::exchange(other.buffer, nullptr))
, buffer_capacity(std::exchange(other.buffer_capacity, 0))
, head(std::exchange(other.head, 0))
, queue_size(std::exchange(other.queue_size, 0))
{
}
VecDeque(std::initializer_list<T> init, const Allocator& alloc = Allocator())
: Allocator{alloc}
{
buffer = this->allocate(init.size());
buffer_capacity = init.size();
queue_size = init.size();
std::uninitialized_copy(init.begin(), init.end(), buffer);
}
~VecDeque() noexcept
{
// destroy any elements that exist
destroyElements();
// free the allocated buffer
this->deallocate(buffer, buffer_capacity);
}
VecDeque& operator=(const VecDeque& other)
{
if (this == &other)
return *this;
// destroy all of the existing elements
destroyElements();
if (buffer_capacity < other.size())
{
// free the current buffer
this->deallocate(buffer, buffer_capacity);
buffer = this->allocate(other.buffer_capacity);
buffer_capacity = other.buffer_capacity;
}
size_t head_size = other.capacity() - other.head; // how many elements are in the head portion (i.e. from the head to the end of the buffer)
size_t tail_size = other.size() - head_size; // how many elements are in the tail portion (i.e. any portion that wrapped to the front)
// copy the contents of the other buffer's head into place
std::uninitialized_copy(other.buffer + other.head, other.buffer + head + head_size, buffer);
// copy the contents of the other buffer's tail into place immediately after
std::uninitialized_copy(other.buffer, other.buffer + tail_size, buffer + head_size);
return *this;
}
VecDeque& operator=(VecDeque&& other)
{
if (this == &other)
return *this;
// destroy all of the existing elements
destroyElements();
// free the current buffer
this->deallocate(buffer, buffer_capacity);
buffer = std::exchange(other.buffer, nullptr);
buffer_capacity = std::exchange(other.buffer_capacity, 0);
head = std::exchange(other.head, 0);
queue_size = std::exchange(other.queue_size, 0);
return *this;
}
Allocator get_allocator() const noexcept
{
return this;
}
// element access
T& at(size_t pos)
{
if (pos >= queue_size)
throw std::out_of_range("VecDeque");
return buffer[logicalToPhysical(pos)];
}
const T& at(size_t pos) const
{
if (pos >= queue_size)
throw std::out_of_range("VecDeque");
return buffer[logicalToPhysical(pos)];
}
[[nodiscard]] T& operator[](size_t pos) noexcept
{
LUAU_ASSERT(pos < queue_size);
return buffer[logicalToPhysical(pos)];
}
[[nodiscard]] const T& operator[](size_t pos) const noexcept
{
LUAU_ASSERT(pos < queue_size);
return buffer[logicalToPhysical(pos)];
}
T& front() {
LUAU_ASSERT(!empty());
return buffer[head];
}
const T& front() const {
LUAU_ASSERT(!empty());
return buffer[head];
}
T& back() {
LUAU_ASSERT(!empty());
size_t back = logicalToPhysical(queue_size - 1);
return buffer[back];
}
const T& back() const {
LUAU_ASSERT(!empty());
size_t back = logicalToPhysical(queue_size - 1);
return buffer[back];
}
// capacity
bool empty() const noexcept
{
return queue_size == 0;
}
size_t size() const noexcept
{
return queue_size;
}
size_t max_size() const noexcept
{
return std::numeric_limits<size_t>::max() / sizeof(T);
}
void reserve(size_t new_capacity)
{
// error if this allocation would be illegal
if (new_capacity > max_size())
throw std::length_error("too large");
size_t old_capacity = capacity();
// do nothing if we're requesting a capacity that would not cause growth
if (new_capacity <= old_capacity)
return;
size_t head_size =
std::min(queue_size, old_capacity - head); // how many elements are in the head portion (i.e. from the head to the end of the buffer)
size_t tail_size = queue_size - head_size; // how many elements are in the tail portion (i.e. any portion that wrapped to the front)
// allocate a new backing buffer
T* new_buffer = this->allocate(new_capacity);
// move the head into the new buffer
std::uninitialized_move(buffer + head, buffer + head + head_size, new_buffer);
// move the tail into the new buffer immediately after, if we have one
if (head_size < queue_size)
std::uninitialized_move(buffer, buffer + tail_size, new_buffer + head_size);
// move the tail into the new buffer immediately after
std::uninitialized_move(buffer, buffer + tail_size, new_buffer + head_size);
// destroy all the existing elements before freeing the old buffer
destroyElements();
// deallocate the old buffer
this->deallocate(buffer, old_capacity);
// set up the queue to be backed by the new buffer
buffer = new_buffer;
buffer_capacity = new_capacity;
head = 0;
}
size_t capacity() const noexcept
{
return buffer_capacity;
}
void shrink_to_fit() {
size_t old_capacity = capacity();
size_t new_capacity = queue_size;
if (old_capacity == new_capacity)
return;
size_t head_size =
std::min(queue_size, old_capacity - head); // how many elements are in the head portion (i.e. from the head to the end of the buffer)
size_t tail_size = queue_size - head_size; // how many elements are in the tail portion (i.e. any portion that wrapped to the front)
// allocate a new backing buffer
T* new_buffer = this->allocate(new_capacity);
// move the head into the new buffer
std::uninitialized_move(buffer + head, buffer + head + head_size, new_buffer);
// move the tail into the new buffer immediately after, if we have one
if (head_size < queue_size)
std::uninitialized_move(buffer, buffer + tail_size, new_buffer + head_size);
// destroy all the existing elements before freeing the old buffer
destroyElements();
// deallocate the old buffer
this->deallocate(buffer, old_capacity);
// set up the queue to be backed by the new buffer
buffer = new_buffer;
buffer_capacity = new_capacity;
head = 0;
}
[[nodiscard]] bool is_contiguous() const noexcept
{
// this is an overflow-safe alternative to writing `head + size <= capacity`.
return head <= capacity() - queue_size;
}
// modifiers
void clear() noexcept
{
destroyElements();
head = 0;
queue_size = 0;
}
void push_back(const T& value)
{
if (is_full())
grow();
size_t next_back = logicalToPhysical(queue_size);
new (buffer + next_back)T(value);
queue_size++;
}
void pop_back()
{
LUAU_ASSERT(!empty());
queue_size--;
size_t next_back = logicalToPhysical(queue_size);
buffer[next_back].~T();
}
void push_front(const T& value)
{
if (is_full())
grow();
head = (head == 0) ? capacity() - 1 : head - 1;
new (buffer + head)T(value);
queue_size++;
}
void pop_front()
{
LUAU_ASSERT(!empty());
buffer[head].~T();
head++;
queue_size--;
if (head == capacity())
head = 0;
}
};
} // namespace Luau

View File

@ -4,10 +4,6 @@
#include "Luau/Bytecode.h"
#include "Luau/Compiler.h"
LUAU_FASTFLAGVARIABLE(LuauBit32ByteswapBuiltin, false)
LUAU_FASTFLAGVARIABLE(LuauBufferBuiltins, false)
namespace Luau
{
namespace Compile
@ -170,7 +166,7 @@ static int getBuiltinFunctionId(const Builtin& builtin, const CompileOptions& op
return LBF_BIT32_COUNTLZ;
if (builtin.method == "countrz")
return LBF_BIT32_COUNTRZ;
if (FFlag::LuauBit32ByteswapBuiltin && builtin.method == "byteswap")
if (builtin.method == "byteswap")
return LBF_BIT32_BYTESWAP;
}
@ -194,7 +190,7 @@ static int getBuiltinFunctionId(const Builtin& builtin, const CompileOptions& op
return LBF_TABLE_UNPACK;
}
if (FFlag::LuauBufferBuiltins && builtin.object == "buffer")
if (builtin.object == "buffer")
{
if (builtin.method == "readi8")
return LBF_BUFFER_READI8;

View File

@ -28,6 +28,7 @@ LUAU_FASTINTVARIABLE(LuauCompileInlineDepth, 5)
LUAU_FASTFLAGVARIABLE(LuauCompileRevK, false)
namespace Luau
{
@ -995,7 +996,11 @@ struct Compiler
bytecode.emitAD(LOP_NEWCLOSURE, target, pid);
for (const Capture& c : captures)
{
bytecode.emitABC(LOP_CAPTURE, uint8_t(c.type), c.data, 0);
}
}
LuauOpcode getUnaryOp(AstExprUnary::Op op)

View File

@ -3,8 +3,6 @@
#include "Luau/BytecodeBuilder.h"
LUAU_FASTFLAGVARIABLE(LuauCompileBufferAnnotation, false)
namespace Luau
{
@ -29,7 +27,7 @@ static LuauBytecodeType getPrimitiveType(AstName name)
return LBC_TYPE_STRING;
else if (name == "thread")
return LBC_TYPE_THREAD;
else if (FFlag::LuauCompileBufferAnnotation && name == "buffer")
else if (name == "buffer")
return LBC_TYPE_BUFFER;
else if (name == "any" || name == "unknown")
return LBC_TYPE_ANY;

View File

@ -7,6 +7,7 @@ if(NOT ${CMAKE_VERSION} VERSION_LESS "3.19")
Common/include/Luau/BytecodeUtils.h
Common/include/Luau/DenseHash.h
Common/include/Luau/ExperimentalFlags.h
Common/include/Luau/VecDeque.h
)
endif()
@ -467,6 +468,7 @@ if(TARGET Luau.UnitTest)
tests/TypeVar.test.cpp
tests/Unifier2.test.cpp
tests/Variant.test.cpp
tests/VecDeque.test.cpp
tests/VisitType.test.cpp
tests/main.cpp)
endif()

View File

@ -5,8 +5,6 @@
#include "lcommon.h"
#include "lnumutils.h"
LUAU_FASTFLAGVARIABLE(LuauBit32Byteswap, false)
#define ALLONES ~0u
#define NBITS int(8 * sizeof(unsigned))
@ -214,9 +212,6 @@ static int b_countrz(lua_State* L)
static int b_swap(lua_State* L)
{
if (!FFlag::LuauBit32Byteswap)
luaL_error(L, "bit32.byteswap isn't enabled");
b_uint n = luaL_checkunsigned(L, 1);
n = (n << 24) | ((n << 8) & 0xff0000) | ((n >> 8) & 0xff00) | (n >> 24);

View File

@ -10,8 +10,6 @@
#include <string.h>
LUAU_FASTFLAGVARIABLE(LuauBufferBetterMsg, false)
// while C API returns 'size_t' for binary compatibility in case of future extensions,
// in the current implementation, length and offset are limited to 31 bits
// because offset is limited to an integer, a single 64bit comparison can be used and will not overflow
@ -38,15 +36,7 @@ static int buffer_create(lua_State* L)
{
int size = luaL_checkinteger(L, 1);
if (FFlag::LuauBufferBetterMsg)
{
luaL_argcheck(L, size >= 0, 1, "size");
}
else
{
if (size < 0)
luaL_error(L, "invalid size");
}
luaL_argcheck(L, size >= 0, 1, "size");
lua_newbuffer(L, size);
return 1;
@ -174,15 +164,7 @@ static int buffer_readstring(lua_State* L)
int offset = luaL_checkinteger(L, 2);
int size = luaL_checkinteger(L, 3);
if (FFlag::LuauBufferBetterMsg)
{
luaL_argcheck(L, size >= 0, 3, "size");
}
else
{
if (size < 0)
luaL_error(L, "invalid size");
}
luaL_argcheck(L, size >= 0, 3, "size");
if (isoutofbounds(offset, len, unsigned(size)))
luaL_error(L, "buffer access out of bounds");
@ -200,15 +182,7 @@ static int buffer_writestring(lua_State* L)
const char* val = luaL_checklstring(L, 3, &size);
int count = luaL_optinteger(L, 4, int(size));
if (FFlag::LuauBufferBetterMsg)
{
luaL_argcheck(L, count >= 0, 4, "count");
}
else
{
if (count < 0)
luaL_error(L, "invalid count");
}
luaL_argcheck(L, count >= 0, 4, "count");
if (size_t(count) > size)
luaL_error(L, "string length overflow");

View File

@ -11,6 +11,8 @@
#include <intrin.h>
#endif
LUAU_FASTFLAGVARIABLE(LuauSciNumberSkipTrailDot, false)
// This work is based on:
// Raffaello Giulietti. The Schubfach way to render doubles. 2021
// https://drive.google.com/file/d/1IEeATSVnEE6TkrHlCYNY2GjaraBjOT4f/edit
@ -361,6 +363,9 @@ char* luai_num2str(char* buf, double n)
char* exp = trimzero(buf + declen + 1);
if (FFlag::LuauSciNumberSkipTrailDot && exp[-1] == '.')
exp--;
return printexp(exp, dot - 1);
}
}

View File

@ -242,8 +242,11 @@ typedef struct TString
// 1 byte padding
int16_t atom;
// 2 byte padding
TString* next; // next string in the hash table bucket
unsigned int hash;
@ -252,6 +255,8 @@ typedef struct TString
char data[1]; // string data is allocated right after the header
} TString;
#define getstr(ts) (ts)->data
#define svalue(o) getstr(tsvalue(o))

View File

@ -10,6 +10,7 @@
#include "ldo.h"
#include "ldebug.h"
/*
** Main thread combines a thread state and the global state
*/
@ -180,6 +181,7 @@ lua_State* lua_newstate(lua_Alloc f, void* ud)
g->uvhead.u.open.next = &g->uvhead;
g->GCthreshold = 0; // mark it as unfinished state
g->registryfree = 0;
g->errorjmp = NULL;
g->rngstate = 0;
g->ptrenckey[0] = 1;

View File

@ -201,8 +201,10 @@ typedef struct global_State
TValue pseudotemp; // storage for temporary values used in pseudo2addr
TValue registry; // registry table, used by lua_ref and LUA_REGISTRYINDEX
int registryfree; // next free slot in registry
struct lua_jmpbuf* errorjmp; // jump buffer data for longjmp-style error handling
uint64_t rngstate; // PCG random number generator state

View File

@ -7,6 +7,8 @@
#include <string.h>
unsigned int luaS_hash(const char* str, size_t len)
{
// Note that this hashing algorithm is replicated in BytecodeBuilder.cpp, BytecodeBuilder::getStringHash
@ -76,6 +78,7 @@ static TString* newlstr(lua_State* L, const char* str, size_t l, unsigned int h)
TString* ts = luaM_newgco(L, TString, sizestring(l), L->activememcat);
luaC_init(L, ts, LUA_TSTRING);
ts->atom = ATOM_UNDEF;
ts->hash = h;
ts->len = unsigned(l);
@ -102,6 +105,7 @@ TString* luaS_bufstart(lua_State* L, size_t size)
TString* ts = luaM_newgco(L, TString, sizestring(size), L->activememcat);
luaC_init(L, ts, LUA_TSTRING);
ts->atom = ATOM_UNDEF;
ts->hash = 0; // computed in luaS_buffinish
ts->len = unsigned(size);
@ -189,5 +193,6 @@ void luaS_free(lua_State* L, TString* ts, lua_Page* page)
else
LUAU_ASSERT(ts->next == NULL); // orphaned string buffer
luaM_freegco(L, ts, sizestring(ts->len), ts->memcat, page);
}

View File

@ -8,8 +8,6 @@
#define iscont(p) ((*(p)&0xC0) == 0x80)
LUAU_DYNAMIC_FASTFLAGVARIABLE(LuauStricterUtf8, false)
// from strlib
// translate a relative string position: negative means back from end
static int u_posrelat(int pos, size_t len)
@ -47,7 +45,7 @@ static const char* utf8_decode(const char* o, int* val)
res |= ((c & 0x7F) << (count * 5)); // add first byte
if (count > 3 || res > MAXUNICODE || res <= limits[count])
return NULL; // invalid byte sequence
if (DFFlag::LuauStricterUtf8 && unsigned(res - 0xD800) < 0x800)
if (unsigned(res - 0xD800) < 0x800)
return NULL; // surrogate
s += count; // skip continuation bytes read
}

View File

@ -778,6 +778,7 @@ reentry:
break;
case LCT_REF:
setupvalue(L, &ncl->l.uprefs[ui], luaF_findupval(L, VM_REG(LUAU_INSN_B(uinsn))));
break;
@ -1112,7 +1113,9 @@ reentry:
VM_NEXT();
case LUA_TLIGHTUSERDATA:
pc += (pvalue(ra) == pvalue(rb) && (!FFlag::LuauTaggedLuData || lightuserdatatag(ra) == lightuserdatatag(rb))) ? LUAU_INSN_D(insn) : 1;
pc += (pvalue(ra) == pvalue(rb) && (!FFlag::LuauTaggedLuData || lightuserdatatag(ra) == lightuserdatatag(rb)))
? LUAU_INSN_D(insn)
: 1;
LUAU_ASSERT(unsigned(pc - cl->l.p->code) < unsigned(cl->l.p->sizecode));
VM_NEXT();
@ -1227,7 +1230,9 @@ reentry:
VM_NEXT();
case LUA_TLIGHTUSERDATA:
pc += (pvalue(ra) != pvalue(rb) || (FFlag::LuauTaggedLuData && lightuserdatatag(ra) != lightuserdatatag(rb))) ? LUAU_INSN_D(insn) : 1;
pc += (pvalue(ra) != pvalue(rb) || (FFlag::LuauTaggedLuData && lightuserdatatag(ra) != lightuserdatatag(rb)))
? LUAU_INSN_D(insn)
: 1;
LUAU_ASSERT(unsigned(pc - cl->l.p->code) < unsigned(cl->l.p->sizecode));
VM_NEXT();

View File

@ -16,7 +16,6 @@
LUAU_FASTFLAG(LuauTraceTypesInNonstrictMode2)
LUAU_FASTFLAG(LuauSetMetatableDoesNotTimeTravel)
LUAU_FASTFLAG(LuauAutocompleteStringLiteralBounds);
LUAU_FASTFLAG(LuauAutocompleteDoEnd);
using namespace Luau;
@ -980,8 +979,6 @@ TEST_CASE_FIXTURE(ACFixture, "autocomplete_end_with_lambda")
TEST_CASE_FIXTURE(ACFixture, "autocomplete_end_of_do_block")
{
ScopedFastFlag sff{FFlag::LuauAutocompleteDoEnd, true};
check("do @1");
auto ac = autocomplete('1');
@ -3107,7 +3104,10 @@ TEST_CASE_FIXTURE(ACFixture, "string_singleton_as_table_key")
// https://github.com/Roblox/luau/issues/858
TEST_CASE_FIXTURE(ACFixture, "string_singleton_in_if_statement")
{
ScopedFastFlag sff{FFlag::LuauAutocompleteStringLiteralBounds, true};
ScopedFastFlag sff[]{
{FFlag::LuauAutocompleteStringLiteralBounds, true},
{FFlag::DebugLuauDeferredConstraintResolution, true},
};
check(R"(
--!strict
@ -3131,7 +3131,7 @@ TEST_CASE_FIXTURE(ACFixture, "string_singleton_in_if_statement")
ac = autocomplete('2');
CHECK(ac.entryMap.count("left"));
CHECK(ac.entryMap.count("right"));
CHECK(!ac.entryMap.count("right"));
ac = autocomplete('3');
@ -3161,7 +3161,7 @@ TEST_CASE_FIXTURE(ACFixture, "string_singleton_in_if_statement")
ac = autocomplete('8');
CHECK(ac.entryMap.count("left"));
CHECK(ac.entryMap.count("right"));
CHECK(!ac.entryMap.count("right"));
ac = autocomplete('9');

View File

@ -26,14 +26,8 @@ extern bool verbose;
extern bool codegen;
extern int optimizationLevel;
LUAU_FASTFLAG(LuauBit32Byteswap);
LUAU_FASTFLAG(LuauBufferBetterMsg);
LUAU_FASTFLAG(LuauBufferDefinitions);
LUAU_FASTFLAG(LuauCodeGenFixByteLower);
LUAU_FASTFLAG(LuauCompileBufferAnnotation);
LUAU_FASTFLAG(LuauLoopInterruptFix);
LUAU_FASTFLAG(LuauTaggedLuData);
LUAU_DYNAMIC_FASTFLAG(LuauStricterUtf8);
LUAU_FASTFLAG(LuauSciNumberSkipTrailDot);
LUAU_FASTINT(CodegenHeuristicsInstructionLimit);
static lua_CompileOptions defaultOptions()
@ -324,9 +318,6 @@ TEST_CASE("Basic")
TEST_CASE("Buffers")
{
ScopedFastFlag luauBufferBetterMsg{FFlag::LuauBufferBetterMsg, true};
ScopedFastFlag luauCodeGenFixByteLower{FFlag::LuauCodeGenFixByteLower, true};
runConformance("buffers.lua");
}
@ -441,13 +432,11 @@ TEST_CASE("GC")
TEST_CASE("Bitwise")
{
ScopedFastFlag sffs{FFlag::LuauBit32Byteswap, true};
runConformance("bitwise.lua");
}
TEST_CASE("UTF8")
{
ScopedFastFlag sff(DFFlag::LuauStricterUtf8, true);
runConformance("utf8.lua");
}
@ -592,8 +581,6 @@ static void populateRTTI(lua_State* L, Luau::TypeId type)
TEST_CASE("Types")
{
ScopedFastFlag luauBufferDefinitions{FFlag::LuauBufferDefinitions, true};
runConformance("types.lua", [](lua_State* L) {
Luau::NullModuleResolver moduleResolver;
Luau::NullFileResolver fileResolver;
@ -1439,6 +1426,8 @@ TEST_CASE("Coverage")
TEST_CASE("StringConversion")
{
ScopedFastFlag luauSciNumberSkipTrailDot{FFlag::LuauSciNumberSkipTrailDot, true};
runConformance("strconv.lua");
}
@ -1540,8 +1529,6 @@ TEST_CASE("GCDump")
TEST_CASE("Interrupt")
{
ScopedFastFlag luauLoopInterruptFix{FFlag::LuauLoopInterruptFix, true};
lua_CompileOptions copts = defaultOptions();
copts.optimizationLevel = 1; // disable loop unrolling to get fixed expected hit results
@ -1719,10 +1706,36 @@ TEST_CASE("LightuserdataApi")
CHECK(!lua_getlightuserdataname(L, 0));
CHECK(strcmp(lua_getlightuserdataname(L, 1), "id") == 0);
CHECK(strcmp(luaL_typename(L, -1), "id") == 0);
lua_pop(L, 1);
lua_pushlightuserdatatagged(L, value, 0);
lua_pushlightuserdatatagged(L, value, 1);
CHECK(lua_rawequal(L, -1, -2) == 0);
lua_pop(L, 2);
// Check lightuserdata table key uniqueness
lua_newtable(L);
lua_pushlightuserdatatagged(L, value, 2);
lua_pushinteger(L, 20);
lua_settable(L, -3);
lua_pushlightuserdatatagged(L, value, 3);
lua_pushinteger(L, 30);
lua_settable(L, -3);
lua_pushlightuserdatatagged(L, value, 2);
lua_gettable(L, -2);
lua_pushinteger(L, 20);
CHECK(lua_rawequal(L, -1, -2) == 1);
lua_pop(L, 2);
lua_pushlightuserdatatagged(L, value, 3);
lua_gettable(L, -2);
lua_pushinteger(L, 30);
CHECK(lua_rawequal(L, -1, -2) == 1);
lua_pop(L, 2);
lua_pop(L, 1);
globalState.reset();
}
@ -1962,8 +1975,6 @@ TEST_CASE("NativeTypeAnnotations")
if (!codegen || !luau_codegen_supported())
return;
ScopedFastFlag luauCompileBufferAnnotation{FFlag::LuauCompileBufferAnnotation, true};
lua_CompileOptions copts = defaultOptions();
copts.vectorCtor = "vector";
copts.vectorType = "vector";
@ -2132,14 +2143,16 @@ end
CHECK_EQ(summaries[0].getName(), "inner");
CHECK_EQ(summaries[0].getLine(), 6);
CHECK_EQ(summaries[0].getCounts(0),
std::vector<unsigned>({1, 1, 0, 0, 1, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0}));
std::vector<unsigned>({0, 0, 0, 0, 1, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0,
1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0}));
CHECK_EQ(summaries[1].getName(), "first");
CHECK_EQ(summaries[1].getLine(), 2);
CHECK_EQ(summaries[1].getCounts(0),
std::vector<unsigned>({1, 0, 1, 0, 2, 0, 3, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
std::vector<unsigned>({0, 0, 1, 0, 2, 0, 3, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}));
CHECK_EQ(summaries[2].getName(), "second");
CHECK_EQ(summaries[2].getLine(), 15);
CHECK_EQ(summaries[2].getCounts(0),

View File

@ -14,7 +14,6 @@ using namespace Luau;
LUAU_FASTFLAG(DebugLuauDeferredConstraintResolution)
LUAU_FASTFLAG(DebugLuauFreezeArena);
LUAU_FASTFLAG(CorrectEarlyReturnInMarkDirty);
namespace
{
@ -1254,8 +1253,6 @@ TEST_CASE_FIXTURE(FrontendFixture, "parse_only")
TEST_CASE_FIXTURE(FrontendFixture, "markdirty_early_return")
{
ScopedFastFlag fflag(FFlag::CorrectEarlyReturnInMarkDirty, true);
constexpr char moduleName[] = "game/Gui/Modules/A";
fileResolver.source[moduleName] = R"(
return 1

View File

@ -1048,6 +1048,24 @@ TEST_CASE_FIXTURE(SubtypeFixture, "~~number <: number")
CHECK_IS_SUBTYPE(negate(negate(builtinTypes->numberType)), builtinTypes->numberType);
}
// See https://github.com/luau-lang/luau/issues/767
TEST_CASE_FIXTURE(SubtypeFixture, "(...any) -> () <: <T>(T...) -> ()")
{
TypeId anysToNothing = arena.addType(FunctionType{builtinTypes->anyTypePack, builtinTypes->emptyTypePack});
TypeId genericTToAnys = arena.addType(FunctionType{genericAs, builtinTypes->emptyTypePack});
CHECK_MESSAGE(subtyping.isSubtype(anysToNothing, genericTToAnys).isSubtype, "(...any) -> () <: <T>(T...) -> ()");
}
// See https://github.com/luau-lang/luau/issues/767
TEST_CASE_FIXTURE(SubtypeFixture, "(...unknown) -> () <: <T>(T...) -> ()")
{
TypeId anysToNothing = arena.addType(FunctionType{arena.addTypePack(VariadicTypePack{builtinTypes->unknownType}), builtinTypes->emptyTypePack});
TypeId genericTToAnys = arena.addType(FunctionType{genericAs, builtinTypes->emptyTypePack});
CHECK_MESSAGE(subtyping.isSubtype(anysToNothing, genericTToAnys).isSubtype, "(...unknown) -> () <: <T>(T...) -> ()");
}
/*
* Within the scope to which a generic belongs, that generic ought to be treated
* as its bounds.
@ -1191,6 +1209,20 @@ TEST_CASE_FIXTURE(SubtypeFixture, "fn_arguments")
}});
}
TEST_CASE_FIXTURE(SubtypeFixture, "arity_mismatch")
{
TypeId subTy = fn({builtinTypes->numberType}, {});
TypeId superTy = fn({}, {});
SubtypingResult result = isSubtype(subTy, superTy);
CHECK(!result.isSubtype);
CHECK(result.reasoning == std::vector{SubtypingReasoning{
/* subPath */ TypePath::PathBuilder().args().build(),
/* superPath */ TypePath::PathBuilder().args().build(),
/* variance */ SubtypingVariance::Contravariant,
}});
}
TEST_CASE_FIXTURE(SubtypeFixture, "fn_arguments_tail")
{
TypeId subTy = fn({}, VariadicTypePack{builtinTypes->numberType}, {});

View File

@ -345,7 +345,7 @@ TEST_CASE("always_emit_a_space_after_local_keyword")
TEST_CASE_FIXTURE(Fixture, "types_should_not_be_considered_cyclic_if_they_are_not_recursive")
{
std::string code = R"(
local common: {foo:string}
local common: {foo:string} = {foo = 'foo'}
local t = {}
t.x = common
@ -353,7 +353,7 @@ TEST_CASE_FIXTURE(Fixture, "types_should_not_be_considered_cyclic_if_they_are_no
)";
std::string expected = R"(
local common: {foo:string}
local common: {foo:string} = {foo = 'foo'}
local t:{x:{foo:string},y:{foo:string}}={}
t.x = common

View File

@ -6,6 +6,7 @@
#include "Luau/TxnLog.h"
#include "Luau/Type.h"
#include "ClassFixture.h"
#include "Fixture.h"
#include "doctest.h"
@ -232,6 +233,31 @@ TEST_CASE_FIXTURE(Fixture, "internal_families_raise_errors")
"signature; this construct cannot be type-checked at this time");
}
TEST_CASE_FIXTURE(BuiltinsFixture, "type_families_can_be_shadowed")
{
if (!FFlag::DebugLuauDeferredConstraintResolution)
return;
CheckResult result = check(R"(
type add<T> = string -- shadow add
-- this should be ok
function hi(f: add<unknown>)
return string.format("hi %s", f)
end
-- this should still work totally fine (and use the real type family)
function plus(a, b)
return a + b
end
)");
LUAU_REQUIRE_NO_ERRORS(result);
CHECK(toString(requireType("hi")) == "(string) -> string");
CHECK(toString(requireType("plus")) == "<a, b>(a, b) -> add<a, b>");
}
TEST_CASE_FIXTURE(BuiltinsFixture, "type_families_inhabited_with_normalization")
{
if (!FFlag::DebugLuauDeferredConstraintResolution)
@ -250,4 +276,287 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "type_families_inhabited_with_normalization")
LUAU_REQUIRE_NO_ERRORS(result);
}
TEST_CASE_FIXTURE(BuiltinsFixture, "keyof_type_family_works")
{
if (!FFlag::DebugLuauDeferredConstraintResolution)
return;
CheckResult result = check(R"(
type MyObject = { x: number, y: number, z: number }
type KeysOfMyObject = keyof<MyObject>
local function ok(idx: KeysOfMyObject): "x" | "y" | "z" return idx end
local function err(idx: KeysOfMyObject): "x" | "y" return idx end
)");
LUAU_REQUIRE_ERROR_COUNT(1, result);
TypePackMismatch* tpm = get<TypePackMismatch>(result.errors[0]);
REQUIRE(tpm);
CHECK_EQ("\"x\" | \"y\"", toString(tpm->wantedTp));
CHECK_EQ("\"x\" | \"y\" | \"z\"", toString(tpm->givenTp));
}
TEST_CASE_FIXTURE(BuiltinsFixture, "keyof_type_family_works_with_metatables")
{
if (!FFlag::DebugLuauDeferredConstraintResolution)
return;
CheckResult result = check(R"(
local metatable = { __index = {w = 1} }
local obj = setmetatable({x = 1, y = 2, z = 3}, metatable)
type MyObject = typeof(obj)
type KeysOfMyObject = keyof<MyObject>
local function ok(idx: KeysOfMyObject): "w" | "x" | "y" | "z" return idx end
local function err(idx: KeysOfMyObject): "x" | "y" | "z" return idx end
)");
LUAU_REQUIRE_ERROR_COUNT(1, result);
TypePackMismatch* tpm = get<TypePackMismatch>(result.errors[0]);
REQUIRE(tpm);
CHECK_EQ("\"x\" | \"y\" | \"z\"", toString(tpm->wantedTp));
CHECK_EQ("\"w\" | \"x\" | \"y\" | \"z\"", toString(tpm->givenTp));
}
TEST_CASE_FIXTURE(BuiltinsFixture, "keyof_type_family_errors_if_it_has_nontable_part")
{
if (!FFlag::DebugLuauDeferredConstraintResolution)
return;
CheckResult result = check(R"(
type MyObject = { x: number, y: number, z: number }
type KeysOfMyObject = keyof<MyObject | boolean>
local function err(idx: KeysOfMyObject): "x" | "y" | "z" return idx end
)");
// FIXME: we should actually only report the type family being uninhabited error at its first use, I think?
LUAU_REQUIRE_ERROR_COUNT(3, result);
CHECK(toString(result.errors[0]) == "Type family instance keyof<MyObject | boolean> is uninhabited");
CHECK(toString(result.errors[1]) == "Type family instance keyof<MyObject | boolean> is uninhabited");
CHECK(toString(result.errors[2]) == "Type family instance keyof<MyObject | boolean> is uninhabited");
}
TEST_CASE_FIXTURE(BuiltinsFixture, "keyof_type_family_errors_if_union_of_differing_tables")
{
if (!FFlag::DebugLuauDeferredConstraintResolution)
return;
CheckResult result = check(R"(
type MyObject = { x: number, y: number, z: number }
type MyOtherObject = { w: number, y: number, z: number }
type KeysOfMyObject = keyof<MyObject | MyOtherObject>
local function err(idx: KeysOfMyObject): "x" | "y" | "z" return idx end
)");
// FIXME: we should actually only report the type family being uninhabited error at its first use, I think?
LUAU_REQUIRE_ERROR_COUNT(3, result);
CHECK(toString(result.errors[0]) == "Type family instance keyof<MyObject | MyOtherObject> is uninhabited");
CHECK(toString(result.errors[1]) == "Type family instance keyof<MyObject | MyOtherObject> is uninhabited");
CHECK(toString(result.errors[2]) == "Type family instance keyof<MyObject | MyOtherObject> is uninhabited");
}
TEST_CASE_FIXTURE(BuiltinsFixture, "keyof_type_family_never_for_empty_table")
{
if (!FFlag::DebugLuauDeferredConstraintResolution)
return;
CheckResult result = check(R"(
type KeyofEmpty = keyof<{}>
local foo = ((nil :: any) :: KeyofEmpty)
)");
LUAU_REQUIRE_NO_ERRORS(result);
CHECK(toString(requireType("foo")) == "never");
}
TEST_CASE_FIXTURE(BuiltinsFixture, "rawkeyof_type_family_works")
{
if (!FFlag::DebugLuauDeferredConstraintResolution)
return;
CheckResult result = check(R"(
type MyObject = { x: number, y: number, z: number }
type KeysOfMyObject = rawkeyof<MyObject>
local function ok(idx: KeysOfMyObject): "x" | "y" | "z" return idx end
local function err(idx: KeysOfMyObject): "x" | "y" return idx end
)");
LUAU_REQUIRE_ERROR_COUNT(1, result);
TypePackMismatch* tpm = get<TypePackMismatch>(result.errors[0]);
REQUIRE(tpm);
CHECK_EQ("\"x\" | \"y\"", toString(tpm->wantedTp));
CHECK_EQ("\"x\" | \"y\" | \"z\"", toString(tpm->givenTp));
}
TEST_CASE_FIXTURE(BuiltinsFixture, "rawkeyof_type_family_ignores_metatables")
{
if (!FFlag::DebugLuauDeferredConstraintResolution)
return;
CheckResult result = check(R"(
local metatable = { __index = {w = 1} }
local obj = setmetatable({x = 1, y = 2, z = 3}, metatable)
type MyObject = typeof(obj)
type KeysOfMyObject = rawkeyof<MyObject>
local function ok(idx: KeysOfMyObject): "x" | "y" | "z" return idx end
local function err(idx: KeysOfMyObject): "x" | "y" return idx end
)");
LUAU_REQUIRE_ERROR_COUNT(1, result);
TypePackMismatch* tpm = get<TypePackMismatch>(result.errors[0]);
REQUIRE(tpm);
CHECK_EQ("\"x\" | \"y\"", toString(tpm->wantedTp));
CHECK_EQ("\"x\" | \"y\" | \"z\"", toString(tpm->givenTp));
}
TEST_CASE_FIXTURE(BuiltinsFixture, "rawkeyof_type_family_errors_if_it_has_nontable_part")
{
if (!FFlag::DebugLuauDeferredConstraintResolution)
return;
CheckResult result = check(R"(
type MyObject = { x: number, y: number, z: number }
type KeysOfMyObject = rawkeyof<MyObject | boolean>
local function err(idx: KeysOfMyObject): "x" | "y" | "z" return idx end
)");
// FIXME: we should actually only report the type family being uninhabited error at its first use, I think?
LUAU_REQUIRE_ERROR_COUNT(3, result);
CHECK(toString(result.errors[0]) == "Type family instance rawkeyof<MyObject | boolean> is uninhabited");
CHECK(toString(result.errors[1]) == "Type family instance rawkeyof<MyObject | boolean> is uninhabited");
CHECK(toString(result.errors[2]) == "Type family instance rawkeyof<MyObject | boolean> is uninhabited");
}
TEST_CASE_FIXTURE(BuiltinsFixture, "rawkeyof_type_family_errors_if_union_of_differing_tables")
{
if (!FFlag::DebugLuauDeferredConstraintResolution)
return;
CheckResult result = check(R"(
type MyObject = { x: number, y: number, z: number }
type MyOtherObject = { w: number, y: number, z: number }
type KeysOfMyObject = rawkeyof<MyObject | MyOtherObject>
local function err(idx: KeysOfMyObject): "x" | "y" | "z" return idx end
)");
// FIXME: we should actually only report the type family being uninhabited error at its first use, I think?
LUAU_REQUIRE_ERROR_COUNT(3, result);
CHECK(toString(result.errors[0]) == "Type family instance rawkeyof<MyObject | MyOtherObject> is uninhabited");
CHECK(toString(result.errors[1]) == "Type family instance rawkeyof<MyObject | MyOtherObject> is uninhabited");
CHECK(toString(result.errors[2]) == "Type family instance rawkeyof<MyObject | MyOtherObject> is uninhabited");
}
TEST_CASE_FIXTURE(BuiltinsFixture, "rawkeyof_type_family_never_for_empty_table")
{
if (!FFlag::DebugLuauDeferredConstraintResolution)
return;
CheckResult result = check(R"(
type RawkeyofEmpty = rawkeyof<{}>
local foo = ((nil :: any) :: RawkeyofEmpty)
)");
LUAU_REQUIRE_NO_ERRORS(result);
CHECK(toString(requireType("foo")) == "never");
}
TEST_CASE_FIXTURE(ClassFixture, "keyof_type_family_works_on_classes")
{
if (!FFlag::DebugLuauDeferredConstraintResolution)
return;
CheckResult result = check(R"(
type KeysOfMyObject = keyof<BaseClass>
local function ok(idx: KeysOfMyObject): "BaseMethod" | "BaseField" return idx end
local function err(idx: KeysOfMyObject): "BaseMethod" return idx end
)");
LUAU_REQUIRE_ERROR_COUNT(1, result);
TypePackMismatch* tpm = get<TypePackMismatch>(result.errors[0]);
REQUIRE(tpm);
CHECK_EQ("\"BaseMethod\"", toString(tpm->wantedTp));
CHECK_EQ("\"BaseField\" | \"BaseMethod\"", toString(tpm->givenTp));
}
TEST_CASE_FIXTURE(ClassFixture, "keyof_type_family_errors_if_it_has_nonclass_part")
{
if (!FFlag::DebugLuauDeferredConstraintResolution)
return;
CheckResult result = check(R"(
type KeysOfMyObject = keyof<BaseClass | boolean>
local function err(idx: KeysOfMyObject): "BaseMethod" | "BaseField" return idx end
)");
// FIXME: we should actually only report the type family being uninhabited error at its first use, I think?
LUAU_REQUIRE_ERROR_COUNT(3, result);
CHECK(toString(result.errors[0]) == "Type family instance keyof<BaseClass | boolean> is uninhabited");
CHECK(toString(result.errors[1]) == "Type family instance keyof<BaseClass | boolean> is uninhabited");
CHECK(toString(result.errors[2]) == "Type family instance keyof<BaseClass | boolean> is uninhabited");
}
TEST_CASE_FIXTURE(ClassFixture, "keyof_type_family_errors_if_union_of_differing_classes")
{
if (!FFlag::DebugLuauDeferredConstraintResolution)
return;
CheckResult result = check(R"(
type KeysOfMyObject = keyof<BaseClass | Vector2>
local function err(idx: KeysOfMyObject): "BaseMethod" | "BaseField" return idx end
)");
// FIXME: we should actually only report the type family being uninhabited error at its first use, I think?
LUAU_REQUIRE_ERROR_COUNT(3, result);
CHECK(toString(result.errors[0]) == "Type family instance keyof<BaseClass | Vector2> is uninhabited");
CHECK(toString(result.errors[1]) == "Type family instance keyof<BaseClass | Vector2> is uninhabited");
CHECK(toString(result.errors[2]) == "Type family instance keyof<BaseClass | Vector2> is uninhabited");
}
TEST_CASE_FIXTURE(BuiltinsFixture, "keyof_rfc_example")
{
if (!FFlag::DebugLuauDeferredConstraintResolution)
return;
CheckResult result = check(R"(
local animals = {
cat = { speak = function() print "meow" end },
dog = { speak = function() print "woof woof" end },
monkey = { speak = function() print "oo oo" end },
fox = { speak = function() print "gekk gekk" end }
}
type AnimalType = keyof<typeof(animals)>
function speakByType(animal: AnimalType)
animals[animal].speak()
end
speakByType("dog") -- ok
speakByType("cactus") -- errors
)");
LUAU_REQUIRE_ERROR_COUNT(1, result);
TypePackMismatch* tpm = get<TypePackMismatch>(result.errors[0]);
REQUIRE(tpm);
CHECK_EQ("\"cat\" | \"dog\" | \"monkey\" | \"fox\"", toString(tpm->wantedTp));
CHECK_EQ("\"cactus\"", toString(tpm->givenTp));
}
TEST_SUITE_END();

View File

@ -9,6 +9,7 @@ using namespace Luau;
LUAU_FASTFLAG(DebugLuauDeferredConstraintResolution)
LUAU_FASTFLAG(DebugLuauSharedSelf);
LUAU_FASTFLAG(LuauForbidAliasNamedTypeof);
TEST_SUITE_BEGIN("TypeAliases");
@ -159,14 +160,16 @@ TEST_CASE_FIXTURE(Fixture, "cyclic_types_of_named_table_fields_do_not_expand_whe
CheckResult result = check(R"(
--!strict
type Node = { Parent: Node?; }
local node: Node;
node.Parent = 1
function f(node: Node)
node.Parent = 1
end
)");
LUAU_REQUIRE_ERROR_COUNT(1, result);
TypeMismatch* tm = get<TypeMismatch>(result.errors[0]);
REQUIRE(tm);
REQUIRE_MESSAGE(tm, result.errors[0]);
CHECK_EQ("Node?", toString(tm->wantedType));
CHECK_EQ(builtinTypes->numberType, tm->givenType);
}
@ -361,15 +364,19 @@ TEST_CASE_FIXTURE(Fixture, "corecursive_types_generic")
const std::string code = R"(
type A<T> = {v:T, b:B<T>}
type B<T> = {v:T, a:A<T>}
local aa:A<number>
local bb = aa
function f(a: A<number>)
return a
end
)";
const std::string expected = R"(
type A<T> = {v:T, b:B<T>}
type B<T> = {v:T, a:A<T>}
local aa:A<number>
local bb:A<number>=aa
function f(a: A<number>): A<number>
return a
end
)";
CHECK_EQ(expected, decorateWithTypes(code));
@ -383,14 +390,12 @@ TEST_CASE_FIXTURE(Fixture, "corecursive_function_types")
CheckResult result = check(R"(
type A = () -> (number, B)
type B = () -> (string, A)
local a: A
local b: B
)");
LUAU_REQUIRE_NO_ERRORS(result);
CHECK_EQ("t1 where t1 = () -> (number, () -> (string, t1))", toString(requireType("a")));
CHECK_EQ("t1 where t1 = () -> (string, () -> (number, t1))", toString(requireType("b")));
CHECK_EQ("t1 where t1 = () -> (number, () -> (string, t1))", toString(requireTypeAlias("A")));
CHECK_EQ("t1 where t1 = () -> (string, () -> (number, t1))", toString(requireTypeAlias("B")));
}
TEST_CASE_FIXTURE(Fixture, "generic_param_remap")
@ -1057,4 +1062,17 @@ TEST_CASE_FIXTURE(Fixture, "table_types_record_the_property_locations")
CHECK_EQ(propIt->second.typeLocation, Location({2, 12}, {2, 18}));
}
TEST_CASE_FIXTURE(Fixture, "typeof_is_not_a_valid_alias_name")
{
ScopedFastFlag sff{FFlag::LuauForbidAliasNamedTypeof, true};
CheckResult result = check(R"(
type typeof = number
)");
LUAU_REQUIRE_ERROR_COUNT(1, result);
CHECK("Type aliases cannot be named typeof" == toString(result.errors[0]));
}
TEST_SUITE_END();

View File

@ -10,6 +10,7 @@ using namespace Luau;
LUAU_FASTFLAG(DebugLuauDeferredConstraintResolution);
LUAU_FASTFLAG(LuauAlwaysCommitInferencesOfFunctionCalls);
LUAU_FASTFLAG(LuauSetMetatableOnUnionsOfTables);
TEST_SUITE_BEGIN("BuiltinTests");
@ -368,6 +369,29 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "setmetatable_unpacks_arg_types_correctly")
LUAU_REQUIRE_NO_ERRORS(result);
}
TEST_CASE_FIXTURE(BuiltinsFixture, "setmetatable_on_union_of_tables")
{
ScopedFastFlag sff{FFlag::LuauSetMetatableOnUnionsOfTables, true};
CheckResult result = check(R"(
type A = {tag: "A", x: number}
type B = {tag: "B", y: string}
type T = A | B
type X = typeof(
setmetatable({} :: T, {})
)
)");
LUAU_REQUIRE_NO_ERRORS(result);
if (FFlag::DebugLuauDeferredConstraintResolution)
CHECK("{ @metatable {| |}, A } | { @metatable {| |}, B }" == toString(requireTypeAlias("X")));
else
CHECK("{ @metatable { }, A } | { @metatable { }, B }" == toString(requireTypeAlias("X")));
}
TEST_CASE_FIXTURE(BuiltinsFixture, "table_insert_correctly_infers_type_of_array_2_args_overload")
{
CheckResult result = check(R"(
@ -796,16 +820,17 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "string_format_use_correct_argument3")
TEST_CASE_FIXTURE(BuiltinsFixture, "debug_traceback_is_crazy")
{
CheckResult result = check(R"(
local co: thread = ...
-- debug.traceback takes thread?, message?, level? - yes, all optional!
debug.traceback()
debug.traceback(nil, 1)
debug.traceback("msg")
debug.traceback("msg", 1)
debug.traceback(co)
debug.traceback(co, "msg")
debug.traceback(co, "msg", 1)
)");
function f(co: thread)
-- debug.traceback takes thread?, message?, level? - yes, all optional!
debug.traceback()
debug.traceback(nil, 1)
debug.traceback("msg")
debug.traceback("msg", 1)
debug.traceback(co)
debug.traceback(co, "msg")
debug.traceback(co, "msg", 1)
end
)");
LUAU_REQUIRE_NO_ERRORS(result);
}
@ -813,13 +838,13 @@ debug.traceback(co, "msg", 1)
TEST_CASE_FIXTURE(BuiltinsFixture, "debug_info_is_crazy")
{
CheckResult result = check(R"(
local co: thread, f: ()->() = ...
-- debug.info takes thread?, level, options or function, options
debug.info(1, "n")
debug.info(co, 1, "n")
debug.info(f, "n")
)");
function f(co: thread, f: () -> ())
-- debug.info takes thread?, level, options or function, options
debug.info(1, "n")
debug.info(co, 1, "n")
debug.info(f, "n")
end
)");
LUAU_REQUIRE_NO_ERRORS(result);
}

View File

@ -7,8 +7,6 @@
#include "doctest.h"
LUAU_FASTFLAG(LuauDefinitionFileSetModuleName)
using namespace Luau;
TEST_SUITE_BEGIN("DefinitionTests");
@ -453,8 +451,6 @@ TEST_CASE_FIXTURE(Fixture, "class_definitions_reference_other_classes")
TEST_CASE_FIXTURE(Fixture, "definition_file_has_source_module_name_set")
{
ScopedFastFlag sff{FFlag::LuauDefinitionFileSetModuleName, true};
LoadDefinitionFileResult result = loadDefinition(R"(
declare class Foo
end

View File

@ -1286,7 +1286,7 @@ TEST_CASE_FIXTURE(Fixture, "variadic_any_is_compatible_with_a_generic_TypePack")
LUAU_REQUIRE_NO_ERRORS(result);
}
// https://github.com/Roblox/luau/issues/767
// https://github.com/luau-lang/luau/issues/767
TEST_CASE_FIXTURE(BuiltinsFixture, "variadic_any_is_compatible_with_a_generic_TypePack_2")
{
CheckResult result = check(R"(

View File

@ -17,14 +17,15 @@ TEST_CASE_FIXTURE(Fixture, "select_correct_union_fn")
CheckResult result = check(R"(
type A = (number) -> (string)
type B = (string) -> (number)
local f:A & B
local b = f(10) -- b is a string
local c = f("a") -- c is a number
local function foo(f: A & B)
return f(10), f("a")
end
)");
LUAU_REQUIRE_NO_ERRORS(result);
CHECK_EQ(requireType("b"), builtinTypes->stringType);
CHECK_EQ(requireType("c"), builtinTypes->numberType);
CHECK_EQ("(((number) -> string) & ((string) -> number)) -> (string, number)", toString(requireType("foo")));
}
TEST_CASE_FIXTURE(Fixture, "table_combines")
@ -32,6 +33,7 @@ TEST_CASE_FIXTURE(Fixture, "table_combines")
CheckResult result = check(R"(
type A={a:number}
type B={b:string}
local c:A & B = {a=10, b="s"}
)");
@ -43,6 +45,7 @@ TEST_CASE_FIXTURE(Fixture, "table_combines_missing")
CheckResult result = check(R"(
type A={a:number}
type B={b:string}
local c:A & B = {a=10}
)");
@ -63,8 +66,10 @@ TEST_CASE_FIXTURE(Fixture, "table_extra_ok")
CheckResult result = check(R"(
type A={a:number}
type B={b:string}
local c:A & B
local d:A = c
local function f(t: A & B): A
return t
end
)");
LUAU_REQUIRE_NO_ERRORS(result);
@ -76,9 +81,10 @@ TEST_CASE_FIXTURE(Fixture, "fx_intersection_as_argument")
type A = (number) -> (string)
type B = (string) -> (number)
type C = (A) -> (number)
local f:A & B
local g:C
local b = g(f)
local function foo(f: A & B, g: C)
return g(f)
end
)");
LUAU_REQUIRE_NO_ERRORS(result);
@ -90,9 +96,10 @@ TEST_CASE_FIXTURE(Fixture, "fx_union_as_argument_fails")
type A = (number) -> (string)
type B = (string) -> (number)
type C = (A) -> (number)
local f:A | B
local g:C
local b = g(f)
local function foo(f: A | B, g: C)
return g(f)
end
)");
REQUIRE(!result.errors.empty());
@ -102,10 +109,11 @@ TEST_CASE_FIXTURE(Fixture, "argument_is_intersection")
{
CheckResult result = check(R"(
type A = (number | boolean) -> number
local f: A
f(5)
f(true)
local function foo(f: A)
f(5)
f(true)
end
)");
LUAU_REQUIRE_NO_ERRORS(result);
@ -114,21 +122,17 @@ TEST_CASE_FIXTURE(Fixture, "argument_is_intersection")
TEST_CASE_FIXTURE(Fixture, "should_still_pick_an_overload_whose_arguments_are_unions")
{
CheckResult result = check(R"(
type A = (number | boolean) -> number
type B = (string | nil) -> string
local f: A & B
type A = (number) -> string
type B = (string) -> number
local a1, a2 = f(1), f(true)
local b1, b2 = f("foo"), f(nil)
local function foo(f: A & B)
return f(1), f("five")
end
)");
LUAU_REQUIRE_NO_ERRORS(result);
CHECK_EQ(*requireType("a1"), *builtinTypes->numberType);
CHECK_EQ(*requireType("a2"), *builtinTypes->numberType);
CHECK_EQ(*requireType("b1"), *builtinTypes->stringType);
CHECK_EQ(*requireType("b2"), *builtinTypes->stringType);
CHECK_EQ("(((number) -> string) & ((string) -> number)) -> (string, number)", toString(requireType("foo")));
}
TEST_CASE_FIXTURE(Fixture, "propagates_name")
@ -137,16 +141,18 @@ TEST_CASE_FIXTURE(Fixture, "propagates_name")
type A={a:number}
type B={b:string}
local c:A&B
local b = c
local function f(t: A & B)
return t
end
)";
const std::string expected = R"(
type A={a:number}
type B={b:string}
local c:A&B
local b:A&B=c
local function f(t: A & B): A&B
return t
end
)";
CHECK_EQ(expected, decorateWithTypes(code));
@ -157,16 +163,18 @@ TEST_CASE_FIXTURE(Fixture, "index_on_an_intersection_type_with_property_guarante
CheckResult result = check(R"(
type A = {x: {y: number}}
type B = {x: {y: number}}
local t: A & B
local r = t.x
local function f(t: A & B)
return t.x
end
)");
LUAU_REQUIRE_NO_ERRORS(result);
if (FFlag::DebugLuauDeferredConstraintResolution)
CHECK("{ y: number }" == toString(requireType("r")));
CHECK("(A & B) -> { y: number }" == toString(requireType("f")));
else
CHECK("{| y: number |} & {| y: number |}" == toString(requireType("r")));
CHECK("(A & B) -> {| y: number |} & {| y: number |}" == toString(requireType("f")));
}
TEST_CASE_FIXTURE(Fixture, "index_on_an_intersection_type_works_at_arbitrary_depth")
@ -174,21 +182,18 @@ TEST_CASE_FIXTURE(Fixture, "index_on_an_intersection_type_works_at_arbitrary_dep
CheckResult result = check(R"(
type A = {x: {y: {z: {thing: string}}}}
type B = {x: {y: {z: {thing: string}}}}
local t: A & B
local r = t.x.y.z.thing
local function f(t: A & B)
return t.x.y.z.thing
end
)");
LUAU_REQUIRE_NO_ERRORS(result);
if (FFlag::DebugLuauDeferredConstraintResolution)
{
CHECK_EQ("string", toString(requireType("r")));
}
CHECK_EQ("(A & B) -> string", toString(requireType("f")));
else
{
CHECK_EQ("string & string", toString(requireType("r")));
}
CHECK_EQ("(A & B) -> string & string", toString(requireType("f")));
}
TEST_CASE_FIXTURE(Fixture, "index_on_an_intersection_type_with_mixed_types")
@ -196,16 +201,18 @@ TEST_CASE_FIXTURE(Fixture, "index_on_an_intersection_type_with_mixed_types")
CheckResult result = check(R"(
type A = {x: number}
type B = {x: string}
local t: A & B
local r = t.x
local function f(t: A & B)
return t.x
end
)");
LUAU_REQUIRE_NO_ERRORS(result);
if (FFlag::DebugLuauDeferredConstraintResolution)
CHECK_EQ("never", toString(requireType("r")));
CHECK_EQ("(A & B) -> never", toString(requireType("f")));
else
CHECK_EQ("number & string", toString(requireType("r")));
CHECK_EQ("(A & B) -> number & string", toString(requireType("f")));
}
TEST_CASE_FIXTURE(Fixture, "index_on_an_intersection_type_with_one_part_missing_the_property")
@ -213,13 +220,14 @@ TEST_CASE_FIXTURE(Fixture, "index_on_an_intersection_type_with_one_part_missing_
CheckResult result = check(R"(
type A = {x: number}
type B = {}
local t: A & B
local r = t.x
local function f(t: A & B)
return t.x
end
)");
LUAU_REQUIRE_NO_ERRORS(result);
CHECK_EQ("number", toString(requireType("r")));
CHECK_EQ("(A & B) -> number", toString(requireType("f")));
}
TEST_CASE_FIXTURE(Fixture, "index_on_an_intersection_type_with_one_property_of_type_any")
@ -227,13 +235,14 @@ TEST_CASE_FIXTURE(Fixture, "index_on_an_intersection_type_with_one_property_of_t
CheckResult result = check(R"(
type A = {y: number}
type B = {x: any}
local t: A & B
local r = t.x
local function f(t: A & B)
return t.x
end
)");
LUAU_REQUIRE_NO_ERRORS(result);
CHECK_EQ(*builtinTypes->anyType, *requireType("r"));
CHECK_EQ("(A & B) -> any", toString(requireType("f")));
}
TEST_CASE_FIXTURE(Fixture, "index_on_an_intersection_type_with_all_parts_missing_the_property")
@ -260,8 +269,9 @@ TEST_CASE_FIXTURE(Fixture, "table_intersection_write")
type X = { x: number }
type XY = X & { y: number }
local a : XY = { x = 1, y = 2 }
a.x = 10
function f(t: XY)
t.x = 10
end
)");
LUAU_REQUIRE_NO_ERRORS(result);
@ -270,8 +280,9 @@ TEST_CASE_FIXTURE(Fixture, "table_intersection_write")
type X = {}
type XY = X & { x: number, y: number }
local a : XY = { x = 1, y = 2 }
a.x = 10
function f(t: XY)
t.x = 10
end
)");
LUAU_REQUIRE_NO_ERRORS(result);
@ -281,8 +292,9 @@ TEST_CASE_FIXTURE(Fixture, "table_intersection_write")
type Y = { y: number }
type XY = X & Y
local a : XY = { x = 1, y = 2 }
a.x = 10
function f(t: XY)
t.x = 10
end
)");
LUAU_REQUIRE_NO_ERRORS(result);
@ -290,10 +302,11 @@ TEST_CASE_FIXTURE(Fixture, "table_intersection_write")
result = check(R"(
type A = { x: {y: number} }
type B = { x: {y: number} }
local t : A & B = { x = { y = 1 } }
t.x = { y = 4 }
t.x.y = 40
function f(t: A & B)
t.x = { y = 4 }
t.x.y = 40
end
)");
LUAU_REQUIRE_NO_ERRORS(result);
@ -306,8 +319,9 @@ TEST_CASE_FIXTURE(Fixture, "table_intersection_write_sealed")
type Y = { y: number }
type XY = X & Y
local a : XY = { x = 1, y = 2 }
a.z = 10
function f(t: XY)
t.z = 10
end
)");
LUAU_REQUIRE_ERROR_COUNT(1, result);
@ -323,26 +337,33 @@ TEST_CASE_FIXTURE(Fixture, "table_intersection_write_sealed_indirect")
type XY = X & Y
local xy : XY = {
x = function(a: number) return -a end,
y = function(a: string) return a .. "b" end
}
function xy.z(a:number) return a * 10 end
function xy:y(a:number) return a * 10 end
function xy:w(a:number) return a * 10 end
function f(t: XY)
function t.z(a:number) return a * 10 end
function t:y(a:number) return a * 10 end
function t:w(a:number) return a * 10 end
end
)");
LUAU_REQUIRE_ERROR_COUNT(4, result);
const std::string expected = R"(Type
if (FFlag::DebugLuauDeferredConstraintResolution)
{
LUAU_REQUIRE_ERROR_COUNT(2, result);
CHECK_EQ(toString(result.errors[0]), "Cannot add property 'z' to table 'X & Y'");
CHECK_EQ(toString(result.errors[1]), "Cannot add property 'w' to table 'X & Y'");
}
else
{
LUAU_REQUIRE_ERROR_COUNT(4, result);
const std::string expected = R"(Type
'(string, number) -> string'
could not be converted into
'(string) -> string'
caused by:
Argument count mismatch. Function expects 2 arguments, but only 1 is specified)";
CHECK_EQ(expected, toString(result.errors[0]));
CHECK_EQ(toString(result.errors[1]), "Cannot add property 'z' to table 'X & Y'");
CHECK_EQ(toString(result.errors[2]), "Type 'number' could not be converted into 'string'");
CHECK_EQ(toString(result.errors[3]), "Cannot add property 'w' to table 'X & Y'");
CHECK_EQ(expected, toString(result.errors[0]));
CHECK_EQ(toString(result.errors[1]), "Cannot add property 'z' to table 'X & Y'");
CHECK_EQ(toString(result.errors[2]), "Type 'number' could not be converted into 'string'");
CHECK_EQ(toString(result.errors[3]), "Cannot add property 'w' to table 'X & Y'");
}
}
TEST_CASE_FIXTURE(Fixture, "table_write_sealed_indirect")
@ -377,8 +398,9 @@ caused by:
TEST_CASE_FIXTURE(BuiltinsFixture, "table_intersection_setmetatable")
{
CheckResult result = check(R"(
local t: {} & {}
setmetatable(t, {})
function f(t: {} & {})
setmetatable(t, {})
end
)");
LUAU_REQUIRE_NO_ERRORS(result);
@ -409,8 +431,10 @@ type X = { x: number }
type Y = { y: number }
type Z = { z: number }
type XYZ = X & Y & Z
local a: XYZ
local b: number = a
function f(a: XYZ): number
return a
end
)");
LUAU_REQUIRE_ERROR_COUNT(1, result);
@ -448,9 +472,10 @@ TEST_CASE_FIXTURE(Fixture, "no_stack_overflow_from_flattenintersection")
TEST_CASE_FIXTURE(Fixture, "intersect_bool_and_false")
{
CheckResult result = check(R"(
local x : (boolean & false)
local y : false = x -- OK
local z : true = x -- Not OK
function f(x: boolean & false)
local y : false = x -- OK
local z : true = x -- Not OK
end
)");
LUAU_REQUIRE_ERROR_COUNT(1, result);
@ -460,9 +485,10 @@ TEST_CASE_FIXTURE(Fixture, "intersect_bool_and_false")
TEST_CASE_FIXTURE(Fixture, "intersect_false_and_bool_and_false")
{
CheckResult result = check(R"(
local x : false & (boolean & false)
local y : false = x -- OK
local z : true = x -- Not OK
function f(x: false & (boolean & false))
local y : false = x -- OK
local z : true = x -- Not OK
end
)");
LUAU_REQUIRE_ERROR_COUNT(1, result);
@ -474,9 +500,10 @@ TEST_CASE_FIXTURE(Fixture, "intersect_false_and_bool_and_false")
TEST_CASE_FIXTURE(Fixture, "intersect_saturate_overloaded_functions")
{
CheckResult result = check(R"(
local x : ((number?) -> number?) & ((string?) -> string?)
local y : (nil) -> nil = x -- OK
local z : (number) -> number = x -- Not OK
function foo(x: ((number?) -> number?) & ((string?) -> string?))
local y : (nil) -> nil = x -- OK
local z : (number) -> number = x -- Not OK
end
)");
LUAU_REQUIRE_ERROR_COUNT(1, result);
@ -489,11 +516,11 @@ could not be converted into
TEST_CASE_FIXTURE(Fixture, "union_saturate_overloaded_functions")
{
CheckResult result = check(R"(
local x : ((number) -> number) & ((string) -> string)
local y : ((number | string) -> (number | string)) = x -- OK
local z : ((number | boolean) -> (number | boolean)) = x -- Not OK
function f(x: ((number) -> number) & ((string) -> string))
local y : ((number | string) -> (number | string)) = x -- OK
local z : ((number | boolean) -> (number | boolean)) = x -- Not OK
end
)");
LUAU_REQUIRE_ERROR_COUNT(1, result);
@ -507,9 +534,10 @@ could not be converted into
TEST_CASE_FIXTURE(Fixture, "intersection_of_tables")
{
CheckResult result = check(R"(
local x : { p : number?, q : string? } & { p : number?, q : number?, r : number? }
local y : { p : number?, q : nil, r : number? } = x -- OK
local z : { p : nil } = x -- Not OK
function f(x: { p : number?, q : string? } & { p : number?, q : number?, r : number? })
local y : { p : number?, q : nil, r : number? } = x -- OK
local z : { p : nil } = x -- Not OK
end
)");
LUAU_REQUIRE_ERROR_COUNT(1, result);
@ -529,9 +557,10 @@ could not be converted into
TEST_CASE_FIXTURE(Fixture, "intersection_of_tables_with_top_properties")
{
CheckResult result = check(R"(
local x : { p : number?, q : any } & { p : unknown, q : string? } = { p = 123, q = "foo" }
local y : { p : number?, q : string? } = x -- OK
local z : { p : string?, q : number? } = x -- Not OK
function f(x : { p : number?, q : any } & { p : unknown, q : string? })
local y : { p : number?, q : string? } = x -- OK
local z : { p : string?, q : number? } = x -- Not OK
end
)");
if (FFlag::DebugLuauDeferredConstraintResolution)
@ -570,9 +599,10 @@ could not be converted into
TEST_CASE_FIXTURE(Fixture, "intersection_of_tables_with_never_properties")
{
CheckResult result = check(R"(
local x : { p : number?, q : never } & { p : never, q : string? } -- OK
local y : { p : never, q : never } = x -- OK
local z : never = x -- OK
function f(x : { p : number?, q : never } & { p : never, q : string? })
local y : { p : never, q : never } = x -- OK
local z : never = x -- OK
end
)");
LUAU_REQUIRE_NO_ERRORS(result);
@ -581,9 +611,10 @@ TEST_CASE_FIXTURE(Fixture, "intersection_of_tables_with_never_properties")
TEST_CASE_FIXTURE(Fixture, "overloaded_functions_returning_intersections")
{
CheckResult result = check(R"(
local x : ((number?) -> ({ p : number } & { q : number })) & ((string?) -> ({ p : number } & { r : number }))
local y : (nil) -> { p : number, q : number, r : number} = x -- OK
local z : (number?) -> { p : number, q : number, r : number} = x -- Not OK
function f(x : ((number?) -> ({ p : number } & { q : number })) & ((string?) -> ({ p : number } & { r : number })))
local y : (nil) -> { p : number, q : number, r : number} = x -- OK
local z : (number?) -> { p : number, q : number, r : number} = x -- Not OK
end
)");
LUAU_REQUIRE_ERROR_COUNT(1, result);
@ -603,11 +634,12 @@ could not be converted into
TEST_CASE_FIXTURE(Fixture, "overloaded_functions_mentioning_generic")
{
CheckResult result = check(R"(
function f<a>()
local x : ((number?) -> (a | number)) & ((string?) -> (a | string))
local y : (nil) -> a = x -- OK
local z : (number?) -> a = x -- Not OK
end
function f<a>()
function g(x : ((number?) -> (a | number)) & ((string?) -> (a | string)))
local y : (nil) -> a = x -- OK
local z : (number?) -> a = x -- Not OK
end
end
)");
LUAU_REQUIRE_ERROR_COUNT(1, result);
@ -621,11 +653,12 @@ could not be converted into
TEST_CASE_FIXTURE(Fixture, "overloaded_functions_mentioning_generics")
{
CheckResult result = check(R"(
function f<a,b,c>()
local x : ((a?) -> (a | b)) & ((c?) -> (b | c))
local y : (nil) -> ((a & c) | b) = x -- OK
local z : (a?) -> ((a & c) | b) = x -- Not OK
end
function f<a,b,c>()
function g(x : ((a?) -> (a | b)) & ((c?) -> (b | c)))
local y : (nil) -> ((a & c) | b) = x -- OK
local z : (a?) -> ((a & c) | b) = x -- Not OK
end
end
)");
LUAU_REQUIRE_ERROR_COUNT(1, result);
@ -639,11 +672,12 @@ could not be converted into
TEST_CASE_FIXTURE(Fixture, "overloaded_functions_mentioning_generic_packs")
{
CheckResult result = check(R"(
function f<a...,b...>()
local x : ((number?, a...) -> (number?, b...)) & ((string?, a...) -> (string?, b...))
local y : ((nil, a...) -> (nil, b...)) = x -- OK
local z : ((nil, b...) -> (nil, a...)) = x -- Not OK
end
function f<a...,b...>()
function g(x : ((number?, a...) -> (number?, b...)) & ((string?, a...) -> (string?, b...)))
local y : ((nil, a...) -> (nil, b...)) = x -- OK
local z : ((nil, b...) -> (nil, a...)) = x -- Not OK
end
end
)");
LUAU_REQUIRE_ERROR_COUNT(1, result);
@ -657,11 +691,12 @@ could not be converted into
TEST_CASE_FIXTURE(Fixture, "overloadeded_functions_with_unknown_result")
{
CheckResult result = check(R"(
function f<a...,b...>()
local x : ((number) -> number) & ((nil) -> unknown)
local y : (number?) -> unknown = x -- OK
local z : (number?) -> number? = x -- Not OK
end
function f<a...,b...>()
function g(x : ((number) -> number) & ((nil) -> unknown))
local y : (number?) -> unknown = x -- OK
local z : (number?) -> number? = x -- Not OK
end
end
)");
LUAU_REQUIRE_ERROR_COUNT(1, result);
@ -675,11 +710,12 @@ could not be converted into
TEST_CASE_FIXTURE(Fixture, "overloadeded_functions_with_unknown_arguments")
{
CheckResult result = check(R"(
function f<a...,b...>()
local x : ((number) -> number?) & ((unknown) -> string?)
local y : (number) -> nil = x -- OK
local z : (number?) -> nil = x -- Not OK
end
function f<a...,b...>()
function g(x : ((number) -> number?) & ((unknown) -> string?))
local y : (number) -> nil = x -- OK
local z : (number?) -> nil = x -- Not OK
end
end
)");
LUAU_REQUIRE_ERROR_COUNT(1, result);
@ -693,11 +729,12 @@ could not be converted into
TEST_CASE_FIXTURE(Fixture, "overloadeded_functions_with_never_result")
{
CheckResult result = check(R"(
function f<a...,b...>()
local x : ((number) -> number) & ((nil) -> never)
local y : (number?) -> number = x -- OK
local z : (number?) -> never = x -- Not OK
end
function f<a...,b...>()
function g(x : ((number) -> number) & ((nil) -> never))
local y : (number?) -> number = x -- OK
local z : (number?) -> never = x -- Not OK
end
end
)");
LUAU_REQUIRE_ERROR_COUNT(1, result);
@ -711,11 +748,12 @@ could not be converted into
TEST_CASE_FIXTURE(Fixture, "overloadeded_functions_with_never_arguments")
{
CheckResult result = check(R"(
function f<a...,b...>()
local x : ((number) -> number?) & ((never) -> string?)
local y : (never) -> nil = x -- OK
local z : (number?) -> nil = x -- Not OK
end
function f<a...,b...>()
function g(x : ((number) -> number?) & ((never) -> string?))
local y : (never) -> nil = x -- OK
local z : (number?) -> nil = x -- Not OK
end
end
)");
LUAU_REQUIRE_ERROR_COUNT(1, result);
@ -729,9 +767,10 @@ could not be converted into
TEST_CASE_FIXTURE(Fixture, "overloadeded_functions_with_overlapping_results_and_variadics")
{
CheckResult result = check(R"(
local x : ((string?) -> (string | number)) & ((number?) -> ...number)
local y : ((nil) -> (number, number?)) = x -- OK
local z : ((string | number) -> (number, number?)) = x -- Not OK
function f(x : ((string?) -> (string | number)) & ((number?) -> ...number))
local y : ((nil) -> (number, number?)) = x -- OK
local z : ((string | number) -> (number, number?)) = x -- Not OK
end
)");
LUAU_REQUIRE_ERROR_COUNT(1, result);
@ -745,11 +784,12 @@ could not be converted into
TEST_CASE_FIXTURE(Fixture, "overloadeded_functions_with_weird_typepacks_1")
{
CheckResult result = check(R"(
function f<a...,b...>()
local x : (() -> a...) & (() -> b...)
local y : (() -> b...) & (() -> a...) = x -- OK
local z : () -> () = x -- Not OK
end
function f<a...,b...>()
function g(x : (() -> a...) & (() -> b...))
local y : (() -> b...) & (() -> a...) = x -- OK
local z : () -> () = x -- Not OK
end
end
)");
LUAU_REQUIRE_ERROR_COUNT(1, result);
@ -760,11 +800,12 @@ TEST_CASE_FIXTURE(Fixture, "overloadeded_functions_with_weird_typepacks_1")
TEST_CASE_FIXTURE(Fixture, "overloadeded_functions_with_weird_typepacks_2")
{
CheckResult result = check(R"(
function f<a...,b...>()
local x : ((a...) -> ()) & ((b...) -> ())
local y : ((b...) -> ()) & ((a...) -> ()) = x -- OK
local z : () -> () = x -- Not OK
end
function f<a...,b...>()
function g(x : ((a...) -> ()) & ((b...) -> ()))
local y : ((b...) -> ()) & ((a...) -> ()) = x -- OK
local z : () -> () = x -- Not OK
end
end
)");
LUAU_REQUIRE_ERROR_COUNT(1, result);
@ -775,11 +816,12 @@ TEST_CASE_FIXTURE(Fixture, "overloadeded_functions_with_weird_typepacks_2")
TEST_CASE_FIXTURE(Fixture, "overloadeded_functions_with_weird_typepacks_3")
{
CheckResult result = check(R"(
function f<a...>()
local x : (() -> a...) & (() -> (number?,a...))
local y : (() -> (number?,a...)) & (() -> a...) = x -- OK
local z : () -> (number) = x -- Not OK
end
function f<a...>()
function g(x : (() -> a...) & (() -> (number?,a...)))
local y : (() -> (number?,a...)) & (() -> a...) = x -- OK
local z : () -> (number) = x -- Not OK
end
end
)");
LUAU_REQUIRE_ERROR_COUNT(1, result);
@ -793,11 +835,12 @@ could not be converted into
TEST_CASE_FIXTURE(Fixture, "overloadeded_functions_with_weird_typepacks_4")
{
CheckResult result = check(R"(
function f<a...>()
local x : ((a...) -> ()) & ((number,a...) -> number)
local y : ((number,a...) -> number) & ((a...) -> ()) = x -- OK
local z : (number?) -> () = x -- Not OK
end
function f<a...>()
function g(x : ((a...) -> ()) & ((number,a...) -> number))
local y : ((number,a...) -> number) & ((a...) -> ()) = x -- OK
local z : (number?) -> () = x -- Not OK
end
end
)");
LUAU_REQUIRE_ERROR_COUNT(1, result);
@ -810,61 +853,89 @@ could not be converted into
TEST_CASE_FIXTURE(BuiltinsFixture, "intersect_metatables")
{
CheckResult result = check(R"(
local a : string? = nil
local b : number? = nil
if (FFlag::DebugLuauDeferredConstraintResolution)
{
CheckResult result = check(R"(
function f(a: string?, b: string?)
local x = setmetatable({}, { p = 5, q = a })
local y = setmetatable({}, { q = b, r = "hi" })
local z = setmetatable({}, { p = 5, q = nil, r = "hi" })
local x = setmetatable({}, { p = 5, q = a });
local y = setmetatable({}, { q = b, r = "hi" });
local z = setmetatable({}, { p = 5, q = nil, r = "hi" });
type X = typeof(x)
type Y = typeof(y)
type Z = typeof(z)
type X = typeof(x)
type Y = typeof(y)
type Z = typeof(z)
function g(xy: X&Y, yx: Y&X): (Z, Z)
return xy, yx
end
local xy : X&Y = z;
local yx : Y&X = z;
z = xy;
z = yx;
)");
g(z, z)
end
)");
LUAU_REQUIRE_NO_ERRORS(result);
LUAU_REQUIRE_NO_ERRORS(result);
}
else
{
CheckResult result = check(R"(
local a : string? = nil
local b : number? = nil
local x = setmetatable({}, { p = 5, q = a });
local y = setmetatable({}, { q = b, r = "hi" });
local z = setmetatable({}, { p = 5, q = nil, r = "hi" });
type X = typeof(x)
type Y = typeof(y)
type Z = typeof(z)
local xy : X&Y = z;
local yx : Y&X = z;
z = xy;
z = yx;
)");
LUAU_REQUIRE_NO_ERRORS(result);
}
}
TEST_CASE_FIXTURE(BuiltinsFixture, "intersect_metatable_subtypes")
{
CheckResult result = check(R"(
local x = setmetatable({ a = 5 }, { p = 5 });
local y = setmetatable({ b = "hi" }, { p = 5, q = "hi" });
local z = setmetatable({ a = 5, b = "hi" }, { p = 5, q = "hi" });
local x = setmetatable({ a = 5 }, { p = 5 })
local y = setmetatable({ b = "hi" }, { p = 5, q = "hi" })
local z = setmetatable({ a = 5, b = "hi" }, { p = 5, q = "hi" })
type X = typeof(x)
type Y = typeof(y)
type Z = typeof(z)
local xy : X&Y = z;
local yx : Y&X = z;
z = xy;
z = yx;
function f(xy: X&Y, yx: Y&X): (Z, Z)
return xy, yx
end
f(z, z)
)");
LUAU_REQUIRE_NO_ERRORS(result);
}
TEST_CASE_FIXTURE(BuiltinsFixture, "intersect_metatables_with_properties")
{
CheckResult result = check(R"(
local x = setmetatable({ a = 5 }, { p = 5 });
local y = setmetatable({ b = "hi" }, { q = "hi" });
local z = setmetatable({ a = 5, b = "hi" }, { p = 5, q = "hi" });
local x = setmetatable({ a = 5 }, { p = 5 })
local y = setmetatable({ b = "hi" }, { q = "hi" })
local z = setmetatable({ a = 5, b = "hi" }, { p = 5, q = "hi" })
type X = typeof(x)
type Y = typeof(y)
type Z = typeof(z)
local xy : X&Y = z;
z = xy;
function f(xy: X&Y): Z
return xy
end
f(z)
)");
LUAU_REQUIRE_NO_ERRORS(result);
@ -872,22 +943,44 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "intersect_metatables_with_properties")
TEST_CASE_FIXTURE(BuiltinsFixture, "intersect_metatable_with_table")
{
CheckResult result = check(R"(
local x = setmetatable({ a = 5 }, { p = 5 });
local z = setmetatable({ a = 5, b = "hi" }, { p = 5 });
if (FFlag::DebugLuauDeferredConstraintResolution)
{
CheckResult result = check(R"(
local x = setmetatable({ a = 5 }, { p = 5 })
local z = setmetatable({ a = 5, b = "hi" }, { p = 5 })
type X = typeof(x)
type Y = { b : string }
type Z = typeof(z)
type X = typeof(x)
type Y = { b : string }
type Z = typeof(z)
-- TODO: once we have shape types, we should be able to initialize these with z
local xy : X&Y;
local yx : Y&X;
z = xy;
z = yx;
)");
function f(xy: X&Y, yx: Y&X): (Z, Z)
return xy, yx
end
LUAU_REQUIRE_NO_ERRORS(result);
f(z, z)
)");
LUAU_REQUIRE_NO_ERRORS(result);
}
else
{
CheckResult result = check(R"(
local x = setmetatable({ a = 5 }, { p = 5 });
local z = setmetatable({ a = 5, b = "hi" }, { p = 5 });
type X = typeof(x)
type Y = { b : string }
type Z = typeof(z)
-- TODO: once we have shape types, we should be able to initialize these with z
local xy : X&Y;
local yx : Y&X;
z = xy;
z = yx;
)");
LUAU_REQUIRE_NO_ERRORS(result);
}
}
TEST_CASE_FIXTURE(Fixture, "CLI-44817")
@ -900,11 +993,11 @@ TEST_CASE_FIXTURE(Fixture, "CLI-44817")
type XY = {x: number, y: number}
type XYZ = {x:number, y: number, z: number}
local xy: XY = {x = 0, y = 0}
local xyz: XYZ = {x = 0, y = 0, z = 0}
function f(xy: XY, xyz: XYZ): (X&Y, X&Y&Z)
return xy, xyz
end
local xNy: X&Y = xy
local xNyNz: X&Y&Z = xyz
local xNy, xNyNz = f({x = 0, y = 0}, {x = 0, y = 0, z = 0})
local t1: XY = xNy -- Type 'X & Y' could not be converted into 'XY'
local t2: XY = xNyNz -- Type 'X & Y & Z' could not be converted into 'XY'
@ -955,8 +1048,9 @@ type Foo = {
Bar: string,
} & { Baz: number }
local x: Foo = { Bar = "1", Baz = 2 }
local y = x.Bar
function f(x: Foo)
return x.Bar
end
)");
LUAU_REQUIRE_NO_ERRORS(result);
@ -969,8 +1063,9 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "index_property_table_intersection_2")
Bar: string,
} & { Baz: number }
local x: Foo = { Bar = "1", Baz = 2 }
local y = x["Bar"]
function f(x: Foo)
return x["Bar"]
end
)");
LUAU_REQUIRE_NO_ERRORS(result);
@ -990,14 +1085,13 @@ TEST_CASE_FIXTURE(Fixture, "cli_80596_simplify_degenerate_intersections")
}
type C = A & B
local obj: C = {
x = 3,
}
local x: number = obj.x or 3
function f(obj: C): number
return obj.x or 3
end
)");
LUAU_REQUIRE_ERRORS(result);
LUAU_REQUIRE_NO_ERRORS(result);
}
TEST_CASE_FIXTURE(Fixture, "cli_80596_simplify_more_realistic_intersections")
@ -1016,14 +1110,13 @@ TEST_CASE_FIXTURE(Fixture, "cli_80596_simplify_more_realistic_intersections")
}
type C = A & B
local obj: C = {
x = 3,
}
local x: number = obj.x or 3
function f(obj: C): number
return obj.x or 3
end
)");
LUAU_REQUIRE_ERRORS(result);
LUAU_REQUIRE_NO_ERRORS(result);
}
TEST_SUITE_END();

View File

@ -365,9 +365,9 @@ TEST_CASE_FIXTURE(Fixture, "parametric_tagged_union_alias")
LUAU_REQUIRE_ERROR_COUNT(1, result);
// FIXME: This could be improved by expanding the contents of `a`
const std::string expectedError =
"Type 'a' could not be converted into 'Err<number> | Ok<string>'; type a (a) is not a subtype of Err<number> | Ok<string>[1] (Err<number>)\n"
"\ttype a[\"success\"] (false) is not exactly Err<number> | Ok<string>[0][\"success\"] (true)";
"Type 'a' could not be converted into 'Err<number> | Ok<string>'";
CHECK(toString(result.errors[0]) == expectedError);
}

View File

@ -250,7 +250,14 @@ TEST_CASE_FIXTURE(Fixture, "tc_member_function_2")
TEST_CASE_FIXTURE(Fixture, "call_method")
{
CheckResult result = check("local T = {} T.x = 0 function T:method() return self.x end local a = T:method()");
CheckResult result = check(R"(
local T = {}
T.x = 0
function T:method()
return self.x
end
local a = T:method()
)");
LUAU_REQUIRE_NO_ERRORS(result);
CHECK_EQ(*builtinTypes->numberType, *requireType("a"));
@ -258,7 +265,17 @@ TEST_CASE_FIXTURE(Fixture, "call_method")
TEST_CASE_FIXTURE(Fixture, "call_method_with_explicit_self_argument")
{
CheckResult result = check("local T = {} T.x = 0 function T:method() return self.x end local a = T.method(T)");
CheckResult result = check(R"(
local T = {}
T.x = 0
function T:method()
return self.x
end
local a = T.method(T)
)");
LUAU_REQUIRE_NO_ERRORS(result);
}
@ -843,10 +860,9 @@ TEST_CASE_FIXTURE(Fixture, "array_factory_function")
TEST_CASE_FIXTURE(Fixture, "sealed_table_indexers_must_unify")
{
CheckResult result = check(R"(
local A = { 5, 7, 8 }
local B = { "one", "two", "three" }
B = A
function f(a: {number}): {string}
return a
end
)");
LUAU_REQUIRE_ERROR_COUNT(1, result);
@ -857,10 +873,9 @@ TEST_CASE_FIXTURE(Fixture, "sealed_table_indexers_must_unify")
TEST_CASE_FIXTURE(Fixture, "indexer_on_sealed_table_must_unify_with_free_table")
{
CheckResult result = check(R"(
local A = { 1, 2, 3 }
function F(t)
function F(t): {number}
t[4] = "hi"
A = t
return t
end
)");
@ -870,8 +885,11 @@ TEST_CASE_FIXTURE(Fixture, "indexer_on_sealed_table_must_unify_with_free_table")
TEST_CASE_FIXTURE(Fixture, "infer_type_when_indexing_from_a_table_indexer")
{
CheckResult result = check(R"(
local t: { [number]: string }
local s = t[1]
function f(t: {string})
return t[1]
end
local s = f({})
)");
LUAU_REQUIRE_NO_ERRORS(result);
@ -879,10 +897,15 @@ TEST_CASE_FIXTURE(Fixture, "infer_type_when_indexing_from_a_table_indexer")
CHECK_EQ(*builtinTypes->stringType, *requireType("s"));
}
TEST_CASE_FIXTURE(Fixture, "indexing_from_a_table_should_prefer_properties_when_possible")
TEST_CASE_FIXTURE(BuiltinsFixture, "indexing_from_a_table_should_prefer_properties_when_possible")
{
CheckResult result = check(R"(
local t: { a: string, [string]: number }
function f(): { a: string, [string]: number }
error("e")
end
local t = f()
local a1 = t.a
local a2 = t["a"]
@ -1143,7 +1166,7 @@ TEST_CASE_FIXTURE(Fixture, "user_defined_table_types_are_named")
CheckResult result = check(R"(
type Vector3 = {x: number, y: number}
local v: Vector3
local v: Vector3 = {x = 5, y = 7}
)");
LUAU_REQUIRE_NO_ERRORS(result);
@ -1185,8 +1208,11 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "result_is_always_any_if_lhs_is_any")
TEST_CASE_FIXTURE(Fixture, "result_is_bool_for_equality_operators_if_lhs_is_any")
{
CheckResult result = check(R"(
local a: any
local b: number
function f(): (any, number)
return 5, 7
end
local a: any, b: number = f()
local c = a < b
)");
@ -1290,13 +1316,14 @@ TEST_CASE_FIXTURE(Fixture, "pass_incompatible_union_to_a_generic_table_without_c
CheckResult result = check(R"(
-- must be in this specific order, and with (roughly) those exact properties!
type A = {x: number, [any]: any} | {}
local a: A
function f(t)
t.y = 1
end
f(a)
function g(a: A)
f(a)
end
)");
LUAU_REQUIRE_ERROR_COUNT(1, result);
@ -1313,7 +1340,9 @@ TEST_CASE_FIXTURE(Fixture, "passing_compatible_unions_to_a_generic_table_without
t.y = 1
end
f({y = 5} :: A)
function g(a: A)
f(a)
end
)");
LUAU_REQUIRE_NO_ERRORS(result);
@ -1532,10 +1561,9 @@ TEST_CASE_FIXTURE(Fixture, "right_table_missing_key")
TEST_CASE_FIXTURE(Fixture, "right_table_missing_key2")
{
CheckResult result = check(R"(
local lt: { [string]: string, a: string }
local rt: {}
lt = rt
function f(t: {}): { [string]: string, a: string }
return t
end
)");
LUAU_REQUIRE_ERROR_COUNT(1, result);
@ -1647,22 +1675,31 @@ TEST_CASE_FIXTURE(Fixture, "casting_tables_with_props_into_table_with_indexer4")
TEST_CASE_FIXTURE(Fixture, "table_subtyping_with_missing_props_dont_report_multiple_errors")
{
CheckResult result = check(R"(
local vec3 = {x = 1, y = 2, z = 3}
local vec1 = {x = 1}
vec3 = vec1
function f(vec1: {x: number}): {x: number, y: number, z: number}
return vec1
end
)");
LUAU_REQUIRE_ERROR_COUNT(1, result);
MissingProperties* mp = get<MissingProperties>(result.errors[0]);
REQUIRE(mp);
CHECK_EQ(mp->context, MissingProperties::Missing);
REQUIRE_EQ(2, mp->properties.size());
CHECK_EQ(mp->properties[0], "y");
CHECK_EQ(mp->properties[1], "z");
CHECK_EQ("vec3", toString(mp->superType));
CHECK_EQ("vec1", toString(mp->subType));
if (FFlag::DebugLuauDeferredConstraintResolution)
{
CHECK_EQ("Type pack '{ x: number }' could not be converted into '{ x: number, y: number, z: number }'"
" at [0], { x: number } is not a subtype of { x: number, y: number, z: number }",
toString(result.errors[0]));
}
else
{
MissingProperties* mp = get<MissingProperties>(result.errors[0]);
REQUIRE_MESSAGE(mp, result.errors[0]);
CHECK_EQ(mp->context, MissingProperties::Missing);
REQUIRE_EQ(2, mp->properties.size());
CHECK_EQ(mp->properties[0], "y");
CHECK_EQ(mp->properties[1], "z");
CHECK_EQ("{| x: number, y: number, z: number |}", toString(mp->superType));
CHECK_EQ("{| x: number |}", toString(mp->subType));
}
}
TEST_CASE_FIXTURE(Fixture, "table_subtyping_with_missing_props_dont_report_multiple_errors2")
@ -1687,8 +1724,8 @@ TEST_CASE_FIXTURE(Fixture, "table_subtyping_with_extra_props_dont_report_multipl
function mkvec3() return {x = 1, y = 2, z = 3} end
function mkvec1() return {x = 1} end
local vec3 = {mkvec3()}
local vec1 = {mkvec1()}
local vec3: {{x: number, y: number, z: number}} = {mkvec3()}
local vec1: {{x: number}} = {mkvec1()}
vec1 = vec3
)");
@ -1697,8 +1734,17 @@ TEST_CASE_FIXTURE(Fixture, "table_subtyping_with_extra_props_dont_report_multipl
TypeMismatch* tm = get<TypeMismatch>(result.errors[0]);
REQUIRE(tm);
CHECK_EQ("vec1", toString(tm->wantedType));
CHECK_EQ("vec3", toString(tm->givenType));
if (FFlag::DebugLuauDeferredConstraintResolution)
{
CHECK_EQ("vec1", toString(tm->wantedType));
CHECK_EQ("vec3", toString(tm->givenType));
}
else
{
CHECK_EQ("{{| x: number |}}", toString(tm->wantedType));
CHECK_EQ("{{| x: number, y: number, z: number |}}", toString(tm->givenType));
}
}
TEST_CASE_FIXTURE(Fixture, "table_subtyping_with_extra_props_is_ok")

View File

@ -226,6 +226,9 @@ TEST_CASE_FIXTURE(Fixture, "variadic_packs")
LUAU_REQUIRE_ERROR_COUNT(2, result);
CHECK(Location{Position{3, 21}, Position{3, 26}} == result.errors[0].location);
CHECK(Location{Position{4, 29}, Position{4, 30}} == result.errors[1].location);
CHECK_EQ(
result.errors[0], (TypeError{Location(Position{3, 21}, Position{3, 26}), TypeMismatch{builtinTypes->numberType, builtinTypes->stringType}}));
@ -1015,7 +1018,10 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "detect_cyclic_typepacks2")
end
)");
LUAU_REQUIRE_ERRORS(result);
LUAU_REQUIRE_ERROR_COUNT(2, result);
CHECK("Unknown type 't0'" == toString(result.errors[0]));
CHECK(get<FunctionExitsWithoutReturning>(result.errors[1]));
}
TEST_CASE_FIXTURE(Fixture, "unify_variadic_tails_in_arguments")

596
tests/VecDeque.test.cpp Normal file
View File

@ -0,0 +1,596 @@
// This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details
#include "Luau/VecDeque.h"
#include "doctest.h"
TEST_SUITE_BEGIN("VecDequeTests");
TEST_CASE("forward_queue_test_no_initial_capacity")
{
// initial capacity is not set, so this should grow to be 11
Luau::VecDeque<int> queue{};
REQUIRE(queue.empty());
for (int i = 0; i < 10; i++)
queue.push_back(i);
// q: 0, 1, 2, 3, 4, 5, 6, 7, 8, 9
REQUIRE(!queue.empty());
REQUIRE(queue.size() == 10);
CHECK_EQ(queue.capacity(), 11);
for (int j = 0; j < 10; j++)
{
CHECK_EQ(queue.front(), j);
CHECK_EQ(queue.back(), 9);
REQUIRE(!queue.empty());
queue.pop_front();
}
}
TEST_CASE("forward_queue_test")
{
// initial capacity set to 5 so that a grow is necessary
Luau::VecDeque<int> queue;
queue.reserve(5);
REQUIRE(queue.empty());
for (int i = 0; i < 10; i++)
queue.push_back(i);
// q: 0, 1, 2, 3, 4, 5, 6, 7, 8, 9
REQUIRE(!queue.empty());
REQUIRE(queue.size() == 10);
CHECK_EQ(queue.capacity(), 13);
for (int j = 0; j < 10; j++)
{
CHECK_EQ(queue.front(), j);
CHECK_EQ(queue.back(), 9);
REQUIRE(!queue.empty());
queue.pop_front();
}
}
TEST_CASE("forward_queue_test_initializer_list")
{
Luau::VecDeque<int> queue{0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
// q: 0, 1, 2, 3, 4, 5, 6, 7, 8, 9
REQUIRE(!queue.empty());
REQUIRE(queue.size() == 10);
CHECK_EQ(queue.capacity(), 10);
for (int j = 0; j < 10; j++)
{
CHECK_EQ(queue.front(), j);
CHECK_EQ(queue.back(), 9);
REQUIRE(!queue.empty());
queue.pop_front();
}
}
TEST_CASE("reverse_queue_test")
{
// initial capacity set to 5 so that a grow is necessary
Luau::VecDeque<int> queue;
queue.reserve(5);
REQUIRE(queue.empty());
for (int i = 0; i < 10; i++)
queue.push_front(i);
// q: 9, 8, 7, 6, 5, 4, 3, 2, 1, 0
REQUIRE(!queue.empty());
REQUIRE(queue.size() == 10);
CHECK_EQ(queue.capacity(), 13);
for (int j = 0; j < 10; j++)
{
CHECK_EQ(queue.front(), 9);
CHECK_EQ(queue.back(), j);
REQUIRE(!queue.empty());
queue.pop_back();
}
}
TEST_CASE("random_access_queue_test")
{
// initial capacity set to 5 so that a grow is necessary
Luau::VecDeque<int> queue;
queue.reserve(5);
REQUIRE(queue.empty());
for (int i = 0; i < 10; i++)
queue.push_back(i);
// q: 0, 1, 2, 3, 4, 5, 6, 7, 8, 9
REQUIRE(!queue.empty());
REQUIRE(queue.size() == 10);
for (int j = 0; j < 10; j++)
{
CHECK_EQ(queue.at(j), j);
CHECK_EQ(queue[j], j);
}
}
TEST_CASE("clear_works_on_queue")
{
// initial capacity set to 5 so that a grow is necessary
Luau::VecDeque<int> queue;
queue.reserve(5);
REQUIRE(queue.empty());
for (int i = 0; i < 10; i++)
queue.push_back(i);
// q: 0, 1, 2, 3, 4, 5, 6, 7, 8, 9
REQUIRE(!queue.empty());
REQUIRE(queue.size() == 10);
for (int j = 0; j < 10; j++)
CHECK_EQ(queue[j], j);
queue.clear();
CHECK(queue.empty());
CHECK(queue.size() == 0);
}
TEST_CASE("pop_front_at_end")
{
// initial capacity set to 5 so that a grow is necessary
Luau::VecDeque<int> queue;
queue.reserve(5);
REQUIRE(queue.empty());
// setting up the internal buffer to be: 1234567890 by the end (i.e. 0 at the end of the buffer)
queue.push_front(0);
for (int i = 1; i < 10; i++)
queue.push_back(i);
// q: 0, 1, 2, 3, 4, 5, 6, 7, 8, 9
REQUIRE(!queue.empty());
REQUIRE(queue.size() == 10);
for (int j = 0; j < 10; j++)
{
CHECK_EQ(queue.front(), j);
CHECK_EQ(queue.back(), 9);
REQUIRE(!queue.empty());
queue.pop_front();
}
}
TEST_CASE("pop_back_at_front")
{
// initial capacity set to 5 so that a grow is necessary
Luau::VecDeque<int> queue;
queue.reserve(5);
REQUIRE(queue.empty());
// setting up the internal buffer to be: 9012345678 by the end (i.e. 9 at the front of the buffer)
queue.push_back(0);
for (int i = 1; i < 10; i++)
queue.push_front(i);
// q: 9, 8, 7, 6, 5, 4, 3, 2, 1, 0
REQUIRE(!queue.empty());
REQUIRE(queue.size() == 10);
for (int j = 0; j < 10; j++)
{
CHECK_EQ(queue.front(), 9);
CHECK_EQ(queue.back(), j);
REQUIRE(!queue.empty());
queue.pop_back();
}
}
TEST_CASE("queue_is_contiguous")
{
// initial capacity is not set, so this should grow to be 11
Luau::VecDeque<int> queue{};
REQUIRE(queue.empty());
for (int i = 0; i < 10; i++)
queue.push_back(i);
// q: 0, 1, 2, 3, 4, 5, 6, 7, 8, 9
REQUIRE(!queue.empty());
REQUIRE(queue.size() == 10);
CHECK_EQ(queue.capacity(), 11);
CHECK(queue.is_contiguous());
}
TEST_CASE("queue_is_not_contiguous")
{
// initial capacity is not set, so this should grow to be 11
Luau::VecDeque<int> queue{};
REQUIRE(queue.empty());
for (int i = 5; i < 10; i++)
queue.push_back(i);
for (int i = 4; i >= 0; i--)
queue.push_front(i);
// buffer: 56789......01234
// q: 0, 1, 2, 3, 4, 5, 6, 7, 8, 9
REQUIRE(!queue.empty());
REQUIRE(queue.size() == 10);
CHECK_EQ(queue.capacity(), 11);
CHECK(!queue.is_contiguous());
// checking that it is indeed sequential integers from 0 to 9
for (int j = 0; j < 10; j++)
CHECK_EQ(queue[j], j);
}
TEST_CASE("shrink_to_fit_works")
{
// initial capacity is not set, so this should grow to be 11
Luau::VecDeque<int> queue{};
REQUIRE(queue.empty());
for (int i = 5; i < 10; i++)
queue.push_back(i);
for (int i = 4; i >= 0; i--)
queue.push_front(i);
// buffer: 56789......01234
// q: 0, 1, 2, 3, 4, 5, 6, 7, 8, 9
REQUIRE(!queue.empty());
REQUIRE(queue.size() == 10);
REQUIRE_EQ(queue.capacity(), 11);
CHECK(!queue.is_contiguous());
// checking that it is indeed sequential integers from 0 to 9
for (int j = 0; j < 10; j++)
CHECK_EQ(queue[j], j);
queue.shrink_to_fit();
// shrink to fit always makes a contiguous buffer
CHECK(queue.is_contiguous());
// the capacity should be exactly the size now
CHECK_EQ(queue.capacity(), queue.size());
REQUIRE(!queue.empty());
// checking that it is still sequential integers from 0 to 9
for (int j = 0; j < 10; j++)
CHECK_EQ(queue[j], j);
}
// To avoid hitting SSO issues, we need sufficiently long strings.
// This list of test strings consists of quotes from Ursula K. Le Guin.
const static std::string testStrings[] = {
"Love doesn't just sit there, like a stone, it has to be made, like bread; remade all the time, made new.",
"People who deny the existence of dragons are often eaten by dragons. From within.",
"It is good to have an end to journey toward; but it is the journey that matters, in the end.",
"We're each of us alone, to be sure. What can you do but hold your hand out in the dark?",
"When you light a candle, you also cast a shadow.",
"You cannot buy the revolution. You cannot make the revolution. You can only be the revolution. It is in your spirit, or it is nowhere.",
"To learn which questions are unanswerable, and not to answer them: this skill is most needful in times of stress and darkness.",
"What sane person could live in this world and not be crazy?",
"The only thing that makes life possible is permanent, intolerable uncertainty: not knowing what comes next.",
"My imagination makes me human and makes me a fool; it gives me all the world and exiles me from it."
};
TEST_CASE("string_queue_test_no_initial_capacity")
{
// initial capacity is not set, so this should grow to be 11
Luau::VecDeque<std::string> queue;
REQUIRE(queue.empty());
for (int i = 0; i < 10; i++)
queue.push_back(testStrings[i]);
// q: 0, 1, 2, 3, 4, 5, 6, 7, 8, 9
REQUIRE(!queue.empty());
REQUIRE(queue.size() == 10);
CHECK_EQ(queue.capacity(), 11);
for (int j = 0; j < 10; j++)
{
CHECK_EQ(queue.front(), testStrings[j]);
CHECK_EQ(queue.back(), testStrings[9]);
REQUIRE(!queue.empty());
queue.pop_front();
}
}
TEST_CASE("string_queue_test")
{
// initial capacity set to 5 so that a grow is necessary
Luau::VecDeque<std::string> queue;
queue.reserve(5);
REQUIRE(queue.empty());
for (int i = 0; i < 10; i++)
queue.push_back(testStrings[i]);
// q: 0, 1, 2, 3, 4, 5, 6, 7, 8, 9
REQUIRE(!queue.empty());
REQUIRE(queue.size() == 10);
CHECK_EQ(queue.capacity(), 13);
for (int j = 0; j < 10; j++)
{
CHECK_EQ(queue.front(), testStrings[j]);
CHECK_EQ(queue.back(), testStrings[9]);
REQUIRE(!queue.empty());
queue.pop_front();
}
}
TEST_CASE("string_queue_test_initializer_list")
{
Luau::VecDeque<std::string> queue{
testStrings[0],
testStrings[1],
testStrings[2],
testStrings[3],
testStrings[4],
testStrings[5],
testStrings[6],
testStrings[7],
testStrings[8],
testStrings[9],
};
// q: 0, 1, 2, 3, 4, 5, 6, 7, 8, 9
REQUIRE(!queue.empty());
REQUIRE(queue.size() == 10);
CHECK_EQ(queue.capacity(), 10);
for (int j = 0; j < 10; j++)
{
CHECK_EQ(queue.front(), testStrings[j]);
CHECK_EQ(queue.back(), testStrings[9]);
REQUIRE(!queue.empty());
queue.pop_front();
}
}
TEST_CASE("reverse_string_queue_test")
{
// initial capacity set to 5 so that a grow is necessary
Luau::VecDeque<std::string> queue;
queue.reserve(5);
REQUIRE(queue.empty());
for (int i = 0; i < 10; i++)
queue.push_front(testStrings[i]);
// q: 9, 8, 7, 6, 5, 4, 3, 2, 1, 0
REQUIRE(!queue.empty());
REQUIRE(queue.size() == 10);
CHECK_EQ(queue.capacity(), 13);
for (int j = 0; j < 10; j++)
{
CHECK_EQ(queue.front(), testStrings[9]);
CHECK_EQ(queue.back(), testStrings[j]);
REQUIRE(!queue.empty());
queue.pop_back();
}
}
TEST_CASE("random_access_string_queue_test")
{
// initial capacity set to 5 so that a grow is necessary
Luau::VecDeque<std::string> queue;
queue.reserve(5);
REQUIRE(queue.empty());
for (int i = 0; i < 10; i++)
queue.push_back(testStrings[i]);
// q: 0, 1, 2, 3, 4, 5, 6, 7, 8, 9
REQUIRE(!queue.empty());
REQUIRE(queue.size() == 10);
for (int j = 0; j < 10; j++)
{
CHECK_EQ(queue.at(j), testStrings[j]);
CHECK_EQ(queue[j], testStrings[j]);
}
}
TEST_CASE("clear_works_on_string_queue")
{
// initial capacity set to 5 so that a grow is necessary
Luau::VecDeque<std::string> queue;
queue.reserve(5);
REQUIRE(queue.empty());
for (int i = 0; i < 10; i++)
queue.push_back(testStrings[i]);
// q: 0, 1, 2, 3, 4, 5, 6, 7, 8, 9
REQUIRE(!queue.empty());
REQUIRE(queue.size() == 10);
for (int j = 0; j < 10; j++)
CHECK_EQ(queue[j], testStrings[j]);
queue.clear();
CHECK(queue.empty());
CHECK(queue.size() == 0);
}
TEST_CASE("pop_front_string_at_end")
{
// initial capacity set to 5 so that a grow is necessary
Luau::VecDeque<std::string> queue;
queue.reserve(5);
REQUIRE(queue.empty());
// setting up the internal buffer to be: 1234567890 by the end (i.e. 0 at the end of the buffer)
queue.push_front(testStrings[0]);
for (int i = 1; i < 10; i++)
queue.push_back(testStrings[i]);
// q: 0, 1, 2, 3, 4, 5, 6, 7, 8, 9
REQUIRE(!queue.empty());
REQUIRE(queue.size() == 10);
for (int j = 0; j < 10; j++)
{
CHECK_EQ(queue.front(), testStrings[j]);
CHECK_EQ(queue.back(), testStrings[9]);
REQUIRE(!queue.empty());
queue.pop_front();
}
}
TEST_CASE("pop_back_string_at_front")
{
// initial capacity set to 5 so that a grow is necessary
Luau::VecDeque<std::string> queue;
queue.reserve(5);
REQUIRE(queue.empty());
// setting up the internal buffer to be: 9012345678 by the end (i.e. 9 at the front of the buffer)
queue.push_back(testStrings[0]);
for (int i = 1; i < 10; i++)
queue.push_front(testStrings[i]);
// q: 9, 8, 7, 6, 5, 4, 3, 2, 1, 0
REQUIRE(!queue.empty());
REQUIRE(queue.size() == 10);
for (int j = 0; j < 10; j++)
{
CHECK_EQ(queue.front(), testStrings[9]);
CHECK_EQ(queue.back(), testStrings[j]);
REQUIRE(!queue.empty());
queue.pop_back();
}
}
TEST_CASE("string_queue_is_contiguous")
{
// initial capacity is not set, so this should grow to be 11
Luau::VecDeque<std::string> queue{};
REQUIRE(queue.empty());
for (int i = 0; i < 10; i++)
queue.push_back(testStrings[i]);
// q: 0, 1, 2, 3, 4, 5, 6, 7, 8, 9
REQUIRE(!queue.empty());
REQUIRE(queue.size() == 10);
CHECK_EQ(queue.capacity(), 11);
CHECK(queue.is_contiguous());
}
TEST_CASE("string_queue_is_not_contiguous")
{
// initial capacity is not set, so this should grow to be 11
Luau::VecDeque<std::string> queue{};
REQUIRE(queue.empty());
for (int i = 5; i < 10; i++)
queue.push_back(testStrings[i]);
for (int i = 4; i >= 0; i--)
queue.push_front(testStrings[i]);
// buffer: 56789......01234
// q: 0, 1, 2, 3, 4, 5, 6, 7, 8, 9
REQUIRE(!queue.empty());
REQUIRE(queue.size() == 10);
CHECK_EQ(queue.capacity(), 11);
CHECK(!queue.is_contiguous());
// checking that it is indeed sequential integers from 0 to 9
for (int j = 0; j < 10; j++)
CHECK_EQ(queue[j], testStrings[j]);
}
TEST_CASE("shrink_to_fit_works_with_strings")
{
// initial capacity is not set, so this should grow to be 11
Luau::VecDeque<std::string> queue{};
REQUIRE(queue.empty());
for (int i = 5; i < 10; i++)
queue.push_back(testStrings[i]);
for (int i = 4; i >= 0; i--)
queue.push_front(testStrings[i]);
// buffer: 56789......01234
// q: 0, 1, 2, 3, 4, 5, 6, 7, 8, 9
REQUIRE(!queue.empty());
REQUIRE(queue.size() == 10);
REQUIRE_EQ(queue.capacity(), 11);
CHECK(!queue.is_contiguous());
// checking that it is indeed sequential integers from 0 to 9
for (int j = 0; j < 10; j++)
CHECK_EQ(queue[j], testStrings[j]);
queue.shrink_to_fit();
// shrink to fit always makes a contiguous buffer
CHECK(queue.is_contiguous());
// the capacity should be exactly the size now
CHECK_EQ(queue.capacity(), queue.size());
REQUIRE(!queue.empty());
// checking that it is still sequential integers from 0 to 9
for (int j = 0; j < 10; j++)
CHECK_EQ(queue[j], testStrings[j]);
}
TEST_SUITE_END();

View File

@ -23,6 +23,10 @@ assert(tostring(0.1) == "0.1")
assert(tostring(-0.17) == "-0.17")
assert(tostring(math.pi) == "3.141592653589793")
-- scientific
assert(tostring(1e+30) == "1e+30")
assert(tostring(-1e+24) == "-1e+24")
-- fuzzing corpus
-- Note: If the assert below fires it may indicate floating point denormalized values
-- are not handled as expected.
@ -32,7 +36,7 @@ assert(tostring(4.4154895841930002e-305) == "4.415489584193e-305")
assert(tostring(1125968630513728) == "1125968630513728")
assert(tostring(3.3951932655938423e-313) == "3.3951932656e-313")
assert(tostring(1.625) == "1.625")
assert(tostring(4.9406564584124654e-324) == "5.e-324")
assert(tostring(4.9406564584124654e-324) == "5e-324")
assert(tostring(2.0049288280105384) == "2.0049288280105384")
assert(tostring(3.0517578125e-05) == "0.000030517578125")
assert(tostring(1.383544921875) == "1.383544921875")

185
tools/codesizeprediction.py Normal file
View File

@ -0,0 +1,185 @@
#!/usr/bin/python3
# This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details
# NOTE: This script is experimental. This script uses a linear regression to construct a model for predicting native
# code size from bytecode. Some initial work has been done to analyze a large corpus of Luau scripts, and while for
# most functions the model predicts the native code size quite well (+/-25%), there are many cases where the predicted
# size is off by as much as 13x. Notably, the predicted size is generally better for smaller functions and worse for
# larger functions. Therefore, in its current form this analysis is probably not suitable for use as a basis for
# compilation heuristics. A nonlinear model may produce better results. The script here exists as a foundation for
# further exploration.
import json
import glob
from pathlib import Path
import pandas as pd
import numpy as np
from sklearn.linear_model import LinearRegression
import matplotlib.pyplot as plt
import argparse
def readStats(statsFileGlob):
'''Reads files matching the supplied glob.
Files should be generated by the Compile.cpp CLI'''
statsFiles = glob.glob(statsFileGlob, recursive=True)
print("Reading %s files." % len(statsFiles))
df_dict = {
"statsFile": [],
"script": [],
"name": [],
"line": [],
"bcodeCount": [],
"irCount": [],
"asmCount": [],
"bytecodeSummary": []
}
for statsFile in statsFiles:
stats = json.loads(Path(statsFile).read_text())
for script, filestats in stats.items():
for funstats in filestats["lowerStats"]["functions"]:
df_dict["statsFile"].append(statsFile)
df_dict["script"].append(script)
df_dict["name"].append(funstats["name"])
df_dict["line"].append(funstats["line"])
df_dict["bcodeCount"].append(funstats["bcodeCount"])
df_dict["irCount"].append(funstats["irCount"])
df_dict["asmCount"].append(funstats["asmCount"])
df_dict["bytecodeSummary"].append(
tuple(funstats["bytecodeSummary"][0]))
return pd.DataFrame.from_dict(df_dict)
def addFunctionCount(df):
df2 = df.drop_duplicates(subset=['asmCount', 'bytecodeSummary'], ignore_index=True).groupby(
['bytecodeSummary']).size().reset_index(name='functionCount')
return df.merge(df2, on='bytecodeSummary', how='left')
# def deduplicateDf(df):
# return df.drop_duplicates(subset=['bcodeCount', 'asmCount', 'bytecodeSummary'], ignore_index=True)
def randomizeDf(df):
return df.sample(frac=1)
def splitSeq(seq):
n = len(seq) // 2
return (seq[:n], seq[n:])
def trainAsmSizePredictor(df):
XTrain, XValidate = splitSeq(
np.array([list(seq) for seq in df.bytecodeSummary]))
YTrain, YValidate = splitSeq(np.array(df.asmCount))
reg = LinearRegression(
positive=True, fit_intercept=False).fit(XTrain, YTrain)
YPredict1 = reg.predict(XTrain)
YPredict2 = reg.predict(XValidate)
trainRmse = np.sqrt(np.mean((np.array(YPredict1) - np.array(YTrain))**2))
predictRmse = np.sqrt(
np.mean((np.array(YPredict2) - np.array(YValidate))**2))
print(f"Score: {reg.score(XTrain, YTrain)}")
print(f"Training RMSE: {trainRmse}")
print(f"Prediction RMSE: {predictRmse}")
print(f"Model Intercept: {reg.intercept_}")
print(f"Model Coefficients:\n{reg.coef_}")
df.loc[:, 'asmCountPredicted'] = np.concatenate(
(YPredict1, YPredict2)).round().astype(int)
df['usedForTraining'] = np.concatenate(
(np.repeat(True, YPredict1.size), np.repeat(False, YPredict2.size)))
df['diff'] = df['asmCountPredicted'] - df['asmCount']
df['diffPerc'] = (100 * df['diff']) / df['asmCount']
df.loc[(df["diffPerc"] == np.inf), 'diffPerc'] = 0.0
df['diffPerc'] = df['diffPerc'].round()
return (reg, df)
def saveModel(reg, file):
f = open(file, "w")
f.write(f"Intercept: {reg.intercept_}\n")
f.write(f"Coefficients: \n{reg.coef_}\n")
f.close()
def bcodeVsAsmPlot(df, plotFile=None, minBcodeCount=None, maxBcodeCount=None):
if minBcodeCount is None:
minBcodeCount = df.bcodeCount.min()
if maxBcodeCount is None:
maxBcodeCount = df.bcodeCount.max()
subDf = df[(df.bcodeCount <= maxBcodeCount) &
(df.bcodeCount >= minBcodeCount)]
plt.scatter(subDf.bcodeCount, subDf.asmCount)
plt.title("ASM variation by Bytecode")
plt.xlabel("Bytecode Instruction Count")
plt.ylabel("ASM Instruction Count")
if plotFile is not None:
plt.savefig(plotFile)
return plt
def predictionErrorPlot(df, plotFile=None, minPerc=None, maxPerc=None, bins=200):
if minPerc is None:
minPerc = df['diffPerc'].min()
if maxPerc is None:
maxPerc = df['diffPerc'].max()
plotDf = df[(df["usedForTraining"] == False) & (
df["diffPerc"] >= minPerc) & (df["diffPerc"] <= maxPerc)]
plt.hist(plotDf["diffPerc"], bins=bins)
plt.title("Prediction Error Distribution")
plt.xlabel("Prediction Error %")
plt.ylabel("Function Count")
if plotFile is not None:
plt.savefig(plotFile)
return plt
def parseArgs():
parser = argparse.ArgumentParser(
prog='codesizeprediction.py',
description='Constructs a linear regression model to predict native instruction count from bytecode opcode distribution')
parser.add_argument("fileglob",
help="glob pattern for stats files to be used for training")
parser.add_argument("modelfile",
help="text file to save model details")
parser.add_argument("--nativesizefig",
help="path for saving the plot showing the variation of native code size with bytecode")
parser.add_argument("--predictionerrorfig",
help="path for saving the plot showing the distribution of prediction error")
return parser.parse_args()
if __name__ == "__main__":
args = parseArgs()
df0 = readStats(args.fileglob)
df1 = addFunctionCount(df0)
df2 = randomizeDf(df1)
plt = bcodeVsAsmPlot(df2, args.nativesizefig, 0, 100)
plt.show()
(reg, df4) = trainAsmSizePredictor(df2)
saveModel(reg, args.modelfile)
plt = predictionErrorPlot(df4, args.predictionerrorfig, -200, 200)
plt.show()

View File

@ -4,11 +4,11 @@ AstQuery::getDocumentationSymbolAtPosition.table_overloaded_function_prop
AutocompleteTest.anonymous_autofilled_generic_on_argument_type_pack_vararg
AutocompleteTest.anonymous_autofilled_generic_type_pack_vararg
AutocompleteTest.autocomplete_interpolated_string_as_singleton
AutocompleteTest.autocomplete_response_perf1
AutocompleteTest.autocomplete_string_singleton_equality
AutocompleteTest.autocomplete_string_singleton_escape
AutocompleteTest.autocomplete_string_singletons
AutocompleteTest.do_wrong_compatible_nonself_calls
AutocompleteTest.string_singleton_in_if_statement
AutocompleteTest.suggest_external_module_type
AutocompleteTest.type_correct_expected_argument_type_pack_suggestion
AutocompleteTest.type_correct_expected_argument_type_suggestion
@ -23,7 +23,6 @@ AutocompleteTest.type_correct_suggestion_for_overloads
AutocompleteTest.type_correct_suggestion_in_argument
BuiltinTests.aliased_string_format
BuiltinTests.assert_removes_falsy_types
BuiltinTests.assert_removes_falsy_types2
BuiltinTests.assert_removes_falsy_types_even_from_type_pack_tail_but_only_for_the_first_type
BuiltinTests.assert_returns_false_and_string_iff_it_knows_the_first_argument_cannot_be_truthy
BuiltinTests.bad_select_should_not_crash
@ -41,11 +40,8 @@ BuiltinTests.ipairs_iterator_should_infer_types_and_type_check
BuiltinTests.next_iterator_should_infer_types_and_type_check
BuiltinTests.os_time_takes_optional_date_table
BuiltinTests.pairs_iterator_should_infer_types_and_type_check
BuiltinTests.see_thru_select
BuiltinTests.see_thru_select_count
BuiltinTests.select_slightly_out_of_range
BuiltinTests.select_way_out_of_range
BuiltinTests.select_with_decimal_argument_is_rounded_down
BuiltinTests.select_with_variadic_typepack_tail_and_string_head
BuiltinTests.set_metatable_needs_arguments
BuiltinTests.setmetatable_should_not_mutate_persisted_types
@ -62,14 +58,8 @@ BuiltinTests.table_dot_remove_optionally_returns_generic
BuiltinTests.table_freeze_is_generic
BuiltinTests.table_insert_correctly_infers_type_of_array_2_args_overload
BuiltinTests.table_insert_correctly_infers_type_of_array_3_args_overload
BuiltinTests.table_pack
BuiltinTests.table_pack_reduce
BuiltinTests.table_pack_variadic
BuiltinTests.tonumber_returns_optional_number_type
BuiltinTests.tonumber_returns_optional_number_type2
BuiltinTests.trivial_select
BuiltinTests.xpcall
ControlFlowAnalysis.do_assert_x
ControlFlowAnalysis.for_record_do_if_not_x_break
ControlFlowAnalysis.for_record_do_if_not_x_continue
ControlFlowAnalysis.if_not_x_break
@ -90,8 +80,6 @@ ControlFlowAnalysis.if_not_x_continue_if_not_y_continue
ControlFlowAnalysis.if_not_x_continue_if_not_y_throw
ControlFlowAnalysis.if_not_x_return_elif_not_y_break
ControlFlowAnalysis.if_not_x_return_elif_not_y_fallthrough_elif_not_z_break
ControlFlowAnalysis.if_not_x_then_assert_false
ControlFlowAnalysis.if_not_x_then_error
ControlFlowAnalysis.prototyping_and_visiting_alias_has_the_same_scope_breaking
ControlFlowAnalysis.prototyping_and_visiting_alias_has_the_same_scope_continuing
ControlFlowAnalysis.tagged_unions
@ -112,7 +100,6 @@ Differ.equal_table_kind_B
Differ.equal_table_kind_C
Differ.equal_table_kind_D
Differ.equal_table_measuring_tapes
Differ.equal_table_two_cyclic_tables_are_not_different
Differ.equal_table_two_shifted_circles_are_not_different
Differ.generictp_normal
Differ.generictp_normal_2
@ -122,7 +109,6 @@ Differ.metatable_metamissing_left
Differ.metatable_metamissing_right
Differ.metatable_metanormal
Differ.negation
Differ.right_cyclic_table_left_table_missing_property
Differ.right_cyclic_table_left_table_property_wrong
Differ.table_left_circle_right_measuring_tape
FrontendTest.accumulate_cached_errors_in_consistent_order
@ -159,6 +145,7 @@ GenericsTests.generic_type_pack_unification1
GenericsTests.generic_type_pack_unification2
GenericsTests.generic_type_pack_unification3
GenericsTests.higher_rank_polymorphism_should_not_accept_instantiated_arguments
GenericsTests.hof_subtype_instantiation_regression
GenericsTests.infer_generic_function
GenericsTests.infer_generic_function_function_argument
GenericsTests.infer_generic_function_function_argument_2
@ -179,17 +166,11 @@ GenericsTests.rank_N_types_via_typeof
GenericsTests.self_recursive_instantiated_param
GenericsTests.type_parameters_can_be_polytypes
GenericsTests.typefuns_sharing_types
IntersectionTypes.argument_is_intersection
IntersectionTypes.error_detailed_intersection_all
IntersectionTypes.error_detailed_intersection_part
IntersectionTypes.fx_intersection_as_argument
IntersectionTypes.index_on_an_intersection_type_with_mixed_types
IntersectionTypes.index_on_an_intersection_type_with_one_part_missing_the_property
IntersectionTypes.index_on_an_intersection_type_with_one_property_of_type_any
IntersectionTypes.index_on_an_intersection_type_with_property_guaranteed_to_exist
IntersectionTypes.index_on_an_intersection_type_works_at_arbitrary_depth
IntersectionTypes.intersect_bool_and_false
IntersectionTypes.intersect_false_and_bool_and_false
IntersectionTypes.intersect_metatables
IntersectionTypes.intersect_saturate_overloaded_functions
IntersectionTypes.intersection_of_tables
IntersectionTypes.intersection_of_tables_with_never_properties
@ -208,16 +189,10 @@ IntersectionTypes.overloadeded_functions_with_weird_typepacks_1
IntersectionTypes.overloadeded_functions_with_weird_typepacks_2
IntersectionTypes.overloadeded_functions_with_weird_typepacks_3
IntersectionTypes.overloadeded_functions_with_weird_typepacks_4
IntersectionTypes.propagates_name
IntersectionTypes.select_correct_union_fn
IntersectionTypes.should_still_pick_an_overload_whose_arguments_are_unions
IntersectionTypes.table_extra_ok
IntersectionTypes.table_intersection_setmetatable
IntersectionTypes.table_intersection_write_sealed
IntersectionTypes.table_intersection_write_sealed_indirect
IntersectionTypes.table_write_sealed_indirect
IntersectionTypes.union_saturate_overloaded_functions
isSubtype.any_is_unknown_union_error
Linter.DeprecatedApiFenv
Linter.FormatStringTyped
Linter.TableOperationsIndexer
@ -236,7 +211,6 @@ Normalize.higher_order_function_with_annotation
Normalize.negations_of_tables
Normalize.specific_functions_cannot_be_negated
ProvisionalTests.assign_table_with_refined_property_with_a_similar_type_is_illegal
ProvisionalTests.choose_the_right_overload_for_pcall
ProvisionalTests.discriminate_from_x_not_equal_to_nil
ProvisionalTests.do_not_ice_when_trying_to_pick_first_of_generic_type_pack
ProvisionalTests.error_on_eq_metamethod_returning_a_type_other_than_boolean
@ -245,15 +219,12 @@ ProvisionalTests.floating_generics_should_not_be_allowed
ProvisionalTests.free_is_not_bound_to_any
ProvisionalTests.free_options_can_be_unified_together
ProvisionalTests.free_options_cannot_be_unified_together
ProvisionalTests.function_returns_many_things_but_first_of_it_is_forgotten
ProvisionalTests.generic_type_leak_to_module_interface
ProvisionalTests.generic_type_leak_to_module_interface_variadic
ProvisionalTests.greedy_inference_with_shared_self_triggers_function_with_no_returns
ProvisionalTests.it_should_be_agnostic_of_actual_size
ProvisionalTests.luau-polyfill.Array.filter
ProvisionalTests.luau_roact_useState_minimization
ProvisionalTests.optional_class_instances_are_invariant
ProvisionalTests.pcall_returns_at_least_two_value_but_function_returns_nothing
ProvisionalTests.setmetatable_constrains_free_type_into_free_table
ProvisionalTests.specialization_binds_with_prototypes_too_early
ProvisionalTests.table_insert_with_a_singleton_argument
@ -262,15 +233,12 @@ ProvisionalTests.typeguard_inference_incomplete
ProvisionalTests.while_body_are_also_refined
ProvisionalTests.xpcall_returns_what_f_returns
RefinementTest.assert_a_to_be_truthy_then_assert_a_to_be_number
RefinementTest.assert_non_binary_expressions_actually_resolve_constraints
RefinementTest.correctly_lookup_a_shadowed_local_that_which_was_previously_refined
RefinementTest.correctly_lookup_property_whose_base_was_previously_refined
RefinementTest.dataflow_analysis_can_tell_refinements_when_its_appropriate_to_refine_into_nil_or_never
RefinementTest.discriminate_from_isa_of_x
RefinementTest.discriminate_from_truthiness_of_x
RefinementTest.discriminate_tag
RefinementTest.discriminate_tag_with_implicit_else
RefinementTest.either_number_or_string
RefinementTest.else_with_no_explicit_expression_should_also_refine_the_tagged_union
RefinementTest.fail_to_refine_a_property_of_subscript_expression
RefinementTest.falsiness_of_TruthyPredicate_narrows_into_nil
@ -280,7 +248,6 @@ RefinementTest.index_on_a_refined_property
RefinementTest.isa_type_refinement_must_be_known_ahead_of_time
RefinementTest.luau_polyfill_isindexkey_refine_conjunction
RefinementTest.luau_polyfill_isindexkey_refine_conjunction_variant
RefinementTest.merge_should_be_fully_agnostic_of_hashmap_ordering
RefinementTest.narrow_property_of_a_bounded_variable
RefinementTest.nonoptional_type_can_narrow_to_nil_if_sense_is_true
RefinementTest.not_t_or_some_prop_of_t
@ -292,12 +259,9 @@ RefinementTest.refinements_should_preserve_error_suppression
RefinementTest.string_not_equal_to_string_or_nil
RefinementTest.truthy_constraint_on_properties
RefinementTest.type_annotations_arent_relevant_when_doing_dataflow_analysis
RefinementTest.type_comparison_ifelse_expression
RefinementTest.type_guard_narrowed_into_nothingness
RefinementTest.type_narrow_to_vector
RefinementTest.typeguard_cast_free_table_to_vector
RefinementTest.typeguard_in_assert_position
RefinementTest.typeguard_in_if_condition_position
RefinementTest.x_as_any_if_x_is_instance_elseif_x_is_table
RefinementTest.x_is_not_instance_or_else_not_part
TableTests.a_free_shape_can_turn_into_a_scalar_directly
@ -306,8 +270,6 @@ TableTests.a_free_shape_cannot_turn_into_a_scalar_if_it_is_not_compatible
TableTests.accidentally_checked_prop_in_opposite_branch
TableTests.any_when_indexing_into_an_unsealed_table_with_no_indexer_in_nonstrict_mode
TableTests.array_factory_function
TableTests.call_method
TableTests.call_method_with_explicit_self_argument
TableTests.casting_tables_with_props_into_table_with_indexer2
TableTests.casting_tables_with_props_into_table_with_indexer3
TableTests.casting_tables_with_props_into_table_with_indexer4
@ -339,12 +301,9 @@ TableTests.explicitly_typed_table_with_indexer
TableTests.generalize_table_argument
TableTests.generic_table_instantiation_potential_regression
TableTests.indexer_mismatch
TableTests.indexer_on_sealed_table_must_unify_with_free_table
TableTests.indexers_get_quantified_too
TableTests.indexing_from_a_table_should_prefer_properties_when_possible
TableTests.inequality_operators_imply_exactly_matching_types
TableTests.infer_indexer_from_its_variable_type_and_unifiable
TableTests.infer_type_when_indexing_from_a_table_indexer
TableTests.inferred_return_type_of_free_table
TableTests.instantiate_table_cloning_3
TableTests.invariant_table_properties_means_instantiating_tables_in_assignment_is_unsound
@ -367,6 +326,7 @@ TableTests.oop_polymorphic
TableTests.open_table_unification_2
TableTests.pass_a_union_of_tables_to_a_function_that_requires_a_table
TableTests.pass_a_union_of_tables_to_a_function_that_requires_a_table_2
TableTests.pass_incompatible_union_to_a_generic_table_without_crashing
TableTests.passing_compatible_unions_to_a_generic_table_without_crashing
TableTests.persistent_sealed_table_is_immutable
TableTests.prop_access_on_key_whose_types_mismatches
@ -376,7 +336,6 @@ TableTests.quantify_metatables_of_metatables_of_table
TableTests.reasonable_error_when_adding_a_nonexistent_property_to_an_array_like_table
TableTests.recursive_metatable_type_call
TableTests.result_is_always_any_if_lhs_is_any
TableTests.result_is_bool_for_equality_operators_if_lhs_is_any
TableTests.right_table_missing_key2
TableTests.scalar_is_a_subtype_of_a_compatible_polymorphic_shape_type
TableTests.scalar_is_not_a_subtype_of_a_compatible_polymorphic_shape_type
@ -386,10 +345,8 @@ TableTests.shared_selfs
TableTests.shared_selfs_from_free_param
TableTests.shared_selfs_through_metatables
TableTests.table_call_metamethod_basic
TableTests.table_call_metamethod_generic
TableTests.table_call_metamethod_must_be_callable
TableTests.table_function_check_use_after_free
TableTests.table_param_width_subtyping_1
TableTests.table_param_width_subtyping_2
TableTests.table_param_width_subtyping_3
TableTests.table_simple_call
@ -404,7 +361,6 @@ TableTests.unification_of_unions_in_a_self_referential_type
TableTests.unifying_tables_shouldnt_uaf1
TableTests.used_colon_instead_of_dot
TableTests.used_dot_instead_of_colon
TableTests.used_dot_instead_of_colon_but_correctly
TableTests.when_augmenting_an_unsealed_table_with_an_indexer_apply_the_correct_scope_to_the_indexer_type
TableTests.wrong_assign_does_hit_indexer
ToDot.function
@ -419,15 +375,12 @@ ToString.toStringDetailed2
ToString.toStringErrorPack
ToString.toStringNamedFunction_generic_pack
ToString.toStringNamedFunction_map
TranspilerTests.types_should_not_be_considered_cyclic_if_they_are_not_recursive
TryUnifyTests.members_of_failed_typepack_unification_are_unified_with_errorType
TryUnifyTests.result_of_failed_typepack_unification_is_constrained
TryUnifyTests.typepack_unification_should_trim_free_tails
TryUnifyTests.uninhabited_table_sub_anything
TryUnifyTests.uninhabited_table_sub_never
TryUnifyTests.variadics_should_use_reversed_properly
TypeAliases.corecursive_types_generic
TypeAliases.cyclic_types_of_named_table_fields_do_not_expand_when_stringified
TypeAliases.dont_lose_track_of_PendingExpansionTypes_after_substitution
TypeAliases.generic_param_remap
TypeAliases.mismatched_generic_type_param
@ -450,6 +403,7 @@ TypeFamilyTests.family_as_fn_arg
TypeFamilyTests.family_as_fn_ret
TypeFamilyTests.function_internal_families
TypeFamilyTests.internal_families_raise_errors
TypeFamilyTests.keyof_rfc_example
TypeFamilyTests.table_internal_families
TypeFamilyTests.type_families_inhabited_with_normalization
TypeFamilyTests.unsolvable_family
@ -477,6 +431,7 @@ TypeInfer.recursive_function_that_invokes_itself_with_a_refinement_of_its_parame
TypeInfer.statements_are_topologically_sorted
TypeInfer.stringify_nested_unions_with_optionals
TypeInfer.tc_after_error_recovery_no_replacement_name_in_error
TypeInfer.tc_if_else_expressions_expected_type_3
TypeInfer.type_infer_recursion_limit_no_ice
TypeInfer.type_infer_recursion_limit_normalizer
TypeInfer.unify_nearly_identical_recursive_types
@ -545,7 +500,6 @@ TypeInferFunctions.infer_return_value_type
TypeInferFunctions.infer_that_function_does_not_return_a_table
TypeInferFunctions.inferred_higher_order_functions_are_quantified_at_the_right_time3
TypeInferFunctions.instantiated_type_packs_must_have_a_non_null_scope
TypeInferFunctions.it_is_ok_to_oversaturate_a_higher_order_function_argument
TypeInferFunctions.list_all_overloads_if_no_overload_takes_given_argument_count
TypeInferFunctions.list_only_alternative_overloads_that_match_argument_count
TypeInferFunctions.luau_subtyping_is_np_hard
@ -569,7 +523,6 @@ TypeInferFunctions.too_many_arguments_error_location
TypeInferFunctions.too_many_return_values_in_parentheses
TypeInferFunctions.too_many_return_values_no_function
TypeInferFunctions.toposort_doesnt_break_mutual_recursion
TypeInferFunctions.vararg_function_is_quantified
TypeInferLoops.cli_68448_iterators_need_not_accept_nil
TypeInferLoops.dcr_iteration_explore_raycast_minimization
TypeInferLoops.dcr_iteration_fragmented_keys
@ -628,12 +581,10 @@ TypeInferOperators.concat_op_on_string_lhs_and_free_rhs
TypeInferOperators.disallow_string_and_types_without_metatables_from_arithmetic_binary_ops
TypeInferOperators.equality_operations_succeed_if_any_union_branch_succeeds
TypeInferOperators.error_on_invalid_operand_types_to_relational_operators2
TypeInferOperators.luau-polyfill.String.slice
TypeInferOperators.luau_polyfill_is_array
TypeInferOperators.mm_comparisons_must_return_a_boolean
TypeInferOperators.normalize_strings_comparison
TypeInferOperators.operator_eq_verifies_types_do_intersect
TypeInferOperators.primitive_arith_no_metatable
TypeInferOperators.reducing_and
TypeInferOperators.refine_and_or
TypeInferOperators.reworked_and
@ -654,6 +605,8 @@ TypeInferUnknownNever.index_on_union_of_tables_for_properties_that_is_sorta_neve
TypeInferUnknownNever.length_of_never
TypeInferUnknownNever.math_operators_and_never
TypeInferUnknownNever.type_packs_containing_never_is_itself_uninhabitable
TypePackTests.detect_cyclic_typepacks2
TypePackTests.fuzz_typepack_iter_follow_2
TypePackTests.pack_tail_unification_check
TypePackTests.type_alias_backwards_compatible
TypePackTests.type_alias_default_type_errors
@ -669,10 +622,8 @@ TypeSingletons.function_call_with_singletons_mismatch
TypeSingletons.return_type_of_f_is_not_widened
TypeSingletons.table_properties_type_error_escapes
TypeSingletons.widen_the_supertype_if_it_is_free_and_subtype_has_singleton
TypeStatesTest.invalidate_type_refinements_upon_assignments
UnionTypes.error_detailed_optional
UnionTypes.error_detailed_union_all
UnionTypes.error_takes_optional_arguments
UnionTypes.generic_function_with_optional_arg
UnionTypes.index_on_a_union_type_with_missing_property
UnionTypes.less_greedy_unification_with_union_types

View File

@ -4,7 +4,7 @@
<Type Name="::lua_TValue">
<DisplayString Condition="tt == lua_Type::LUA_TNIL">nil</DisplayString>
<DisplayString Condition="tt == lua_Type::LUA_TBOOLEAN">{(bool)value.b}</DisplayString>
<DisplayString Condition="tt == lua_Type::LUA_TLIGHTUSERDATA">lightuserdata {value.p}</DisplayString>
<DisplayString Condition="tt == lua_Type::LUA_TLIGHTUSERDATA">lightuserdata {(uintptr_t)value.p,h} tag: {extra[0]}</DisplayString>
<DisplayString Condition="tt == lua_Type::LUA_TNUMBER">number = {value.n}</DisplayString>
<DisplayString Condition="tt == lua_Type::LUA_TVECTOR">vector = {value.v[0]}, {value.v[1]}, {*(float*)&amp;extra}</DisplayString>
<DisplayString Condition="tt == lua_Type::LUA_TSTRING">{value.gc->ts}</DisplayString>
@ -28,6 +28,7 @@
<Item Name="[buffer]" Condition="tt == lua_Type::LUA_TBUFFER">value.gc->buf</Item>
<Item Name="[proto]" Condition="tt == lua_Type::LUA_TPROTO">value.gc->p</Item>
<Item Name="[upvalue]" Condition="tt == lua_Type::LUA_TUPVAL">value.gc->uv</Item>
<Item Name="[tag]" Condition="tt == lua_Type::LUA_TLIGHTUSERDATA">extra[0]</Item>
<Synthetic Name="[gc]" Condition="tt >= lua_Type::LUA_TSTRING">
<DisplayString Condition="value.gc-&gt;gch.marked &amp; 8">fixed ({(int)value.gc-&gt;gch.marked})</DisplayString>
<DisplayString Condition="value.gc-&gt;gch.marked &amp; 4">black ({(int)value.gc-&gt;gch.marked})</DisplayString>
@ -41,7 +42,7 @@
<Type Name="::TKey">
<DisplayString Condition="tt == lua_Type::LUA_TNIL">nil</DisplayString>
<DisplayString Condition="tt == lua_Type::LUA_TBOOLEAN">{(bool)value.b}</DisplayString>
<DisplayString Condition="tt == lua_Type::LUA_TLIGHTUSERDATA">lightuserdata {value.p}</DisplayString>
<DisplayString Condition="tt == lua_Type::LUA_TLIGHTUSERDATA">lightuserdata {(uintptr_t)value.p,h} tag: {extra[0]}</DisplayString>
<DisplayString Condition="tt == lua_Type::LUA_TNUMBER">number = {value.n}</DisplayString>
<DisplayString Condition="tt == lua_Type::LUA_TVECTOR">vector = {value.v[0]}, {value.v[1]}, {*(float*)&amp;extra}</DisplayString>
<DisplayString Condition="tt == lua_Type::LUA_TSTRING">{value.gc->ts}</DisplayString>
@ -66,6 +67,7 @@
<Item Name="[proto]" Condition="tt == lua_Type::LUA_TPROTO">value.gc->p</Item>
<Item Name="[upvalue]" Condition="tt == lua_Type::LUA_TUPVAL">value.gc->uv</Item>
<Item Name="[tag]" Condition="tt == lua_Type::LUA_TLIGHTUSERDATA">extra[0]</Item>
<Item Name="[next]">next</Item>
</Expand>
</Type>

View File

@ -60,29 +60,35 @@ def getDuration(nodes, nid):
node = nodes[nid - 1]
total = node['TotalDuration']
for cid in node['NodeIds']:
total -= nodes[cid - 1]['TotalDuration']
if 'NodeIds' in node:
for cid in node['NodeIds']:
total -= nodes[cid - 1]['TotalDuration']
return total
def getFunctionKey(fn):
return fn['Source'] + "," + fn['Name'] + "," + str(fn['Line'])
source = fn['Source'] if 'Source' in fn else ''
name = fn['Name'] if 'Name' in fn else ''
line = str(fn['Line']) if 'Line' in fn else '-1'
return source + "," + name + "," + line
def recursivelyBuildNodeTree(nodes, functions, parent, fid, nid):
ninfo = nodes[nid - 1]
finfo = functions[fid - 1]
child = parent.child(getFunctionKey(finfo))
child.source = finfo['Source']
child.function = finfo['Name']
child.line = int(finfo['Line']) if finfo['Line'] > 0 else 0
child.source = finfo['Source'] if 'Source' in finfo else ''
child.function = finfo['Name'] if 'Name' in finfo else ''
child.line = int(finfo['Line']) if 'Line' in finfo and finfo['Line'] > 0 else 0
child.ticks = getDuration(nodes, nid)
assert(len(ninfo['FunctionIds']) == len(ninfo['NodeIds']))
if 'FunctionIds' in ninfo:
assert(len(ninfo['FunctionIds']) == len(ninfo['NodeIds']))
for i in range(0, len(ninfo['FunctionIds'])):
recursivelyBuildNodeTree(nodes, functions, child, ninfo['FunctionIds'][i], ninfo['NodeIds'][i])
for i in range(0, len(ninfo['FunctionIds'])):
recursivelyBuildNodeTree(nodes, functions, child, ninfo['FunctionIds'][i], ninfo['NodeIds'][i])
return
@ -104,10 +110,11 @@ def nodeFromJSONV2(dump):
child.function = name
child.ticks = getDuration(nodes, nid)
assert(len(node['FunctionIds']) == len(node['NodeIds']))
if 'FunctionIds' in node:
assert(len(node['FunctionIds']) == len(node['NodeIds']))
for i in range(0, len(node['FunctionIds'])):
recursivelyBuildNodeTree(nodes, functions, child, node['FunctionIds'][i], node['NodeIds'][i])
for i in range(0, len(node['FunctionIds'])):
recursivelyBuildNodeTree(nodes, functions, child, node['FunctionIds'][i], node['NodeIds'][i])
return root