diff --git a/Analysis/include/Luau/Substitution.h b/Analysis/include/Luau/Substitution.h index d4c0f166b..8e3bdcd5c 100644 --- a/Analysis/include/Luau/Substitution.h +++ b/Analysis/include/Luau/Substitution.h @@ -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 typeToIndex{nullptr}; DenseHashMap packToIndex{nullptr}; diff --git a/Analysis/include/Luau/Type.h b/Analysis/include/Luau/Type.h index ac104f90f..b368bb202 100644 --- a/Analysis/include/Luau/Type.h +++ b/Analysis/include/Luau/Type.h @@ -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 -#include #include #include #include @@ -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 @@ struct TypeIterator // (T* t, size_t currentIndex) using SavedIterInfo = std::pair; - std::deque stack; + VecDeque stack; DenseHashSet seen{nullptr}; // Only needed to protect the iterator from hanging the thread. void advance() diff --git a/Analysis/include/Luau/TypeFamily.h b/Analysis/include/Luau/TypeFamily.h index 7efe338fb..49c652ee5 100644 --- a/Analysis/include/Luau/TypeFamily.h +++ b/Analysis/include/Luau/TypeFamily.h @@ -163,6 +163,8 @@ struct BuiltinTypeFamilies TypeFamily eqFamily; TypeFamily refineFamily; + TypeFamily keyofFamily; + TypeFamily rawkeyofFamily; void addToScope(NotNull arena, NotNull scope) const; }; diff --git a/Analysis/src/Autocomplete.cpp b/Analysis/src/Autocomplete.cpp index 52cb54e34..55e6d8f0a 100644 --- a/Analysis/src/Autocomplete.cpp +++ b/Analysis/src/Autocomplete.cpp @@ -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 kStatementStartingKeywords = { @@ -1093,11 +1092,8 @@ static AutocompleteEntryMap autocompleteStatement( result.emplace("end", AutocompleteEntry{AutocompleteEntryKind::Keyword}); else if (AstExprFunction* exprFunction = (*it)->as(); exprFunction && !exprFunction->body->hasEnd) result.emplace("end", AutocompleteEntry{AutocompleteEntryKind::Keyword}); - if (FFlag::LuauAutocompleteDoEnd) - { - if (AstStatBlock* exprBlock = (*it)->as(); exprBlock && !exprBlock->hasEnd) - result.emplace("end", AutocompleteEntry{AutocompleteEntryKind::Keyword}); - } + if (AstStatBlock* exprBlock = (*it)->as(); 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(); exprFunction && !exprFunction->DEPRECATED_hasEnd) result.emplace("end", AutocompleteEntry{AutocompleteEntryKind::Keyword}); - if (FFlag::LuauAutocompleteDoEnd) - { - if (AstStatBlock* exprBlock = (*it)->as(); exprBlock && !exprBlock->hasEnd) - result.emplace("end", AutocompleteEntry{AutocompleteEntryKind::Keyword}); - } + if (AstStatBlock* exprBlock = (*it)->as(); exprBlock && !exprBlock->hasEnd) + result.emplace("end", AutocompleteEntry{AutocompleteEntryKind::Keyword}); } } diff --git a/Analysis/src/BuiltinDefinitions.cpp b/Analysis/src/BuiltinDefinitions.cpp index 5ce128731..c0d88d9aa 100644 --- a/Analysis/src/BuiltinDefinitions.cpp +++ b/Analysis/src/BuiltinDefinitions.cpp @@ -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> magicFunctionSetMetaTable( else if (get(target) || get(target) || isTableIntersection(target)) { } + else if (FFlag::LuauSetMetatableOnUnionsOfTables && isTableUnion(target)) + { + const UnionType* ut = get(target); + LUAU_ASSERT(ut); + + std::vector resultParts; + + for (TypeId ty : ut) + resultParts.push_back(arena.addType(MetatableType{ty, mt})); + + return WithPredicate{arena.addTypePack({arena.addType(UnionType{std::move(resultParts)})})}; + } else { typechecker.reportError(TypeError{expr.location, GenericError{"setmetatable should take a table"}}); diff --git a/Analysis/src/Clone.cpp b/Analysis/src/Clone.cpp index 8fc9a3572..6d2d04b6c 100644 --- a/Analysis/src/Clone.cpp +++ b/Analysis/src/Clone.cpp @@ -159,6 +159,15 @@ class TypeCloner2 TypeId target = arena->addType(ty->ty); asMutable(target)->documentationSymbol = ty->documentationSymbol; + if (auto generic = getMutable(target)) + generic->scope = nullptr; + else if (auto free = getMutable(target)) + free->scope = nullptr; + else if (auto fn = getMutable(target)) + fn->scope = nullptr; + else if (auto table = getMutable(target)) + table->scope = nullptr; + (*types)[ty] = target; queue.push_back(target); return target; @@ -175,6 +184,11 @@ class TypeCloner2 TypePackId target = arena->addTypePack(tp->ty); + if (auto generic = getMutable(target)) + generic->scope = nullptr; + else if (auto free = getMutable(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; } diff --git a/Analysis/src/ConstraintGenerator.cpp b/Analysis/src/ConstraintGenerator.cpp index 5bfe39e75..3ef73b33a 100644 --- a/Analysis/src/ConstraintGenerator.cpp +++ b/Analysis/src/ConstraintGenerator.cpp @@ -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* 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(target); + std::vector 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()) { diff --git a/Analysis/src/ConstraintSolver.cpp b/Analysis/src/ConstraintSolver.cpp index 9024bd858..7430dbf3a 100644 --- a/Analysis/src/ConstraintSolver.cpp +++ b/Analysis/src/ConstraintSolver.cpp @@ -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 #include @@ -450,10 +451,10 @@ struct TypeAndLocation struct FreeTypeSearcher : TypeOnceVisitor { - std::deque* result; + VecDeque* result; Location location; - FreeTypeSearcher(std::deque* result, Location location) + FreeTypeSearcher(VecDeque* result, Location location) : result(result) , location(location) { @@ -484,7 +485,7 @@ void ConstraintSolver::finalizeModule() Unifier2 u2{NotNull{arena}, builtinTypes, rootScope, NotNull{&iceReporter}}; - std::deque queue; + VecDeque queue; for (auto& [name, binding] : rootScope->bindings) queue.push_back({binding.typeId, binding.location}); diff --git a/Analysis/src/Def.cpp b/Analysis/src/Def.cpp index 1ecaca221..09dc2bb13 100644 --- a/Analysis/src/Def.cpp +++ b/Analysis/src/Def.cpp @@ -4,7 +4,6 @@ #include "Luau/Common.h" #include -#include namespace Luau { diff --git a/Analysis/src/EmbeddedBuiltinDefinitions.cpp b/Analysis/src/EmbeddedBuiltinDefinitions.cpp index 874f86279..4525368f9 100644 --- a/Analysis/src/EmbeddedBuiltinDefinitions.cpp +++ b/Analysis/src/EmbeddedBuiltinDefinitions.cpp @@ -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(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...) + +declare function type(value: T): string +declare function typeof(value: T): string + +-- `assert` has a magic function attached that will give more detailed type information +declare function assert(value: T, errorMessage: string?): T +declare function error(message: T, level: number?): never + +declare function tostring(value: T): string +declare function tonumber(value: T, radix: number?): number? + +declare function rawequal(a: T1, b: T2): boolean +declare function rawget(tab: {[K]: V}, k: K): V +declare function rawset(tab: {[K]: V}, k: K, v: V): {[K]: V} +declare function rawlen(obj: {[K]: V} | string): number + +declare function setfenv(target: number | (T...) -> R..., env: {[string]: any}): ((T...) -> R...)? + +declare function ipairs(tab: {V}): (({V}, number) -> (number?, V), {V}, number) + +declare function pcall(f: (A...) -> R..., ...: A...): (boolean, R...) + +-- FIXME: The actual type of `xpcall` is: +-- (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(f: (A...) -> R1..., err: (E) -> R2..., ...: A...): (boolean, R1...) + +-- `select` has a magic function attached to provide more detailed type information +declare function select(i: string | number, ...: A...): ...any + +-- FIXME: This type is not entirely correct - `loadstring` returns a function or +-- (nil, string). +declare function loadstring(src: string, chunkname: string?): (((A...) -> any)?, string?) + +declare function @checked newproxy(mt: boolean?): any + +declare coroutine: { + create: (f: (A...) -> R...) -> thread, + resume: (co: thread, A...) -> (boolean, R...), + running: () -> thread, + status: @checked (co: thread) -> "dead" | "running" | "normal" | "suspended", + wrap: (f: (A...) -> R...) -> ((A...) -> R...), + yield: (A...) -> R..., + isyieldable: () -> boolean, + close: @checked (co: thread) -> (boolean, any) +} + +declare table: { + concat: (t: {V}, sep: string?, i: number?, j: number?) -> string, + insert: ((t: {V}, value: V) -> ()) & ((t: {V}, pos: number, value: V) -> ()), + maxn: (t: {V}) -> number, + remove: (t: {V}, number?) -> V?, + sort: (t: {V}, comp: ((V, V) -> boolean)?) -> (), + create: (count: number, value: V?) -> {V}, + find: (haystack: {V}, needle: V, init: number?) -> number?, + + unpack: (list: {V}, i: number?, j: number?) -> ...V, + pack: (...V) -> { n: number, [number]: V }, + + getn: (t: {V}) -> number, + foreach: (t: {[K]: V}, f: (K, V) -> ()) -> (), + foreachi: ({V}, (number, V) -> ()) -> (), + + move: (src: {V}, a: number, b: number, t: number, dst: {V}?) -> {V}, + clear: (table: {[K]: V}) -> (), + + isfrozen: (t: {[K]: V}) -> boolean, +} + +declare debug: { + info: ((thread: thread, level: number, options: string) -> R...) & ((level: number, options: string) -> R...) & ((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(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; } diff --git a/Analysis/src/Frontend.cpp b/Analysis/src/Frontend.cpp index 13b8949e7..005dcf3cc 100644 --- a/Analysis/src/Frontend.cpp +++ b/Analysis/src/Frontend.cpp @@ -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* 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> reverseDeps; for (const auto& module : sourceNodes) diff --git a/Analysis/src/GlobalTypes.cpp b/Analysis/src/GlobalTypes.cpp index 1c96ad702..ee59f52bb 100644 --- a/Analysis/src/GlobalTypes.cpp +++ b/Analysis/src/GlobalTypes.cpp @@ -2,7 +2,6 @@ #include "Luau/GlobalTypes.h" -LUAU_FASTFLAG(LuauInitializeStringMetatableInGlobalTypes) LUAU_FASTFLAG(LuauBufferTypeck) namespace Luau @@ -24,14 +23,11 @@ GlobalTypes::GlobalTypes(NotNull 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::String, stringMetatableTy); - persist(stringMetatableTy); - freeze(*builtinTypes->arena); - } + unfreeze(*builtinTypes->arena); + TypeId stringMetatableTy = makeStringMetatable(builtinTypes); + asMutable(builtinTypes->stringType)->ty.emplace(PrimitiveType::String, stringMetatableTy); + persist(stringMetatableTy); + freeze(*builtinTypes->arena); } } // namespace Luau diff --git a/Analysis/src/Substitution.cpp b/Analysis/src/Substitution.cpp index 2f4e96112..7da9aee85 100644 --- a/Analysis/src/Substitution.cpp +++ b/Analysis/src/Substitution.cpp @@ -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)); diff --git a/Analysis/src/Subtyping.cpp b/Analysis/src/Subtyping.cpp index 49db9cd31..b8671d211 100644 --- a/Analysis/src/Subtyping.cpp +++ b/Analysis/src/Subtyping.cpp @@ -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(*subTail, *superTail)) + else if (auto p = get2(*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(*subTail, *superTail)) + else if (auto p = get2(*subTail, *superTail)) { - if (variance == Variance::Contravariant) + if (TypeId t = follow(p.second->ty); get(t) || get(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...) -> number results.push_back(SubtypingResult{false}.withBothComponent(TypePath::PackField::Tail)); @@ -747,6 +754,8 @@ SubtypingResult Subtyping::isCovariantWith(SubtypingEnvironment& env, TypePackId template 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 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 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)); } diff --git a/Analysis/src/Type.cpp b/Analysis/src/Type.cpp index e76837a8d..843ac25ef 100644 --- a/Analysis/src/Type.cpp +++ b/Analysis/src/Type.cpp @@ -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 @@ -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 flattenIntersection(TypeId ty) return {ty}; std::unordered_set seen; - std::deque queue{ty}; + VecDeque queue{ty}; std::vector 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(follow(ty)); + if (!ut) + return false; + + return std::all_of(begin(ut), end(ut), getTableType); +} + bool isOverloadedFunction(TypeId ty) { if (!get(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 queue{ty}; + VecDeque queue{ty}; while (!queue.empty()) { diff --git a/Analysis/src/TypeChecker2.cpp b/Analysis/src/TypeChecker2.cpp index f67286575..349443ab5 100644 --- a/Analysis/src/TypeChecker2.cpp +++ b/Analysis/src/TypeChecker2.cpp @@ -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. + + const size_t firstUnsatisfiedArgument = argExprs->size(); + const auto [requiredHead, _requiredTail] = flatten(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}}; - ++paramIter; - ++argOffset; - } + return {Analysis::ArityMismatch, {error}}; + } - 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 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}}; - 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}}; + return {Analysis::Ok, {}}; } - else if (auto vtp = get(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(follow(paramIter.tail()))) - argumentErrors.push_back(TypeError{argExpr->location, TypePackMismatch{fn->argTypes, arena->addTypePack(*args)}}); - - ++argOffset; } - 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()); + ErrorVec errors; - // It may have a tail, however, so check that. - if (auto vtp = get(follow(args->tail))) + if (!sr.isErrorSuppressing) + { + for (const SubtypingReasoning& reason : sr.reasoning) { - AstExpr* argExpr = argExprs->at(argExprs->size() - 1); + /* The return type of our prospective function is always + * any... so any subtype failures here can only arise from + * argument type mismatches. + */ - 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); + Location argLocation; - // 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}}; - } + if (const Luau::TypePath::Index* pathIndexComponent = get_if(&reason.superPath.components.at(1))) + { + size_t nthArgument = pathIndexComponent->index; + argLocation = argExprs->at(nthArgument)->location; - ++paramIter; - } + std::optional failedSubTy = traverseForType(fnTy, reason.subPath, builtinTypes); + std::optional failedSuperTy = traverseForType(prospectiveFunction, reason.superPath, builtinTypes); - // 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()); + 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; + } + } + } - const Location argLoc = argExprs->empty() ? Location{} // TODO - : argExprs->at(argExprs->size() - 1)->location; + std::optional failedSubPack = traverseForPack(fnTy, reason.subPath, builtinTypes); + std::optional failedSuperPack = traverseForPack(prospectiveFunction, reason.superPath, builtinTypes); - 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 (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; + } + } - if (get(paramTail)) - { - argumentErrors.push_back(TypeError{argLoc, TypePackMismatch{fn->argTypes, arena->addTypePack(*args)}}); - } - else if (get(paramTail)) - { - // Nothing. This is ok. } } - return {argumentErrors.empty() ? Ok : OverloadIsNonviable, argumentErrors}; + 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; } diff --git a/Analysis/src/TypeFamily.cpp b/Analysis/src/TypeFamily.cpp index 3e2c9cc49..11457b8c8 100644 --- a/Analysis/src/TypeFamily.cpp +++ b/Analysis/src/TypeFamily.cpp @@ -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 tys; - std::deque tps; + VecDeque tys; + VecDeque tps; bool visit(TypeId ty, const TypeFamilyInstanceType&) override { @@ -60,8 +61,8 @@ struct FamilyReducer { TypeFamilyContext ctx; - std::deque queuedTys; - std::deque queuedTps; + VecDeque queuedTys; + VecDeque queuedTps; DenseHashSet irreducible{nullptr}; FamilyGraphReductionResult result; bool force = false; @@ -69,7 +70,7 @@ struct FamilyReducer // Local to the constraint being reduced. Location location; - FamilyReducer(std::deque queuedTys, std::deque queuedTps, Location location, TypeFamilyContext ctx, bool force = false) + FamilyReducer(VecDeque queuedTys, VecDeque 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 queuedTys, std::deque queuedTps, Location location, TypeFamilyContext ctx, bool force) + VecDeque queuedTys, VecDeque 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 refineFamilyFn(const std::vector& 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& result, DenseHashSet& seen, bool isRaw, NotNull ctx) +{ + // if the type is the top table type, the answer is just "all strings" + if (get(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(ty)) + { + for (auto [key, _] : tableTy->props) + result.insert(key); + return true; + } + + // otherwise, we have a metatable to deal with + if (auto metatableTy = get(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 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 keyofFamilyImpl(const std::vector& typeParams, const std::vector& packParams, NotNull 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 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(*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(*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 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 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` 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 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 keyofFamilyFn(const std::vector& typeParams, const std::vector& packParams, NotNull 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 rawkeyofFamilyFn(const std::vector& typeParams, const std::vector& packParams, NotNull 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 arena, NotNull sc scope->exportedTypeBindings[ltFamily.name] = mkBinaryTypeFamily(<Family); 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 diff --git a/Analysis/src/TypeInfer.cpp b/Analysis/src/TypeInfer.cpp index 0ffe40df6..54018a266 100644 --- a/Analysis/src/TypeInfer.cpp +++ b/Analysis/src/TypeInfer.cpp @@ -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()) { - 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 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 binding; diff --git a/CLI/Compile.cpp b/CLI/Compile.cpp index 950432717..c993d3088 100644 --- a/CLI/Compile.cpp +++ b/CLI/Compile.cpp @@ -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& 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: compile with debug level n (default 1, n should be between 0 and 2).\n"); printf(" --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 of compilation stats (total, file, function).\n"); + printf(" --bytecode-summary: Compute bytecode operation distribution.\n"); printf(" --stats-file=: file in which compilation stats will be recored (default 'stats.json').\n"); - printf(" --record-stats=: granularity of compilation stats recorded in stats.json (total, file, function).\n"); printf(" --vector-lib=: name of the library providing vector type operations.\n"); printf(" --vector-ctor=: name of the function constructing a vector value.\n"); printf(" --vector-type=: 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, "}"); diff --git a/CodeGen/include/Luau/AssemblyBuilderA64.h b/CodeGen/include/Luau/AssemblyBuilderA64.h index 32bf56db6..0ed359107 100644 --- a/CodeGen/include/Luau/AssemblyBuilderA64.h +++ b/CodeGen/include/Luau/AssemblyBuilderA64.h @@ -180,6 +180,8 @@ class AssemblyBuilderA64 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 data; diff --git a/CodeGen/include/Luau/AssemblyBuilderX64.h b/CodeGen/include/Luau/AssemblyBuilderX64.h index 65d3ce0ac..e5b82f080 100644 --- a/CodeGen/include/Luau/AssemblyBuilderX64.h +++ b/CodeGen/include/Luau/AssemblyBuilderX64.h @@ -187,6 +187,8 @@ class AssemblyBuilderX64 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 data; @@ -266,6 +268,8 @@ class AssemblyBuilderX64 uint8_t* codePos = nullptr; uint8_t* codeEnd = nullptr; + + unsigned instructionCount = 0; }; } // namespace X64 diff --git a/CodeGen/include/Luau/CodeGen.h b/CodeGen/include/Luau/CodeGen.h index ce8b1df8a..6fa634e16 100644 --- a/CodeGen/include/Luau/CodeGen.h +++ b/CodeGen/include/Luau/CodeGen.h @@ -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> bytecodeSummary; }; struct LoweringStats @@ -127,7 +137,7 @@ struct LoweringStats BlockLinearizationStats blockLinearizationStats; - bool collectFunctionStats = false; + unsigned functionStatsFlags = 0; std::vector 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; } diff --git a/CodeGen/include/Luau/IrData.h b/CodeGen/include/Luau/IrData.h index 2317459aa..bd44a9e21 100644 --- a/CodeGen/include/Luau/IrData.h +++ b/CodeGen/include/Luau/IrData.h @@ -13,8 +13,6 @@ #include #include -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 valueRestoreOps; std::vector 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]; - - if (instIdx >= block.start && instIdx <= block.finish) - return valueRestoreOps[instIdx]; - } + const IrBlock& block = blocks[blockIdx]; - 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 diff --git a/CodeGen/include/Luau/IrVisitUseDef.h b/CodeGen/include/Luau/IrVisitUseDef.h index 603f1ec59..a05229d6e 100644 --- a/CodeGen/include/Luau/IrVisitUseDef.h +++ b/CodeGen/include/Luau/IrVisitUseDef.h @@ -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: diff --git a/CodeGen/src/AssemblyBuilderA64.cpp b/CodeGen/src/AssemblyBuilderA64.cpp index 16d9bfd4b..3873a513d 100644 --- a/CodeGen/src/AssemblyBuilderA64.cpp +++ b/CodeGen/src/AssemblyBuilderA64.cpp @@ -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); diff --git a/CodeGen/src/AssemblyBuilderX64.cpp b/CodeGen/src/AssemblyBuilderX64.cpp index ec53916f4..482aca844 100644 --- a/CodeGen/src/AssemblyBuilderX64.cpp +++ b/CodeGen/src/AssemblyBuilderX64.cpp @@ -6,8 +6,6 @@ #include #include -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(); } diff --git a/CodeGen/src/BytecodeSummary.cpp b/CodeGen/src/BytecodeSummary.cpp index 7bf38cc4e..32029b118 100644 --- a/CodeGen/src/BytecodeSummary.cpp +++ b/CodeGen/src/BytecodeSummary.cpp @@ -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 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; diff --git a/CodeGen/src/CodeGenAssembly.cpp b/CodeGen/src/CodeGenAssembly.cpp index 116e28dab..7246f0e72 100644 --- a/CodeGen/src/CodeGenAssembly.cpp +++ b/CodeGen/src/CodeGenAssembly.cpp @@ -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) diff --git a/CodeGen/src/CodeGenLower.h b/CodeGen/src/CodeGenLower.h index 3075ac9ad..3acecc130 100644 --- a/CodeGen/src/CodeGenLower.h +++ b/CodeGen/src/CodeGenLower.h @@ -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(); } diff --git a/CodeGen/src/EmitCommonX64.h b/CodeGen/src/EmitCommonX64.h index bc1f99c90..4ae1c3b64 100644 --- a/CodeGen/src/EmitCommonX64.h +++ b/CodeGen/src/EmitCommonX64.h @@ -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)]; diff --git a/CodeGen/src/EmitInstructionX64.cpp b/CodeGen/src/EmitInstructionX64.cpp index 13be20b9f..f478d6b5b 100644 --- a/CodeGen/src/EmitInstructionX64.cpp +++ b/CodeGen/src/EmitInstructionX64.cpp @@ -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(uintptr_t(index + 1))); + // setpvalue(ra + 2, reinterpret_cast(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)); diff --git a/CodeGen/src/IrBuilder.cpp b/CodeGen/src/IrBuilder.cpp index 1fcc516c4..54605a7b9 100644 --- a/CodeGen/src/IrBuilder.cpp +++ b/CodeGen/src/IrBuilder.cpp @@ -13,8 +13,6 @@ #include -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) diff --git a/CodeGen/src/IrDump.cpp b/CodeGen/src/IrDump.cpp index 727ce10d2..ec9f978b4 100644 --- a/CodeGen/src/IrDump.cpp +++ b/CodeGen/src/IrDump.cpp @@ -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: diff --git a/CodeGen/src/IrLoweringA64.cpp b/CodeGen/src/IrLoweringA64.cpp index 5d0be75a0..dc20d6e8e 100644 --- a/CodeGen/src/IrLoweringA64.cpp +++ b/CodeGen/src/IrLoweringA64.cpp @@ -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)); diff --git a/CodeGen/src/IrLoweringX64.cpp b/CodeGen/src/IrLoweringX64.cpp index b5fba08fe..081c63204 100644 --- a/CodeGen/src/IrLoweringX64.cpp +++ b/CodeGen/src/IrLoweringX64.cpp @@ -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))); - - 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)); + 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); - } + 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))); - - 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)); + 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); - } + build.mov(word[bufferAddrOp(inst.a, inst.b)], value); break; } diff --git a/CodeGen/src/IrTranslateBuiltins.cpp b/CodeGen/src/IrTranslateBuiltins.cpp index 8dd330eb9..2f233b6c6 100644 --- a/CodeGen/src/IrTranslateBuiltins.cpp +++ b/CodeGen/src/IrTranslateBuiltins.cpp @@ -8,8 +8,6 @@ #include -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}; diff --git a/CodeGen/src/IrTranslation.cpp b/CodeGen/src/IrTranslation.cpp index b8bfd1721..8897996eb 100644 --- a/CodeGen/src/IrTranslation.cpp +++ b/CodeGen/src/IrTranslation.cpp @@ -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(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(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(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(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); diff --git a/CodeGen/src/IrUtils.cpp b/CodeGen/src/IrUtils.cpp index 15cd94263..46d638a94 100644 --- a/CodeGen/src/IrUtils.cpp +++ b/CodeGen/src/IrUtils.cpp @@ -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: diff --git a/CodeGen/src/IrValueLocationTracking.cpp b/CodeGen/src/IrValueLocationTracking.cpp index b17be6821..a0d926064 100644 --- a/CodeGen/src/IrValueLocationTracking.cpp +++ b/CodeGen/src/IrValueLocationTracking.cpp @@ -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: diff --git a/CodeGen/src/OptimizeConstProp.cpp b/CodeGen/src/OptimizeConstProp.cpp index d19ce50e1..3c447ea75 100644 --- a/CodeGen/src/OptimizeConstProp.cpp +++ b/CodeGen/src/OptimizeConstProp.cpp @@ -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& visited, IrBlock* block, ConstPropState& state) @@ -1403,15 +1393,12 @@ static void constPropInBlockChain(IrBuilder& build, std::vector& 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 diff --git a/Common/include/Luau/DenseHash.h b/Common/include/Luau/DenseHash.h index 8f1fc8b7e..507a9c488 100644 --- a/Common/include/Luau/DenseHash.h +++ b/Common/include/Luau/DenseHash.h @@ -3,7 +3,7 @@ #include "Luau/Common.h" -#include +#include #include #include #include diff --git a/Common/include/Luau/VecDeque.h b/Common/include/Luau/VecDeque.h new file mode 100644 index 000000000..37c0ca486 --- /dev/null +++ b/Common/include/Luau/VecDeque.h @@ -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 +#include +#include +#include +#include + +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> +class VecDeque : Allocator +{ +private: + static_assert(std::is_nothrow_move_constructible_v); + static_assert(std::is_nothrow_move_assignable_v); + + 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 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::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 diff --git a/Compiler/src/Builtins.cpp b/Compiler/src/Builtins.cpp index 407b76a41..2b09b7e06 100644 --- a/Compiler/src/Builtins.cpp +++ b/Compiler/src/Builtins.cpp @@ -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; diff --git a/Compiler/src/Compiler.cpp b/Compiler/src/Compiler.cpp index 4363a74c8..5174ac9d7 100644 --- a/Compiler/src/Compiler.cpp +++ b/Compiler/src/Compiler.cpp @@ -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) diff --git a/Compiler/src/Types.cpp b/Compiler/src/Types.cpp index bb89cd154..8f630f389 100644 --- a/Compiler/src/Types.cpp +++ b/Compiler/src/Types.cpp @@ -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; diff --git a/Sources.cmake b/Sources.cmake index a46496759..bbe9efee3 100644 --- a/Sources.cmake +++ b/Sources.cmake @@ -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() diff --git a/VM/src/lbitlib.cpp b/VM/src/lbitlib.cpp index 627d599ef..fbbc95ba5 100644 --- a/VM/src/lbitlib.cpp +++ b/VM/src/lbitlib.cpp @@ -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); diff --git a/VM/src/lbuflib.cpp b/VM/src/lbuflib.cpp index 3fb6e1660..178261fb3 100644 --- a/VM/src/lbuflib.cpp +++ b/VM/src/lbuflib.cpp @@ -10,8 +10,6 @@ #include -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"); diff --git a/VM/src/lnumprint.cpp b/VM/src/lnumprint.cpp index d64e3ca40..c09b1be27 100644 --- a/VM/src/lnumprint.cpp +++ b/VM/src/lnumprint.cpp @@ -11,6 +11,8 @@ #include #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); } } diff --git a/VM/src/lobject.h b/VM/src/lobject.h index 9459c6e68..842be5ff3 100644 --- a/VM/src/lobject.h +++ b/VM/src/lobject.h @@ -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)) diff --git a/VM/src/lstate.cpp b/VM/src/lstate.cpp index 858f61a3a..1c4d9c51f 100644 --- a/VM/src/lstate.cpp +++ b/VM/src/lstate.cpp @@ -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; diff --git a/VM/src/lstate.h b/VM/src/lstate.h index 2c6d35dc5..0776d6dcd 100644 --- a/VM/src/lstate.h +++ b/VM/src/lstate.h @@ -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 diff --git a/VM/src/lstring.cpp b/VM/src/lstring.cpp index e57f6c29e..9f6bb4761 100644 --- a/VM/src/lstring.cpp +++ b/VM/src/lstring.cpp @@ -7,6 +7,8 @@ #include + + 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); } diff --git a/VM/src/lutf8lib.cpp b/VM/src/lutf8lib.cpp index ef99b94fe..e18c0b966 100644 --- a/VM/src/lutf8lib.cpp +++ b/VM/src/lutf8lib.cpp @@ -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 } diff --git a/VM/src/lvmexecute.cpp b/VM/src/lvmexecute.cpp index 82d8ea7e6..d8e316bd3 100644 --- a/VM/src/lvmexecute.cpp +++ b/VM/src/lvmexecute.cpp @@ -778,6 +778,7 @@ static void luau_execute(lua_State* L) break; case LCT_REF: + setupvalue(L, &ncl->l.uprefs[ui], luaF_findupval(L, VM_REG(LUAU_INSN_B(uinsn)))); break; @@ -1112,7 +1113,9 @@ static void luau_execute(lua_State* L) 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 @@ static void luau_execute(lua_State* L) 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(); diff --git a/tests/Autocomplete.test.cpp b/tests/Autocomplete.test.cpp index 0a1e5a7e6..2cfd20d32 100644 --- a/tests/Autocomplete.test.cpp +++ b/tests/Autocomplete.test.cpp @@ -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'); diff --git a/tests/Conformance.test.cpp b/tests/Conformance.test.cpp index 978656aa9..fe555ba0d 100644 --- a/tests/Conformance.test.cpp +++ b/tests/Conformance.test.cpp @@ -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({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({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({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({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), diff --git a/tests/Frontend.test.cpp b/tests/Frontend.test.cpp index 0f9d87329..039decb59 100644 --- a/tests/Frontend.test.cpp +++ b/tests/Frontend.test.cpp @@ -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 diff --git a/tests/Subtyping.test.cpp b/tests/Subtyping.test.cpp index 56eba0240..1eca7ec11 100644 --- a/tests/Subtyping.test.cpp +++ b/tests/Subtyping.test.cpp @@ -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...) -> ()") +{ + 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...) -> ()"); +} + +// See https://github.com/luau-lang/luau/issues/767 +TEST_CASE_FIXTURE(SubtypeFixture, "(...unknown) -> () <: (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...) -> ()"); +} + /* * 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}, {}); diff --git a/tests/Transpiler.test.cpp b/tests/Transpiler.test.cpp index 871d984ee..c429eb5b8 100644 --- a/tests/Transpiler.test.cpp +++ b/tests/Transpiler.test.cpp @@ -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 diff --git a/tests/TypeFamily.test.cpp b/tests/TypeFamily.test.cpp index 7de508a03..c8280dbfa 100644 --- a/tests/TypeFamily.test.cpp +++ b/tests/TypeFamily.test.cpp @@ -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 = string -- shadow add + + -- this should be ok + function hi(f: add) + 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) -> add"); +} + 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 + + 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(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 + + 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(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 + + 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 is uninhabited"); + CHECK(toString(result.errors[1]) == "Type family instance keyof is uninhabited"); + CHECK(toString(result.errors[2]) == "Type family instance keyof 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 + + 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 is uninhabited"); + CHECK(toString(result.errors[1]) == "Type family instance keyof is uninhabited"); + CHECK(toString(result.errors[2]) == "Type family instance keyof 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 + + 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(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 + + 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(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 + + 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 is uninhabited"); + CHECK(toString(result.errors[1]) == "Type family instance rawkeyof is uninhabited"); + CHECK(toString(result.errors[2]) == "Type family instance rawkeyof 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 + + 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 is uninhabited"); + CHECK(toString(result.errors[1]) == "Type family instance rawkeyof is uninhabited"); + CHECK(toString(result.errors[2]) == "Type family instance rawkeyof 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 + + 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(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 + + 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 is uninhabited"); + CHECK(toString(result.errors[1]) == "Type family instance keyof is uninhabited"); + CHECK(toString(result.errors[2]) == "Type family instance keyof 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 + + 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 is uninhabited"); + CHECK(toString(result.errors[1]) == "Type family instance keyof is uninhabited"); + CHECK(toString(result.errors[2]) == "Type family instance keyof 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 + + function speakByType(animal: AnimalType) + animals[animal].speak() + end + + speakByType("dog") -- ok + speakByType("cactus") -- errors + )"); + + LUAU_REQUIRE_ERROR_COUNT(1, result); + + TypePackMismatch* tpm = get(result.errors[0]); + REQUIRE(tpm); + CHECK_EQ("\"cat\" | \"dog\" | \"monkey\" | \"fox\"", toString(tpm->wantedTp)); + CHECK_EQ("\"cactus\"", toString(tpm->givenTp)); +} + TEST_SUITE_END(); diff --git a/tests/TypeInfer.aliases.test.cpp b/tests/TypeInfer.aliases.test.cpp index 00f59a772..93b8bbabe 100644 --- a/tests/TypeInfer.aliases.test.cpp +++ b/tests/TypeInfer.aliases.test.cpp @@ -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(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 = {v:T, b:B} type B = {v:T, a:A} - local aa:A - local bb = aa + + function f(a: A) + return a + end )"; const std::string expected = R"( type A = {v:T, b:B} type B = {v:T, a:A} - local aa:A - local bb:A=aa + + function f(a: A): A + 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(); diff --git a/tests/TypeInfer.builtins.test.cpp b/tests/TypeInfer.builtins.test.cpp index ef7c968ae..45f5d01c2 100644 --- a/tests/TypeInfer.builtins.test.cpp +++ b/tests/TypeInfer.builtins.test.cpp @@ -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); } diff --git a/tests/TypeInfer.definitions.test.cpp b/tests/TypeInfer.definitions.test.cpp index 6de1b0503..c57eab798 100644 --- a/tests/TypeInfer.definitions.test.cpp +++ b/tests/TypeInfer.definitions.test.cpp @@ -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 diff --git a/tests/TypeInfer.functions.test.cpp b/tests/TypeInfer.functions.test.cpp index 2ed91e964..21187ffe9 100644 --- a/tests/TypeInfer.functions.test.cpp +++ b/tests/TypeInfer.functions.test.cpp @@ -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"( diff --git a/tests/TypeInfer.intersectionTypes.test.cpp b/tests/TypeInfer.intersectionTypes.test.cpp index d2f5a472f..80511b4bf 100644 --- a/tests/TypeInfer.intersectionTypes.test.cpp +++ b/tests/TypeInfer.intersectionTypes.test.cpp @@ -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() - local x : ((number?) -> (a | number)) & ((string?) -> (a | string)) - local y : (nil) -> a = x -- OK - local z : (number?) -> a = x -- Not OK - end + function f() + 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() - 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() + 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() - 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() + 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() - local x : ((number) -> number) & ((nil) -> unknown) - local y : (number?) -> unknown = x -- OK - local z : (number?) -> number? = x -- Not OK - end + function f() + 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() - local x : ((number) -> number?) & ((unknown) -> string?) - local y : (number) -> nil = x -- OK - local z : (number?) -> nil = x -- Not OK - end + function f() + 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() - local x : ((number) -> number) & ((nil) -> never) - local y : (number?) -> number = x -- OK - local z : (number?) -> never = x -- Not OK - end + function f() + 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() - local x : ((number) -> number?) & ((never) -> string?) - local y : (never) -> nil = x -- OK - local z : (number?) -> nil = x -- Not OK - end + function f() + 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() - local x : (() -> a...) & (() -> b...) - local y : (() -> b...) & (() -> a...) = x -- OK - local z : () -> () = x -- Not OK - end + function f() + 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() - local x : ((a...) -> ()) & ((b...) -> ()) - local y : ((b...) -> ()) & ((a...) -> ()) = x -- OK - local z : () -> () = x -- Not OK - end + function f() + 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() - local x : (() -> a...) & (() -> (number?,a...)) - local y : (() -> (number?,a...)) & (() -> a...) = x -- OK - local z : () -> (number) = x -- Not OK - end + function f() + 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() - local x : ((a...) -> ()) & ((number,a...) -> number) - local y : ((number,a...) -> number) & ((a...) -> ()) = x -- OK - local z : (number?) -> () = x -- Not OK - end + function f() + 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(); diff --git a/tests/TypeInfer.singletons.test.cpp b/tests/TypeInfer.singletons.test.cpp index b8ab635aa..2af18684e 100644 --- a/tests/TypeInfer.singletons.test.cpp +++ b/tests/TypeInfer.singletons.test.cpp @@ -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 | Ok'; type a (a) is not a subtype of Err | Ok[1] (Err)\n" - "\ttype a[\"success\"] (false) is not exactly Err | Ok[0][\"success\"] (true)"; + "Type 'a' could not be converted into 'Err | Ok'"; CHECK(toString(result.errors[0]) == expectedError); } diff --git a/tests/TypeInfer.tables.test.cpp b/tests/TypeInfer.tables.test.cpp index f483d8230..fb30c4024 100644 --- a/tests/TypeInfer.tables.test.cpp +++ b/tests/TypeInfer.tables.test.cpp @@ -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(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(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(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") diff --git a/tests/TypeInfer.typePacks.test.cpp b/tests/TypeInfer.typePacks.test.cpp index a955e3efc..cf05abf4e 100644 --- a/tests/TypeInfer.typePacks.test.cpp +++ b/tests/TypeInfer.typePacks.test.cpp @@ -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(result.errors[1])); } TEST_CASE_FIXTURE(Fixture, "unify_variadic_tails_in_arguments") diff --git a/tests/VecDeque.test.cpp b/tests/VecDeque.test.cpp new file mode 100644 index 000000000..066b15e68 --- /dev/null +++ b/tests/VecDeque.test.cpp @@ -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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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(); diff --git a/tests/conformance/strconv.lua b/tests/conformance/strconv.lua index 8f056eb0b..444234567 100644 --- a/tests/conformance/strconv.lua +++ b/tests/conformance/strconv.lua @@ -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") diff --git a/tools/codesizeprediction.py b/tools/codesizeprediction.py new file mode 100644 index 000000000..ba877efe4 --- /dev/null +++ b/tools/codesizeprediction.py @@ -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() diff --git a/tools/faillist.txt b/tools/faillist.txt index 9f6c3c827..6ab283593 100644 --- a/tools/faillist.txt +++ b/tools/faillist.txt @@ -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 diff --git a/tools/natvis/VM.natvis b/tools/natvis/VM.natvis index 2dc508154..5d5bf5cb2 100644 --- a/tools/natvis/VM.natvis +++ b/tools/natvis/VM.natvis @@ -4,7 +4,7 @@ nil {(bool)value.b} - lightuserdata {value.p} + lightuserdata {(uintptr_t)value.p,h} tag: {extra[0]} number = {value.n} vector = {value.v[0]}, {value.v[1]}, {*(float*)&extra} {value.gc->ts} @@ -28,6 +28,7 @@ value.gc->buf value.gc->p value.gc->uv + extra[0] fixed ({(int)value.gc->gch.marked}) black ({(int)value.gc->gch.marked}) @@ -41,7 +42,7 @@ nil {(bool)value.b} - lightuserdata {value.p} + lightuserdata {(uintptr_t)value.p,h} tag: {extra[0]} number = {value.n} vector = {value.v[0]}, {value.v[1]}, {*(float*)&extra} {value.gc->ts} @@ -66,6 +67,7 @@ value.gc->p value.gc->uv + extra[0] next diff --git a/tools/perfgraph.py b/tools/perfgraph.py index 94c57cc77..1f6ecc2f1 100644 --- a/tools/perfgraph.py +++ b/tools/perfgraph.py @@ -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