Sync to upstream/release/514 (#357)

This commit is contained in:
Arseny Kapoulkine 2022-02-11 11:02:09 -08:00 committed by GitHub
parent aecd60371b
commit 63d5423bbb
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
45 changed files with 989 additions and 268 deletions

View File

@ -86,6 +86,8 @@ struct OwningAutocompleteResult
}; };
AutocompleteResult autocomplete(Frontend& frontend, const ModuleName& moduleName, Position position, StringCompletionCallback callback); AutocompleteResult autocomplete(Frontend& frontend, const ModuleName& moduleName, Position position, StringCompletionCallback callback);
// Deprecated, do not use in new work.
OwningAutocompleteResult autocompleteSource(Frontend& frontend, std::string_view source, Position position, StringCompletionCallback callback); OwningAutocompleteResult autocompleteSource(Frontend& frontend, std::string_view source, Position position, StringCompletionCallback callback);
} // namespace Luau } // namespace Luau

View File

@ -49,6 +49,7 @@ struct LintWarning
Code_DeprecatedApi = 22, Code_DeprecatedApi = 22,
Code_TableOperations = 23, Code_TableOperations = 23,
Code_DuplicateCondition = 24, Code_DuplicateCondition = 24,
Code_MisleadingAndOr = 25,
Code__Count Code__Count
}; };

View File

@ -93,7 +93,7 @@ struct Tarjan
// This should never be null; ensure you initialize it before calling // This should never be null; ensure you initialize it before calling
// substitution methods. // substitution methods.
const TxnLog* log; const TxnLog* log = nullptr;
std::vector<TypeId> edgesTy; std::vector<TypeId> edgesTy;
std::vector<TypePackId> edgesTp; std::vector<TypePackId> edgesTp;

View File

@ -307,8 +307,8 @@ private:
// //
// We can't use a DenseHashMap here because we need a non-const iterator // We can't use a DenseHashMap here because we need a non-const iterator
// over the map when we concatenate. // over the map when we concatenate.
std::unordered_map<TypeId, std::unique_ptr<PendingType>> typeVarChanges; std::unordered_map<TypeId, std::unique_ptr<PendingType>, DenseHashPointer> typeVarChanges;
std::unordered_map<TypePackId, std::unique_ptr<PendingTypePack>> typePackChanges; std::unordered_map<TypePackId, std::unique_ptr<PendingTypePack>, DenseHashPointer> typePackChanges;
TxnLog* parent = nullptr; TxnLog* parent = nullptr;

View File

@ -103,6 +103,11 @@ struct GenericTypeDefinitions
std::vector<GenericTypePackDefinition> genericPacks; std::vector<GenericTypePackDefinition> genericPacks;
}; };
struct HashBoolNamePair
{
size_t operator()(const std::pair<bool, Name>& pair) const;
};
// All TypeVars are retained via Environment::typeVars. All TypeIds // All TypeVars are retained via Environment::typeVars. All TypeIds
// within a program are borrowed pointers into this set. // within a program are borrowed pointers into this set.
struct TypeChecker struct TypeChecker
@ -411,6 +416,12 @@ public:
private: private:
int checkRecursionCount = 0; int checkRecursionCount = 0;
int recursionCount = 0; int recursionCount = 0;
/**
* We use this to avoid doing second-pass analysis of type aliases that are duplicates. We record a pair
* (exported, name) to properly deal with the case where the two duplicates do not have the same export status.
*/
DenseHashSet<std::pair<bool, Name>, HashBoolNamePair> duplicateTypeAliases;
}; };
// Unit test hook // Unit test hook

View File

@ -54,9 +54,6 @@ struct TypePackVar
bool persistent = false; bool persistent = false;
// Pointer to the type arena that allocated this type. // Pointer to the type arena that allocated this type.
// Do not depend on the value of this under any circumstances. This is for
// debugging purposes only. This is only set in debug builds; it is nullptr
// in all other environments.
TypeArena* owningArena = nullptr; TypeArena* owningArena = nullptr;
}; };

View File

@ -449,9 +449,6 @@ struct TypeVar final
std::optional<std::string> documentationSymbol; std::optional<std::string> documentationSymbol;
// Pointer to the type arena that allocated this type. // Pointer to the type arena that allocated this type.
// Do not depend on the value of this under any circumstances. This is for
// debugging purposes only. This is only set in debug builds; it is nullptr
// in all other environments.
TypeArena* owningArena = nullptr; TypeArena* owningArena = nullptr;
bool operator==(const TypeVar& rhs) const; bool operator==(const TypeVar& rhs) const;

View File

@ -1,8 +1,6 @@
// This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details // This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details
#include "Luau/BuiltinDefinitions.h" #include "Luau/BuiltinDefinitions.h"
LUAU_FASTFLAGVARIABLE(LuauFixTonumberReturnType, false)
namespace Luau namespace Luau
{ {
@ -115,6 +113,7 @@ declare function gcinfo(): number
declare function error<T>(message: T, level: number?) declare function error<T>(message: T, level: number?)
declare function tostring<T>(value: T): string declare function tostring<T>(value: T): string
declare function tonumber<T>(value: T, radix: number?): number?
declare function rawequal<T1, T2>(a: T1, b: T2): boolean declare function rawequal<T1, T2>(a: T1, b: T2): boolean
declare function rawget<K, V>(tab: {[K]: V}, k: K): V declare function rawget<K, V>(tab: {[K]: V}, k: K): V
@ -200,14 +199,7 @@ declare function gcinfo(): number
std::string getBuiltinDefinitionSource() std::string getBuiltinDefinitionSource()
{ {
std::string result = kBuiltinDefinitionLuaSrc; return kBuiltinDefinitionLuaSrc;
if (FFlag::LuauFixTonumberReturnType)
result += "declare function tonumber<T>(value: T, radix: number?): number?\n";
else
result += "declare function tonumber<T>(value: T, radix: number?): number\n";
return result;
} }
} // namespace Luau } // namespace Luau

View File

@ -43,6 +43,7 @@ static const char* kWarningNames[] = {
"DeprecatedApi", "DeprecatedApi",
"TableOperations", "TableOperations",
"DuplicateCondition", "DuplicateCondition",
"MisleadingAndOr",
}; };
// clang-format on // clang-format on
@ -2040,18 +2041,28 @@ private:
const Property* prop = lookupClassProp(cty, node->index.value); const Property* prop = lookupClassProp(cty, node->index.value);
if (prop && prop->deprecated) if (prop && prop->deprecated)
{ report(node->location, *prop, cty->name.c_str(), node->index.value);
if (!prop->deprecatedSuggestion.empty()) }
emitWarning(*context, LintWarning::Code_DeprecatedApi, node->location, "Member '%s.%s' is deprecated, use '%s' instead", else if (const TableTypeVar* tty = get<TableTypeVar>(follow(*ty)))
cty->name.c_str(), node->index.value, prop->deprecatedSuggestion.c_str()); {
else auto prop = tty->props.find(node->index.value);
emitWarning(*context, LintWarning::Code_DeprecatedApi, node->location, "Member '%s.%s' is deprecated", cty->name.c_str(),
node->index.value); if (prop != tty->props.end() && prop->second.deprecated)
} report(node->location, prop->second, tty->name ? tty->name->c_str() : nullptr, node->index.value);
} }
return true; return true;
} }
void report(const Location& location, const Property& prop, const char* container, const char* field)
{
std::string suggestion = prop.deprecatedSuggestion.empty() ? "" : format(", use '%s' instead", prop.deprecatedSuggestion.c_str());
if (container)
emitWarning(*context, LintWarning::Code_DeprecatedApi, location, "Member '%s.%s' is deprecated%s", container, field, suggestion.c_str());
else
emitWarning(*context, LintWarning::Code_DeprecatedApi, location, "Member '%s' is deprecated%s", field, suggestion.c_str());
}
}; };
class LintTableOperations : AstVisitor class LintTableOperations : AstVisitor
@ -2257,6 +2268,39 @@ private:
return false; return false;
} }
bool visit(AstExprIfElse* expr) override
{
if (!expr->falseExpr->is<AstExprIfElse>())
return true;
// if..elseif chain detected, we need to unroll it
std::vector<AstExpr*> conditions;
conditions.reserve(2);
AstExprIfElse* head = expr;
while (head)
{
head->condition->visit(this);
head->trueExpr->visit(this);
conditions.push_back(head->condition);
if (head->falseExpr->is<AstExprIfElse>())
{
head = head->falseExpr->as<AstExprIfElse>();
continue;
}
head->falseExpr->visit(this);
break;
}
detectDuplicates(conditions);
// block recursive visits so that we only analyze each chain once
return false;
}
bool visit(AstExprBinary* expr) override bool visit(AstExprBinary* expr) override
{ {
if (expr->op != AstExprBinary::And && expr->op != AstExprBinary::Or) if (expr->op != AstExprBinary::And && expr->op != AstExprBinary::Or)
@ -2418,6 +2462,46 @@ private:
} }
}; };
class LintMisleadingAndOr : AstVisitor
{
public:
LUAU_NOINLINE static void process(LintContext& context)
{
LintMisleadingAndOr pass;
pass.context = &context;
context.root->visit(&pass);
}
private:
LintContext* context;
bool visit(AstExprBinary* node) override
{
if (node->op != AstExprBinary::Or)
return true;
AstExprBinary* and_ = node->left->as<AstExprBinary>();
if (!and_ || and_->op != AstExprBinary::And)
return true;
const char* alt = nullptr;
if (and_->right->is<AstExprConstantNil>())
alt = "nil";
else if (AstExprConstantBool* c = and_->right->as<AstExprConstantBool>(); c && c->value == false)
alt = "false";
if (alt)
emitWarning(*context, LintWarning::Code_MisleadingAndOr, node->location,
"The and-or expression always evaluates to the second alternative because the first alternative is %s; consider using if-then-else "
"expression instead",
alt);
return true;
}
};
static void fillBuiltinGlobals(LintContext& context, const AstNameTable& names, const ScopePtr& env) static void fillBuiltinGlobals(LintContext& context, const AstNameTable& names, const ScopePtr& env)
{ {
ScopePtr current = env; ScopePtr current = env;
@ -2522,6 +2606,9 @@ std::vector<LintWarning> lint(AstStat* root, const AstNameTable& names, const Sc
if (context.warningEnabled(LintWarning::Code_DuplicateLocal)) if (context.warningEnabled(LintWarning::Code_DuplicateLocal))
LintDuplicateLocal::process(context); LintDuplicateLocal::process(context);
if (context.warningEnabled(LintWarning::Code_MisleadingAndOr))
LintMisleadingAndOr::process(context);
std::sort(context.result.begin(), context.result.end(), WarningComparator()); std::sort(context.result.begin(), context.result.end(), WarningComparator());
return context.result; return context.result;

View File

@ -12,10 +12,10 @@
#include <algorithm> #include <algorithm>
LUAU_FASTFLAGVARIABLE(DebugLuauFreezeArena, false) LUAU_FASTFLAGVARIABLE(DebugLuauFreezeArena, false)
LUAU_FASTFLAGVARIABLE(DebugLuauTrackOwningArena, false) LUAU_FASTFLAGVARIABLE(DebugLuauTrackOwningArena, false) // Remove with FFlagLuauImmutableTypes
LUAU_FASTINTVARIABLE(LuauTypeCloneRecursionLimit, 300) LUAU_FASTINTVARIABLE(LuauTypeCloneRecursionLimit, 300)
LUAU_FASTFLAG(LuauTypeAliasDefaults) LUAU_FASTFLAG(LuauTypeAliasDefaults)
LUAU_FASTFLAG(LuauImmutableTypes)
LUAU_FASTFLAGVARIABLE(LuauPrepopulateUnionOptionsBeforeAllocation, false) LUAU_FASTFLAGVARIABLE(LuauPrepopulateUnionOptionsBeforeAllocation, false)
namespace Luau namespace Luau
@ -66,7 +66,7 @@ TypeId TypeArena::addTV(TypeVar&& tv)
{ {
TypeId allocated = typeVars.allocate(std::move(tv)); TypeId allocated = typeVars.allocate(std::move(tv));
if (FFlag::DebugLuauTrackOwningArena) if (FFlag::DebugLuauTrackOwningArena || FFlag::LuauImmutableTypes)
asMutable(allocated)->owningArena = this; asMutable(allocated)->owningArena = this;
return allocated; return allocated;
@ -76,7 +76,7 @@ TypeId TypeArena::freshType(TypeLevel level)
{ {
TypeId allocated = typeVars.allocate(FreeTypeVar{level}); TypeId allocated = typeVars.allocate(FreeTypeVar{level});
if (FFlag::DebugLuauTrackOwningArena) if (FFlag::DebugLuauTrackOwningArena || FFlag::LuauImmutableTypes)
asMutable(allocated)->owningArena = this; asMutable(allocated)->owningArena = this;
return allocated; return allocated;
@ -86,7 +86,7 @@ TypePackId TypeArena::addTypePack(std::initializer_list<TypeId> types)
{ {
TypePackId allocated = typePacks.allocate(TypePack{std::move(types)}); TypePackId allocated = typePacks.allocate(TypePack{std::move(types)});
if (FFlag::DebugLuauTrackOwningArena) if (FFlag::DebugLuauTrackOwningArena || FFlag::LuauImmutableTypes)
asMutable(allocated)->owningArena = this; asMutable(allocated)->owningArena = this;
return allocated; return allocated;
@ -96,7 +96,7 @@ TypePackId TypeArena::addTypePack(std::vector<TypeId> types)
{ {
TypePackId allocated = typePacks.allocate(TypePack{std::move(types)}); TypePackId allocated = typePacks.allocate(TypePack{std::move(types)});
if (FFlag::DebugLuauTrackOwningArena) if (FFlag::DebugLuauTrackOwningArena || FFlag::LuauImmutableTypes)
asMutable(allocated)->owningArena = this; asMutable(allocated)->owningArena = this;
return allocated; return allocated;
@ -106,7 +106,7 @@ TypePackId TypeArena::addTypePack(TypePack tp)
{ {
TypePackId allocated = typePacks.allocate(std::move(tp)); TypePackId allocated = typePacks.allocate(std::move(tp));
if (FFlag::DebugLuauTrackOwningArena) if (FFlag::DebugLuauTrackOwningArena || FFlag::LuauImmutableTypes)
asMutable(allocated)->owningArena = this; asMutable(allocated)->owningArena = this;
return allocated; return allocated;
@ -116,7 +116,7 @@ TypePackId TypeArena::addTypePack(TypePackVar tp)
{ {
TypePackId allocated = typePacks.allocate(std::move(tp)); TypePackId allocated = typePacks.allocate(std::move(tp));
if (FFlag::DebugLuauTrackOwningArena) if (FFlag::DebugLuauTrackOwningArena || FFlag::LuauImmutableTypes)
asMutable(allocated)->owningArena = this; asMutable(allocated)->owningArena = this;
return allocated; return allocated;
@ -454,8 +454,16 @@ TypeId clone(TypeId typeId, TypeArena& dest, SeenTypes& seenTypes, SeenTypePacks
TypeCloner cloner{dest, typeId, seenTypes, seenTypePacks, cloneState}; TypeCloner cloner{dest, typeId, seenTypes, seenTypePacks, cloneState};
Luau::visit(cloner, typeId->ty); // Mutates the storage that 'res' points into. Luau::visit(cloner, typeId->ty); // Mutates the storage that 'res' points into.
// TODO: Make this work when the arena of 'res' might be frozen if (FFlag::LuauImmutableTypes)
asMutable(res)->documentationSymbol = typeId->documentationSymbol; {
// Persistent types are not being cloned and we get the original type back which might be read-only
if (!res->persistent)
asMutable(res)->documentationSymbol = typeId->documentationSymbol;
}
else
{
asMutable(res)->documentationSymbol = typeId->documentationSymbol;
}
} }
return res; return res;

View File

@ -2,6 +2,8 @@
#include "Luau/Scope.h" #include "Luau/Scope.h"
LUAU_FASTFLAG(LuauTwoPassAliasDefinitionFix);
namespace Luau namespace Luau
{ {
@ -17,6 +19,8 @@ Scope::Scope(const ScopePtr& parent, int subLevel)
, returnType(parent->returnType) , returnType(parent->returnType)
, level(parent->level.incr()) , level(parent->level.incr())
{ {
if (FFlag::LuauTwoPassAliasDefinitionFix)
level = level.incr();
level.subLevel = subLevel; level.subLevel = subLevel;
} }

View File

@ -250,6 +250,10 @@ PendingTypePack* TxnLog::queue(TypePackId tp)
PendingType* TxnLog::pending(TypeId ty) const PendingType* TxnLog::pending(TypeId ty) const
{ {
// This function will technically work if `this` is nullptr, but this
// indicates a bug, so we explicitly assert.
LUAU_ASSERT(static_cast<const void*>(this) != nullptr);
for (const TxnLog* current = this; current; current = current->parent) for (const TxnLog* current = this; current; current = current->parent)
{ {
if (auto it = current->typeVarChanges.find(ty); it != current->typeVarChanges.end()) if (auto it = current->typeVarChanges.find(ty); it != current->typeVarChanges.end())
@ -261,6 +265,10 @@ PendingType* TxnLog::pending(TypeId ty) const
PendingTypePack* TxnLog::pending(TypePackId tp) const PendingTypePack* TxnLog::pending(TypePackId tp) const
{ {
// This function will technically work if `this` is nullptr, but this
// indicates a bug, so we explicitly assert.
LUAU_ASSERT(static_cast<const void*>(this) != nullptr);
for (const TxnLog* current = this; current; current = current->parent) for (const TxnLog* current = this; current; current = current->parent)
{ {
if (auto it = current->typePackChanges.find(tp); it != current->typePackChanges.end()) if (auto it = current->typePackChanges.find(tp); it != current->typePackChanges.end())

View File

@ -32,12 +32,13 @@ LUAU_FASTFLAGVARIABLE(LuauRecursiveTypeParameterRestriction, false)
LUAU_FASTFLAGVARIABLE(LuauGenericFunctionsDontCacheTypeParams, false) LUAU_FASTFLAGVARIABLE(LuauGenericFunctionsDontCacheTypeParams, false)
LUAU_FASTFLAGVARIABLE(LuauIfElseBranchTypeUnion, false) LUAU_FASTFLAGVARIABLE(LuauIfElseBranchTypeUnion, false)
LUAU_FASTFLAGVARIABLE(LuauIfElseExpectedType2, false) LUAU_FASTFLAGVARIABLE(LuauIfElseExpectedType2, false)
LUAU_FASTFLAGVARIABLE(LuauImmutableTypes, false)
LUAU_FASTFLAGVARIABLE(LuauLengthOnCompositeType, false) LUAU_FASTFLAGVARIABLE(LuauLengthOnCompositeType, false)
LUAU_FASTFLAGVARIABLE(LuauNoSealedTypeMod, false) LUAU_FASTFLAGVARIABLE(LuauNoSealedTypeMod, false)
LUAU_FASTFLAGVARIABLE(LuauQuantifyInPlace2, false) LUAU_FASTFLAGVARIABLE(LuauQuantifyInPlace2, false)
LUAU_FASTFLAGVARIABLE(LuauSealExports, false) LUAU_FASTFLAGVARIABLE(LuauSealExports, false)
LUAU_FASTFLAGVARIABLE(LuauSingletonTypes, false) LUAU_FASTFLAGVARIABLE(LuauSingletonTypes, false)
LUAU_FASTFLAGVARIABLE(LuauDiscriminableUnions, false) LUAU_FASTFLAGVARIABLE(LuauDiscriminableUnions2, false)
LUAU_FASTFLAGVARIABLE(LuauTypeAliasDefaults, false) LUAU_FASTFLAGVARIABLE(LuauTypeAliasDefaults, false)
LUAU_FASTFLAGVARIABLE(LuauExpectedTypesOfProperties, false) LUAU_FASTFLAGVARIABLE(LuauExpectedTypesOfProperties, false)
LUAU_FASTFLAGVARIABLE(LuauErrorRecoveryType, false) LUAU_FASTFLAGVARIABLE(LuauErrorRecoveryType, false)
@ -47,7 +48,10 @@ LUAU_FASTFLAGVARIABLE(LuauProperTypeLevels, false)
LUAU_FASTFLAGVARIABLE(LuauAscribeCorrectLevelToInferredProperitesOfFreeTables, false) LUAU_FASTFLAGVARIABLE(LuauAscribeCorrectLevelToInferredProperitesOfFreeTables, false)
LUAU_FASTFLAG(LuauUnionTagMatchFix) LUAU_FASTFLAG(LuauUnionTagMatchFix)
LUAU_FASTFLAGVARIABLE(LuauUnsealedTableLiteral, false) LUAU_FASTFLAGVARIABLE(LuauUnsealedTableLiteral, false)
LUAU_FASTFLAGVARIABLE(LuauTwoPassAliasDefinitionFix, false)
LUAU_FASTFLAGVARIABLE(LuauAssertStripsFalsyTypes, false) LUAU_FASTFLAGVARIABLE(LuauAssertStripsFalsyTypes, false)
LUAU_FASTFLAGVARIABLE(LuauReturnAnyInsteadOfICE, false) // Eventually removed as false.
LUAU_FASTFLAGVARIABLE(LuauAnotherTypeLevelFix, false)
namespace Luau namespace Luau
{ {
@ -213,6 +217,11 @@ static bool isMetamethod(const Name& name)
name == "__metatable" || name == "__eq" || name == "__lt" || name == "__le" || name == "__mode"; name == "__metatable" || name == "__eq" || name == "__lt" || name == "__le" || name == "__mode";
} }
size_t HashBoolNamePair::operator()(const std::pair<bool, Name>& pair) const
{
return std::hash<bool>()(pair.first) ^ std::hash<Name>()(pair.second);
}
TypeChecker::TypeChecker(ModuleResolver* resolver, InternalErrorReporter* iceHandler) TypeChecker::TypeChecker(ModuleResolver* resolver, InternalErrorReporter* iceHandler)
: resolver(resolver) : resolver(resolver)
, iceHandler(iceHandler) , iceHandler(iceHandler)
@ -225,6 +234,7 @@ TypeChecker::TypeChecker(ModuleResolver* resolver, InternalErrorReporter* iceHan
, anyType(getSingletonTypes().anyType) , anyType(getSingletonTypes().anyType)
, optionalNumberType(getSingletonTypes().optionalNumberType) , optionalNumberType(getSingletonTypes().optionalNumberType)
, anyTypePack(getSingletonTypes().anyTypePack) , anyTypePack(getSingletonTypes().anyTypePack)
, duplicateTypeAliases{{false, {}}}
{ {
globalScope = std::make_shared<Scope>(globalTypes.addTypePack(TypePackVar{FreeTypePack{TypeLevel{}}})); globalScope = std::make_shared<Scope>(globalTypes.addTypePack(TypePackVar{FreeTypePack{TypeLevel{}}}));
@ -291,6 +301,9 @@ ModulePtr TypeChecker::check(const SourceModule& module, Mode mode, std::optiona
unifierState.skipCacheForType.clear(); unifierState.skipCacheForType.clear();
} }
if (FFlag::LuauTwoPassAliasDefinitionFix)
duplicateTypeAliases.clear();
return std::move(currentModule); return std::move(currentModule);
} }
@ -496,6 +509,9 @@ LUAU_NOINLINE void TypeChecker::checkBlockTypeAliases(const ScopePtr& scope, std
{ {
if (const auto& typealias = stat->as<AstStatTypeAlias>()) if (const auto& typealias = stat->as<AstStatTypeAlias>())
{ {
if (FFlag::LuauTwoPassAliasDefinitionFix && typealias->name == Parser::errorName)
continue;
auto& bindings = typealias->exported ? scope->exportedTypeBindings : scope->privateTypeBindings; auto& bindings = typealias->exported ? scope->exportedTypeBindings : scope->privateTypeBindings;
Name name = typealias->name.value; Name name = typealias->name.value;
@ -1176,6 +1192,10 @@ void TypeChecker::check(const ScopePtr& scope, const AstStatTypeAlias& typealias
// Once with forwardDeclare, and once without. // Once with forwardDeclare, and once without.
Name name = typealias.name.value; Name name = typealias.name.value;
// If the alias is missing a name, we can't do anything with it. Ignore it.
if (FFlag::LuauTwoPassAliasDefinitionFix && name == Parser::errorName)
return;
std::optional<TypeFun> binding; std::optional<TypeFun> binding;
if (auto it = scope->exportedTypeBindings.find(name); it != scope->exportedTypeBindings.end()) if (auto it = scope->exportedTypeBindings.find(name); it != scope->exportedTypeBindings.end())
binding = it->second; binding = it->second;
@ -1192,6 +1212,8 @@ void TypeChecker::check(const ScopePtr& scope, const AstStatTypeAlias& typealias
reportError(TypeError{typealias.location, DuplicateTypeDefinition{name, location}}); reportError(TypeError{typealias.location, DuplicateTypeDefinition{name, location}});
bindingsMap[name] = TypeFun{binding->typeParams, binding->typePackParams, errorRecoveryType(anyType)}; bindingsMap[name] = TypeFun{binding->typeParams, binding->typePackParams, errorRecoveryType(anyType)};
if (FFlag::LuauTwoPassAliasDefinitionFix)
duplicateTypeAliases.insert({typealias.exported, name});
} }
else else
{ {
@ -1211,6 +1233,11 @@ void TypeChecker::check(const ScopePtr& scope, const AstStatTypeAlias& typealias
} }
else else
{ {
// If the first pass failed (this should mean a duplicate definition), the second pass isn't going to be
// interesting.
if (FFlag::LuauTwoPassAliasDefinitionFix && duplicateTypeAliases.find({typealias.exported, name}))
return;
if (!binding) if (!binding)
ice("Not predeclared"); ice("Not predeclared");
@ -1235,7 +1262,8 @@ void TypeChecker::check(const ScopePtr& scope, const AstStatTypeAlias& typealias
if (auto ttv = getMutable<TableTypeVar>(follow(ty))) if (auto ttv = getMutable<TableTypeVar>(follow(ty)))
{ {
// If the table is already named and we want to rename the type function, we have to bind new alias to a copy // If the table is already named and we want to rename the type function, we have to bind new alias to a copy
if (ttv->name) // Additionally, we can't modify types that come from other modules
if (ttv->name || (FFlag::LuauImmutableTypes && follow(ty)->owningArena != &currentModule->internalTypes))
{ {
bool sameTys = std::equal(ttv->instantiatedTypeParams.begin(), ttv->instantiatedTypeParams.end(), binding->typeParams.begin(), bool sameTys = std::equal(ttv->instantiatedTypeParams.begin(), ttv->instantiatedTypeParams.end(), binding->typeParams.begin(),
binding->typeParams.end(), [](auto&& itp, auto&& tp) { binding->typeParams.end(), [](auto&& itp, auto&& tp) {
@ -1247,7 +1275,7 @@ void TypeChecker::check(const ScopePtr& scope, const AstStatTypeAlias& typealias
}); });
// Copy can be skipped if this is an identical alias // Copy can be skipped if this is an identical alias
if (ttv->name != name || !sameTys || !sameTps) if ((FFlag::LuauImmutableTypes && !ttv->name) || ttv->name != name || !sameTys || !sameTps)
{ {
// This is a shallow clone, original recursive links to self are not updated // This is a shallow clone, original recursive links to self are not updated
TableTypeVar clone = TableTypeVar{ttv->props, ttv->indexer, ttv->level, ttv->state}; TableTypeVar clone = TableTypeVar{ttv->props, ttv->indexer, ttv->level, ttv->state};
@ -1279,9 +1307,17 @@ void TypeChecker::check(const ScopePtr& scope, const AstStatTypeAlias& typealias
} }
} }
else if (auto mtv = getMutable<MetatableTypeVar>(follow(ty))) else if (auto mtv = getMutable<MetatableTypeVar>(follow(ty)))
mtv->syntheticName = name; {
// We can't modify types that come from other modules
if (!FFlag::LuauImmutableTypes || follow(ty)->owningArena == &currentModule->internalTypes)
mtv->syntheticName = name;
}
unify(ty, bindingsMap[name].type, typealias.location); TypeId& bindingType = bindingsMap[name].type;
bool ok = unify(ty, bindingType, typealias.location);
if (FFlag::LuauTwoPassAliasDefinitionFix && ok)
bindingType = ty;
} }
} }
@ -1564,7 +1600,12 @@ ExprResult<TypeId> TypeChecker::checkExpr(const ScopePtr& scope, const AstExprCa
else if (auto vtp = get<VariadicTypePack>(retPack)) else if (auto vtp = get<VariadicTypePack>(retPack))
return {vtp->ty, std::move(result.predicates)}; return {vtp->ty, std::move(result.predicates)};
else if (get<Unifiable::Generic>(retPack)) else if (get<Unifiable::Generic>(retPack))
ice("Unexpected abstract type pack!", expr.location); {
if (FFlag::LuauReturnAnyInsteadOfICE)
return {anyType, std::move(result.predicates)};
else
ice("Unexpected abstract type pack!", expr.location);
}
else else
ice("Unknown TypePack type!", expr.location); ice("Unknown TypePack type!", expr.location);
} }
@ -1614,11 +1655,23 @@ std::optional<TypeId> TypeChecker::getIndexTypeFromType(
tablify(type); tablify(type);
const PrimitiveTypeVar* primitiveType = get<PrimitiveTypeVar>(type); if (FFlag::LuauDiscriminableUnions2)
if (primitiveType && primitiveType->type == PrimitiveTypeVar::String)
{ {
if (std::optional<TypeId> mtIndex = findMetatableEntry(type, "__index", location)) if (isString(type))
{
std::optional<TypeId> mtIndex = findMetatableEntry(stringType, "__index", location);
LUAU_ASSERT(mtIndex);
type = *mtIndex; type = *mtIndex;
}
}
else
{
const PrimitiveTypeVar* primitiveType = get<PrimitiveTypeVar>(type);
if (primitiveType && primitiveType->type == PrimitiveTypeVar::String)
{
if (std::optional<TypeId> mtIndex = findMetatableEntry(type, "__index", location))
type = *mtIndex;
}
} }
if (TableTypeVar* tableType = getMutableTableType(type)) if (TableTypeVar* tableType = getMutableTableType(type))
@ -2476,7 +2529,7 @@ ExprResult<TypeId> TypeChecker::checkExpr(const ScopePtr& scope, const AstExprBi
auto [rhsTy, rhsPredicates] = checkExpr(innerScope, *expr.right); auto [rhsTy, rhsPredicates] = checkExpr(innerScope, *expr.right);
return {checkBinaryOperation(FFlag::LuauDiscriminableUnions ? scope : innerScope, expr, lhsTy, rhsTy), return {checkBinaryOperation(FFlag::LuauDiscriminableUnions2 ? scope : innerScope, expr, lhsTy, rhsTy),
{AndPredicate{std::move(lhsPredicates), std::move(rhsPredicates)}}}; {AndPredicate{std::move(lhsPredicates), std::move(rhsPredicates)}}};
} }
else if (expr.op == AstExprBinary::Or) else if (expr.op == AstExprBinary::Or)
@ -2489,7 +2542,7 @@ ExprResult<TypeId> TypeChecker::checkExpr(const ScopePtr& scope, const AstExprBi
auto [rhsTy, rhsPredicates] = checkExpr(innerScope, *expr.right); auto [rhsTy, rhsPredicates] = checkExpr(innerScope, *expr.right);
// Because of C++, I'm not sure if lhsPredicates was not moved out by the time we call checkBinaryOperation. // Because of C++, I'm not sure if lhsPredicates was not moved out by the time we call checkBinaryOperation.
TypeId result = checkBinaryOperation(FFlag::LuauDiscriminableUnions ? scope : innerScope, expr, lhsTy, rhsTy, lhsPredicates); TypeId result = checkBinaryOperation(FFlag::LuauDiscriminableUnions2 ? scope : innerScope, expr, lhsTy, rhsTy, lhsPredicates);
return {result, {OrPredicate{std::move(lhsPredicates), std::move(rhsPredicates)}}}; return {result, {OrPredicate{std::move(lhsPredicates), std::move(rhsPredicates)}}};
} }
else if (expr.op == AstExprBinary::CompareEq || expr.op == AstExprBinary::CompareNe) else if (expr.op == AstExprBinary::CompareEq || expr.op == AstExprBinary::CompareNe)
@ -2497,8 +2550,8 @@ ExprResult<TypeId> TypeChecker::checkExpr(const ScopePtr& scope, const AstExprBi
if (auto predicate = tryGetTypeGuardPredicate(expr)) if (auto predicate = tryGetTypeGuardPredicate(expr))
return {booleanType, {std::move(*predicate)}}; return {booleanType, {std::move(*predicate)}};
ExprResult<TypeId> lhs = checkExpr(scope, *expr.left, std::nullopt, /*forceSingleton=*/FFlag::LuauDiscriminableUnions); ExprResult<TypeId> lhs = checkExpr(scope, *expr.left, std::nullopt, /*forceSingleton=*/FFlag::LuauDiscriminableUnions2);
ExprResult<TypeId> rhs = checkExpr(scope, *expr.right, std::nullopt, /*forceSingleton=*/FFlag::LuauDiscriminableUnions); ExprResult<TypeId> rhs = checkExpr(scope, *expr.right, std::nullopt, /*forceSingleton=*/FFlag::LuauDiscriminableUnions2);
PredicateVec predicates; PredicateVec predicates;
@ -2785,12 +2838,16 @@ TypeId TypeChecker::checkLValueBinding(const ScopePtr& scope, const AstExprIndex
} }
else if (exprTable->state == TableState::Unsealed || exprTable->state == TableState::Free) else if (exprTable->state == TableState::Unsealed || exprTable->state == TableState::Free)
{ {
TypeId resultType = freshType(scope); TypeId resultType = freshType(FFlag::LuauAnotherTypeLevelFix ? exprTable->level : scope->level);
exprTable->indexer = TableIndexer{anyIfNonstrict(indexType), anyIfNonstrict(resultType)}; exprTable->indexer = TableIndexer{anyIfNonstrict(indexType), anyIfNonstrict(resultType)};
return resultType; return resultType;
} }
else else
{ {
/*
* If we use [] indexing to fetch a property from a sealed table that has no indexer, we have no idea if it will
* work, so we just mint a fresh type, return that, and hope for the best.
*/
TypeId resultType = freshType(scope); TypeId resultType = freshType(scope);
return resultType; return resultType;
} }
@ -4195,6 +4252,9 @@ TypeId TypeChecker::checkRequire(const ScopePtr& scope, const ModuleInfo& module
return errorRecoveryType(scope); return errorRecoveryType(scope);
} }
if (FFlag::LuauImmutableTypes)
return *moduleType;
SeenTypes seenTypes; SeenTypes seenTypes;
SeenTypePacks seenTypePacks; SeenTypePacks seenTypePacks;
CloneState cloneState; CloneState cloneState;
@ -5446,7 +5506,7 @@ GenericTypeDefinitions TypeChecker::createGenericTypes(const ScopePtr& scope, st
void TypeChecker::refineLValue(const LValue& lvalue, RefinementMap& refis, const ScopePtr& scope, TypeIdPredicate predicate) void TypeChecker::refineLValue(const LValue& lvalue, RefinementMap& refis, const ScopePtr& scope, TypeIdPredicate predicate)
{ {
LUAU_ASSERT(FFlag::LuauDiscriminableUnions); LUAU_ASSERT(FFlag::LuauDiscriminableUnions2);
const LValue* target = &lvalue; const LValue* target = &lvalue;
std::optional<LValue> key; // If set, we know we took the base of the lvalue path and should be walking down each option of the base's type. std::optional<LValue> key; // If set, we know we took the base of the lvalue path and should be walking down each option of the base's type.
@ -5659,7 +5719,7 @@ void TypeChecker::resolve(const TruthyPredicate& truthyP, ErrorVec& errVec, Refi
return std::nullopt; return std::nullopt;
}; };
if (FFlag::LuauDiscriminableUnions) if (FFlag::LuauDiscriminableUnions2)
{ {
std::optional<TypeId> ty = resolveLValue(refis, scope, truthyP.lvalue); std::optional<TypeId> ty = resolveLValue(refis, scope, truthyP.lvalue);
if (ty && fromOr) if (ty && fromOr)
@ -5772,7 +5832,7 @@ void TypeChecker::resolve(const IsAPredicate& isaP, ErrorVec& errVec, Refinement
return res; return res;
}; };
if (FFlag::LuauDiscriminableUnions) if (FFlag::LuauDiscriminableUnions2)
{ {
refineLValue(isaP.lvalue, refis, scope, predicate); refineLValue(isaP.lvalue, refis, scope, predicate);
} }
@ -5847,7 +5907,7 @@ void TypeChecker::resolve(const TypeGuardPredicate& typeguardP, ErrorVec& errVec
if (auto it = primitives.find(typeguardP.kind); it != primitives.end()) if (auto it = primitives.find(typeguardP.kind); it != primitives.end())
{ {
if (FFlag::LuauDiscriminableUnions) if (FFlag::LuauDiscriminableUnions2)
{ {
refineLValue(typeguardP.lvalue, refis, scope, it->second(sense)); refineLValue(typeguardP.lvalue, refis, scope, it->second(sense));
return; return;
@ -5869,7 +5929,7 @@ void TypeChecker::resolve(const TypeGuardPredicate& typeguardP, ErrorVec& errVec
} }
auto fail = [&](const TypeErrorData& err) { auto fail = [&](const TypeErrorData& err) {
if (!FFlag::LuauDiscriminableUnions) if (!FFlag::LuauDiscriminableUnions2)
errVec.push_back(TypeError{typeguardP.location, err}); errVec.push_back(TypeError{typeguardP.location, err});
addRefinement(refis, typeguardP.lvalue, errorRecoveryType(scope)); addRefinement(refis, typeguardP.lvalue, errorRecoveryType(scope));
}; };
@ -5901,7 +5961,7 @@ void TypeChecker::resolve(const EqPredicate& eqP, ErrorVec& errVec, RefinementMa
return {ty}; return {ty};
}; };
if (FFlag::LuauDiscriminableUnions) if (FFlag::LuauDiscriminableUnions2)
{ {
std::vector<TypeId> rhs = options(eqP.type); std::vector<TypeId> rhs = options(eqP.type);

View File

@ -28,6 +28,7 @@ LUAU_FASTFLAGVARIABLE(LuauMetatableAreEqualRecursion, false)
LUAU_FASTFLAGVARIABLE(LuauRefactorTypeVarQuestions, false) LUAU_FASTFLAGVARIABLE(LuauRefactorTypeVarQuestions, false)
LUAU_FASTFLAG(LuauErrorRecoveryType) LUAU_FASTFLAG(LuauErrorRecoveryType)
LUAU_FASTFLAG(LuauUnionTagMatchFix) LUAU_FASTFLAG(LuauUnionTagMatchFix)
LUAU_FASTFLAG(LuauDiscriminableUnions2)
namespace Luau namespace Luau
{ {
@ -393,7 +394,8 @@ bool hasLength(TypeId ty, DenseHashSet<TypeId>& seen, int* recursionCount)
if (seen.contains(ty)) if (seen.contains(ty))
return true; return true;
if (isPrim(ty, PrimitiveTypeVar::String) || get<AnyTypeVar>(ty) || get<TableTypeVar>(ty) || get<MetatableTypeVar>(ty)) bool isStr = FFlag::LuauDiscriminableUnions2 ? isString(ty) : isPrim(ty, PrimitiveTypeVar::String);
if (isStr || get<AnyTypeVar>(ty) || get<TableTypeVar>(ty) || get<MetatableTypeVar>(ty))
return true; return true;
if (auto uty = get<UnionTypeVar>(ty)) if (auto uty = get<UnionTypeVar>(ty))

View File

@ -15,6 +15,7 @@
LUAU_FASTINT(LuauTypeInferRecursionLimit); LUAU_FASTINT(LuauTypeInferRecursionLimit);
LUAU_FASTINT(LuauTypeInferTypePackLoopLimit); LUAU_FASTINT(LuauTypeInferTypePackLoopLimit);
LUAU_FASTFLAGVARIABLE(LuauCommittingTxnLogFreeTpPromote, false) LUAU_FASTFLAGVARIABLE(LuauCommittingTxnLogFreeTpPromote, false)
LUAU_FASTFLAG(LuauImmutableTypes)
LUAU_FASTFLAG(LuauUseCommittingTxnLog) LUAU_FASTFLAG(LuauUseCommittingTxnLog)
LUAU_FASTINTVARIABLE(LuauTypeInferIterationLimit, 2000); LUAU_FASTINTVARIABLE(LuauTypeInferIterationLimit, 2000);
LUAU_FASTFLAGVARIABLE(LuauTableSubtypingVariance2, false); LUAU_FASTFLAGVARIABLE(LuauTableSubtypingVariance2, false);
@ -24,6 +25,7 @@ LUAU_FASTFLAG(LuauErrorRecoveryType);
LUAU_FASTFLAG(LuauProperTypeLevels); LUAU_FASTFLAG(LuauProperTypeLevels);
LUAU_FASTFLAGVARIABLE(LuauUnifyPackTails, false) LUAU_FASTFLAGVARIABLE(LuauUnifyPackTails, false)
LUAU_FASTFLAGVARIABLE(LuauUnionTagMatchFix, false) LUAU_FASTFLAGVARIABLE(LuauUnionTagMatchFix, false)
LUAU_FASTFLAGVARIABLE(LuauFollowWithCommittingTxnLogInAnyUnification, false)
namespace Luau namespace Luau
{ {
@ -32,11 +34,13 @@ struct PromoteTypeLevels
{ {
DEPRECATED_TxnLog& DEPRECATED_log; DEPRECATED_TxnLog& DEPRECATED_log;
TxnLog& log; TxnLog& log;
const TypeArena* typeArena = nullptr;
TypeLevel minLevel; TypeLevel minLevel;
explicit PromoteTypeLevels(DEPRECATED_TxnLog& DEPRECATED_log, TxnLog& log, TypeLevel minLevel) explicit PromoteTypeLevels(DEPRECATED_TxnLog& DEPRECATED_log, TxnLog& log, const TypeArena* typeArena, TypeLevel minLevel)
: DEPRECATED_log(DEPRECATED_log) : DEPRECATED_log(DEPRECATED_log)
, log(log) , log(log)
, typeArena(typeArena)
, minLevel(minLevel) , minLevel(minLevel)
{ {
} }
@ -65,8 +69,12 @@ struct PromoteTypeLevels
} }
template<typename TID, typename T> template<typename TID, typename T>
bool operator()(TID, const T&) bool operator()(TID ty, const T&)
{ {
// Type levels of types from other modules are already global, so we don't need to promote anything inside
if (FFlag::LuauImmutableTypes && ty->owningArena != typeArena)
return false;
return true; return true;
} }
@ -83,12 +91,20 @@ struct PromoteTypeLevels
bool operator()(TypeId ty, const FunctionTypeVar&) bool operator()(TypeId ty, const FunctionTypeVar&)
{ {
// Type levels of types from other modules are already global, so we don't need to promote anything inside
if (FFlag::LuauImmutableTypes && ty->owningArena != typeArena)
return false;
promote(ty, FFlag::LuauUseCommittingTxnLog ? log.getMutable<FunctionTypeVar>(ty) : getMutable<FunctionTypeVar>(ty)); promote(ty, FFlag::LuauUseCommittingTxnLog ? log.getMutable<FunctionTypeVar>(ty) : getMutable<FunctionTypeVar>(ty));
return true; return true;
} }
bool operator()(TypeId ty, const TableTypeVar& ttv) bool operator()(TypeId ty, const TableTypeVar& ttv)
{ {
// Type levels of types from other modules are already global, so we don't need to promote anything inside
if (FFlag::LuauImmutableTypes && ty->owningArena != typeArena)
return false;
if (ttv.state != TableState::Free && ttv.state != TableState::Generic) if (ttv.state != TableState::Free && ttv.state != TableState::Generic)
return true; return true;
@ -108,24 +124,33 @@ struct PromoteTypeLevels
} }
}; };
void promoteTypeLevels(DEPRECATED_TxnLog& DEPRECATED_log, TxnLog& log, TypeLevel minLevel, TypeId ty) void promoteTypeLevels(DEPRECATED_TxnLog& DEPRECATED_log, TxnLog& log, const TypeArena* typeArena, TypeLevel minLevel, TypeId ty)
{ {
PromoteTypeLevels ptl{DEPRECATED_log, log, minLevel}; // Type levels of types from other modules are already global, so we don't need to promote anything inside
if (FFlag::LuauImmutableTypes && ty->owningArena != typeArena)
return;
PromoteTypeLevels ptl{DEPRECATED_log, log, typeArena, minLevel};
DenseHashSet<void*> seen{nullptr}; DenseHashSet<void*> seen{nullptr};
visitTypeVarOnce(ty, ptl, seen); visitTypeVarOnce(ty, ptl, seen);
} }
void promoteTypeLevels(DEPRECATED_TxnLog& DEPRECATED_log, TxnLog& log, TypeLevel minLevel, TypePackId tp) void promoteTypeLevels(DEPRECATED_TxnLog& DEPRECATED_log, TxnLog& log, const TypeArena* typeArena, TypeLevel minLevel, TypePackId tp)
{ {
PromoteTypeLevels ptl{DEPRECATED_log, log, minLevel}; // Type levels of types from other modules are already global, so we don't need to promote anything inside
if (FFlag::LuauImmutableTypes && tp->owningArena != typeArena)
return;
PromoteTypeLevels ptl{DEPRECATED_log, log, typeArena, minLevel};
DenseHashSet<void*> seen{nullptr}; DenseHashSet<void*> seen{nullptr};
visitTypeVarOnce(tp, ptl, seen); visitTypeVarOnce(tp, ptl, seen);
} }
struct SkipCacheForType struct SkipCacheForType
{ {
SkipCacheForType(const DenseHashMap<TypeId, bool>& skipCacheForType) SkipCacheForType(const DenseHashMap<TypeId, bool>& skipCacheForType, const TypeArena* typeArena)
: skipCacheForType(skipCacheForType) : skipCacheForType(skipCacheForType)
, typeArena(typeArena)
{ {
} }
@ -152,6 +177,10 @@ struct SkipCacheForType
bool operator()(TypeId ty, const TableTypeVar&) bool operator()(TypeId ty, const TableTypeVar&)
{ {
// Types from other modules don't contain mutable elements and are ok to cache
if (FFlag::LuauImmutableTypes && ty->owningArena != typeArena)
return false;
TableTypeVar& ttv = *getMutable<TableTypeVar>(ty); TableTypeVar& ttv = *getMutable<TableTypeVar>(ty);
if (ttv.boundTo) if (ttv.boundTo)
@ -172,6 +201,10 @@ struct SkipCacheForType
template<typename T> template<typename T>
bool operator()(TypeId ty, const T& t) bool operator()(TypeId ty, const T& t)
{ {
// Types from other modules don't contain mutable elements and are ok to cache
if (FFlag::LuauImmutableTypes && ty->owningArena != typeArena)
return false;
const bool* prev = skipCacheForType.find(ty); const bool* prev = skipCacheForType.find(ty);
if (prev && *prev) if (prev && *prev)
@ -184,8 +217,12 @@ struct SkipCacheForType
} }
template<typename T> template<typename T>
bool operator()(TypePackId, const T&) bool operator()(TypePackId tp, const T&)
{ {
// Types from other modules don't contain mutable elements and are ok to cache
if (FFlag::LuauImmutableTypes && tp->owningArena != typeArena)
return false;
return true; return true;
} }
@ -208,6 +245,7 @@ struct SkipCacheForType
} }
const DenseHashMap<TypeId, bool>& skipCacheForType; const DenseHashMap<TypeId, bool>& skipCacheForType;
const TypeArena* typeArena = nullptr;
bool result = false; bool result = false;
}; };
@ -422,13 +460,13 @@ void Unifier::tryUnify_(TypeId subTy, TypeId superTy, bool isFunctionCall, bool
{ {
if (FFlag::LuauUseCommittingTxnLog) if (FFlag::LuauUseCommittingTxnLog)
{ {
promoteTypeLevels(DEPRECATED_log, log, superLevel, subTy); promoteTypeLevels(DEPRECATED_log, log, types, superLevel, subTy);
log.replace(superTy, BoundTypeVar(subTy)); log.replace(superTy, BoundTypeVar(subTy));
} }
else else
{ {
if (FFlag::LuauProperTypeLevels) if (FFlag::LuauProperTypeLevels)
promoteTypeLevels(DEPRECATED_log, log, superLevel, subTy); promoteTypeLevels(DEPRECATED_log, log, types, superLevel, subTy);
else if (auto subLevel = getMutableLevel(subTy)) else if (auto subLevel = getMutableLevel(subTy))
{ {
if (!subLevel->subsumes(superFree->level)) if (!subLevel->subsumes(superFree->level))
@ -466,13 +504,13 @@ void Unifier::tryUnify_(TypeId subTy, TypeId superTy, bool isFunctionCall, bool
{ {
if (FFlag::LuauUseCommittingTxnLog) if (FFlag::LuauUseCommittingTxnLog)
{ {
promoteTypeLevels(DEPRECATED_log, log, subLevel, superTy); promoteTypeLevels(DEPRECATED_log, log, types, subLevel, superTy);
log.replace(subTy, BoundTypeVar(superTy)); log.replace(subTy, BoundTypeVar(superTy));
} }
else else
{ {
if (FFlag::LuauProperTypeLevels) if (FFlag::LuauProperTypeLevels)
promoteTypeLevels(DEPRECATED_log, log, subLevel, superTy); promoteTypeLevels(DEPRECATED_log, log, types, subLevel, superTy);
else if (auto superLevel = getMutableLevel(superTy)) else if (auto superLevel = getMutableLevel(superTy))
{ {
if (!superLevel->subsumes(subFree->level)) if (!superLevel->subsumes(subFree->level))
@ -849,7 +887,7 @@ void Unifier::cacheResult(TypeId subTy, TypeId superTy)
return; return;
auto skipCacheFor = [this](TypeId ty) { auto skipCacheFor = [this](TypeId ty) {
SkipCacheForType visitor{sharedState.skipCacheForType}; SkipCacheForType visitor{sharedState.skipCacheForType, types};
visitTypeVarOnce(ty, visitor, sharedState.seenAny); visitTypeVarOnce(ty, visitor, sharedState.seenAny);
sharedState.skipCacheForType[ty] = visitor.result; sharedState.skipCacheForType[ty] = visitor.result;
@ -1637,32 +1675,35 @@ void Unifier::tryUnifyFunctions(TypeId subTy, TypeId superTy, bool isFunctionCal
tryUnify_(subFunction->retType, superFunction->retType); tryUnify_(subFunction->retType, superFunction->retType);
} }
if (FFlag::LuauUseCommittingTxnLog) if (!FFlag::LuauImmutableTypes)
{ {
if (superFunction->definition && !subFunction->definition && !subTy->persistent) if (FFlag::LuauUseCommittingTxnLog)
{ {
PendingType* newSubTy = log.queue(subTy); if (superFunction->definition && !subFunction->definition && !subTy->persistent)
FunctionTypeVar* newSubFtv = getMutable<FunctionTypeVar>(newSubTy); {
LUAU_ASSERT(newSubFtv); PendingType* newSubTy = log.queue(subTy);
newSubFtv->definition = superFunction->definition; FunctionTypeVar* newSubFtv = getMutable<FunctionTypeVar>(newSubTy);
LUAU_ASSERT(newSubFtv);
newSubFtv->definition = superFunction->definition;
}
else if (!superFunction->definition && subFunction->definition && !superTy->persistent)
{
PendingType* newSuperTy = log.queue(superTy);
FunctionTypeVar* newSuperFtv = getMutable<FunctionTypeVar>(newSuperTy);
LUAU_ASSERT(newSuperFtv);
newSuperFtv->definition = subFunction->definition;
}
} }
else if (!superFunction->definition && subFunction->definition && !superTy->persistent) else
{ {
PendingType* newSuperTy = log.queue(superTy); if (superFunction->definition && !subFunction->definition && !subTy->persistent)
FunctionTypeVar* newSuperFtv = getMutable<FunctionTypeVar>(newSuperTy); {
LUAU_ASSERT(newSuperFtv); subFunction->definition = superFunction->definition;
newSuperFtv->definition = subFunction->definition; }
} else if (!superFunction->definition && subFunction->definition && !superTy->persistent)
} {
else superFunction->definition = subFunction->definition;
{ }
if (superFunction->definition && !subFunction->definition && !subTy->persistent)
{
subFunction->definition = superFunction->definition;
}
else if (!superFunction->definition && subFunction->definition && !superTy->persistent)
{
superFunction->definition = subFunction->definition;
} }
} }
@ -2631,7 +2672,7 @@ static void queueTypePack(std::vector<TypeId>& queue, DenseHashSet<TypePackId>&
{ {
while (true) while (true)
{ {
a = follow(a); a = FFlag::LuauFollowWithCommittingTxnLogInAnyUnification ? state.log.follow(a) : follow(a);
if (seenTypePacks.find(a)) if (seenTypePacks.find(a))
break; break;
@ -2738,7 +2779,7 @@ void Unifier::tryUnifyVariadics(TypePackId subTp, TypePackId superTp, bool rever
} }
static void tryUnifyWithAny(std::vector<TypeId>& queue, Unifier& state, DenseHashSet<TypeId>& seen, DenseHashSet<TypePackId>& seenTypePacks, static void tryUnifyWithAny(std::vector<TypeId>& queue, Unifier& state, DenseHashSet<TypeId>& seen, DenseHashSet<TypePackId>& seenTypePacks,
TypeId anyType, TypePackId anyTypePack) const TypeArena* typeArena, TypeId anyType, TypePackId anyTypePack)
{ {
while (!queue.empty()) while (!queue.empty())
{ {
@ -2746,8 +2787,14 @@ static void tryUnifyWithAny(std::vector<TypeId>& queue, Unifier& state, DenseHas
{ {
TypeId ty = state.log.follow(queue.back()); TypeId ty = state.log.follow(queue.back());
queue.pop_back(); queue.pop_back();
// Types from other modules don't have free types
if (FFlag::LuauImmutableTypes && ty->owningArena != typeArena)
continue;
if (seen.find(ty)) if (seen.find(ty))
continue; continue;
seen.insert(ty); seen.insert(ty);
if (state.log.getMutable<FreeTypeVar>(ty)) if (state.log.getMutable<FreeTypeVar>(ty))
@ -2853,7 +2900,7 @@ void Unifier::tryUnifyWithAny(TypeId subTy, TypeId anyTy)
sharedState.tempSeenTy.clear(); sharedState.tempSeenTy.clear();
sharedState.tempSeenTp.clear(); sharedState.tempSeenTp.clear();
Luau::tryUnifyWithAny(queue, *this, sharedState.tempSeenTy, sharedState.tempSeenTp, getSingletonTypes().anyType, anyTP); Luau::tryUnifyWithAny(queue, *this, sharedState.tempSeenTy, sharedState.tempSeenTp, types, getSingletonTypes().anyType, anyTP);
} }
void Unifier::tryUnifyWithAny(TypePackId subTy, TypePackId anyTp) void Unifier::tryUnifyWithAny(TypePackId subTy, TypePackId anyTp)
@ -2869,7 +2916,7 @@ void Unifier::tryUnifyWithAny(TypePackId subTy, TypePackId anyTp)
queueTypePack(queue, sharedState.tempSeenTp, *this, subTy, anyTp); queueTypePack(queue, sharedState.tempSeenTp, *this, subTy, anyTp);
Luau::tryUnifyWithAny(queue, *this, sharedState.tempSeenTy, sharedState.tempSeenTp, anyTy, anyTp); Luau::tryUnifyWithAny(queue, *this, sharedState.tempSeenTy, sharedState.tempSeenTp, types, anyTy, anyTp);
} }
std::optional<TypeId> Unifier::findTablePropertyRespectingMeta(TypeId lhsType, Name name) std::optional<TypeId> Unifier::findTablePropertyRespectingMeta(TypeId lhsType, Name name)

View File

@ -1133,7 +1133,7 @@ AstTypePack* Parser::parseTypeList(TempVector<AstType*>& result, TempVector<std:
resultNames.push_back({}); resultNames.push_back({});
resultNames.push_back(AstArgumentName{AstName(lexer.current().name), lexer.current().location}); resultNames.push_back(AstArgumentName{AstName(lexer.current().name), lexer.current().location});
lexer.next(); nextLexeme();
expectAndConsume(':'); expectAndConsume(':');
} }

View File

@ -1,4 +1,6 @@
// This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details // This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details
#include "Repl.h"
#include "lua.h" #include "lua.h"
#include "lualib.h" #include "lualib.h"
@ -38,13 +40,14 @@ enum class CompileFormat
struct GlobalOptions struct GlobalOptions
{ {
int optimizationLevel = 1; int optimizationLevel = 1;
int debugLevel = 1;
} globalOptions; } globalOptions;
static Luau::CompileOptions copts() static Luau::CompileOptions copts()
{ {
Luau::CompileOptions result = {}; Luau::CompileOptions result = {};
result.optimizationLevel = globalOptions.optimizationLevel; result.optimizationLevel = globalOptions.optimizationLevel;
result.debugLevel = 1; result.debugLevel = globalOptions.debugLevel;
result.coverageLevel = coverageActive() ? 2 : 0; result.coverageLevel = coverageActive() ? 2 : 0;
return result; return result;
@ -240,9 +243,8 @@ std::string runCode(lua_State* L, const std::string& source)
return std::string(); return std::string();
} }
static void completeIndexer(ic_completion_env_t* cenv, const char* editBuffer) static void completeIndexer(lua_State* L, const std::string& editBuffer, const AddCompletionCallback& addCompletionCallback)
{ {
auto* L = reinterpret_cast<lua_State*>(ic_completion_arg(cenv));
std::string_view lookup = editBuffer; std::string_view lookup = editBuffer;
char lastSep = 0; char lastSep = 0;
@ -276,7 +278,7 @@ static void completeIndexer(ic_completion_env_t* cenv, const char* editBuffer)
// Add an opening paren for function calls by default. // Add an opening paren for function calls by default.
completion += "("; completion += "(";
} }
ic_add_completion_ex(cenv, completion.data(), key.data(), nullptr); addCompletionCallback(completion, std::string(key));
} }
} }
lua_pop(L, 1); lua_pop(L, 1);
@ -295,10 +297,11 @@ static void completeIndexer(ic_completion_env_t* cenv, const char* editBuffer)
{ {
// Replace the string object with the string class to perform further lookups of string functions // Replace the string object with the string class to perform further lookups of string functions
// Note: We retrieve the string class from _G to prevent issues if the user assigns to `string`. // Note: We retrieve the string class from _G to prevent issues if the user assigns to `string`.
lua_pop(L, 1); // Pop the string instance
lua_getglobal(L, "_G"); lua_getglobal(L, "_G");
lua_pushlstring(L, "string", 6); lua_pushlstring(L, "string", 6);
lua_rawget(L, -2); lua_rawget(L, -2);
lua_remove(L, -2); lua_remove(L, -2); // Remove the global table
LUAU_ASSERT(lua_istable(L, -1)); LUAU_ASSERT(lua_istable(L, -1));
} }
else if (!lua_istable(L, -1)) else if (!lua_istable(L, -1))
@ -312,6 +315,26 @@ static void completeIndexer(ic_completion_env_t* cenv, const char* editBuffer)
lua_pop(L, 1); lua_pop(L, 1);
} }
void getCompletions(lua_State* L, const std::string& editBuffer, const AddCompletionCallback& addCompletionCallback)
{
// look the value up in current global table first
lua_pushvalue(L, LUA_GLOBALSINDEX);
completeIndexer(L, editBuffer, addCompletionCallback);
// and in actual global table after that
lua_getglobal(L, "_G");
completeIndexer(L, editBuffer, addCompletionCallback);
}
static void icGetCompletions(ic_completion_env_t* cenv, const char* editBuffer)
{
auto* L = reinterpret_cast<lua_State*>(ic_completion_arg(cenv));
getCompletions(L, std::string(editBuffer), [cenv](const std::string& completion, const std::string& display) {
ic_add_completion_ex(cenv, completion.data(), display.data(), nullptr);
});
}
static bool isMethodOrFunctionChar(const char* s, long len) static bool isMethodOrFunctionChar(const char* s, long len)
{ {
char c = *s; char c = *s;
@ -320,15 +343,7 @@ static bool isMethodOrFunctionChar(const char* s, long len)
static void completeRepl(ic_completion_env_t* cenv, const char* editBuffer) static void completeRepl(ic_completion_env_t* cenv, const char* editBuffer)
{ {
auto* L = reinterpret_cast<lua_State*>(ic_completion_arg(cenv)); ic_complete_word(cenv, editBuffer, icGetCompletions, isMethodOrFunctionChar);
// look the value up in current global table first
lua_pushvalue(L, LUA_GLOBALSINDEX);
ic_complete_word(cenv, editBuffer, completeIndexer, isMethodOrFunctionChar);
// and in actual global table after that
lua_getglobal(L, "_G");
ic_complete_word(cenv, editBuffer, completeIndexer, isMethodOrFunctionChar);
} }
struct LinenoiseScopedHistory struct LinenoiseScopedHistory
@ -372,19 +387,20 @@ static void runReplImpl(lua_State* L)
for (;;) for (;;)
{ {
const char* line = ic_readline(buffer.empty() ? "" : ">"); const char* prompt = buffer.empty() ? "" : ">";
std::unique_ptr<char, void (*)(void*)> line(ic_readline(prompt), free);
if (!line) if (!line)
break; break;
if (buffer.empty() && runCode(L, std::string("return ") + line) == std::string()) if (buffer.empty() && runCode(L, std::string("return ") + line.get()) == std::string())
{ {
ic_history_add(line); ic_history_add(line.get());
continue; continue;
} }
if (!buffer.empty()) if (!buffer.empty())
buffer += "\n"; buffer += "\n";
buffer += line; buffer += line.get();
std::string error = runCode(L, buffer); std::string error = runCode(L, buffer);
@ -400,7 +416,6 @@ static void runReplImpl(lua_State* L)
ic_history_add(buffer.c_str()); ic_history_add(buffer.c_str());
buffer.clear(); buffer.clear();
free((void*)line);
} }
} }
@ -504,7 +519,7 @@ static bool compileFile(const char* name, CompileFormat format)
if (format == CompileFormat::Text) if (format == CompileFormat::Text)
{ {
bcb.setDumpFlags(Luau::BytecodeBuilder::Dump_Code | Luau::BytecodeBuilder::Dump_Source); bcb.setDumpFlags(Luau::BytecodeBuilder::Dump_Code | Luau::BytecodeBuilder::Dump_Source | Luau::BytecodeBuilder::Dump_Locals);
bcb.setDumpSource(*source); bcb.setDumpSource(*source);
} }
@ -549,7 +564,8 @@ static void displayHelp(const char* argv0)
printf(" --coverage: collect code coverage while running the code and output results to coverage.out\n"); printf(" --coverage: collect code coverage while running the code and output results to coverage.out\n");
printf(" -h, --help: Display this usage message.\n"); printf(" -h, --help: Display this usage message.\n");
printf(" -i, --interactive: Run an interactive REPL after executing the last script specified.\n"); printf(" -i, --interactive: Run an interactive REPL after executing the last script specified.\n");
printf(" -O<n>: use compiler optimization level (n=0-2).\n"); printf(" -O<n>: compile with optimization level n (default 1, n should be between 0 and 2).\n");
printf(" -g<n>: compile with debug level n (default 1, n should be between 0 and 2).\n");
printf(" --profile[=N]: profile the code using N Hz sampling (default 10000) and output results to profile.out\n"); printf(" --profile[=N]: profile the code using N Hz sampling (default 10000) and output results to profile.out\n");
printf(" --timetrace: record compiler time tracing information into trace.json\n"); printf(" --timetrace: record compiler time tracing information into trace.json\n");
} }
@ -620,6 +636,16 @@ int replMain(int argc, char** argv)
} }
globalOptions.optimizationLevel = level; globalOptions.optimizationLevel = level;
} }
else if (strncmp(argv[i], "-g", 2) == 0)
{
int level = atoi(argv[i] + 2);
if (level < 0 || level > 2)
{
fprintf(stderr, "Error: Debug level must be between 0 and 2 inclusive.\n");
return 1;
}
globalOptions.debugLevel = level;
}
else if (strcmp(argv[i], "--profile") == 0) else if (strcmp(argv[i], "--profile") == 0)
{ {
profile = 10000; // default to 10 KHz profile = 10000; // default to 10 KHz

View File

@ -3,10 +3,15 @@
#include "lua.h" #include "lua.h"
#include <functional>
#include <string> #include <string>
using AddCompletionCallback = std::function<void(const std::string& completion, const std::string& display)>;
// Note: These are internal functions which are being exposed in a header // Note: These are internal functions which are being exposed in a header
// so they can be included by unit tests. // so they can be included by unit tests.
int replMain(int argc, char** argv);
void setupState(lua_State* L); void setupState(lua_State* L);
std::string runCode(lua_State* L, const std::string& source); std::string runCode(lua_State* L, const std::string& source);
void getCompletions(lua_State* L, const std::string& editBuffer, const AddCompletionCallback& addCompletionCallback);
int replMain(int argc, char** argv);

View File

@ -6,8 +6,6 @@
#include <algorithm> #include <algorithm>
#include <string.h> #include <string.h>
LUAU_FASTFLAGVARIABLE(LuauBytecodeV2Write, false)
namespace Luau namespace Luau
{ {
@ -510,7 +508,7 @@ uint32_t BytecodeBuilder::getDebugPC() const
void BytecodeBuilder::finalize() void BytecodeBuilder::finalize()
{ {
LUAU_ASSERT(bytecode.empty()); LUAU_ASSERT(bytecode.empty());
bytecode = char(FFlag::LuauBytecodeV2Write ? LBC_VERSION_FUTURE : LBC_VERSION); bytecode = char(LBC_VERSION_FUTURE);
writeStringTable(bytecode); writeStringTable(bytecode);
@ -611,9 +609,7 @@ void BytecodeBuilder::writeFunction(std::string& ss, uint32_t id) const
writeVarInt(ss, child); writeVarInt(ss, child);
// debug info // debug info
if (FFlag::LuauBytecodeV2Write) writeVarInt(ss, func.debuglinedefined);
writeVarInt(ss, func.debuglinedefined);
writeVarInt(ss, func.debugname); writeVarInt(ss, func.debugname);
bool hasLines = true; bool hasLines = true;

View File

@ -15,7 +15,6 @@
#include <bitset> #include <bitset>
#include <math.h> #include <math.h>
LUAU_FASTFLAGVARIABLE(LuauCompileTableIndexOpt, false)
LUAU_FASTFLAG(LuauCompileSelectBuiltin2) LUAU_FASTFLAG(LuauCompileSelectBuiltin2)
namespace Luau namespace Luau
@ -1182,18 +1181,9 @@ struct Compiler
const AstExprTable::Item& item = expr->items.data[i]; const AstExprTable::Item& item = expr->items.data[i];
LUAU_ASSERT(item.key); // no list portion => all items have keys LUAU_ASSERT(item.key); // no list portion => all items have keys
if (FFlag::LuauCompileTableIndexOpt) const Constant* ckey = constants.find(item.key);
{
const Constant* ckey = constants.find(item.key);
indexSize += (ckey && ckey->type == Constant::Type_Number && ckey->valueNumber == double(indexSize + 1)); indexSize += (ckey && ckey->type == Constant::Type_Number && ckey->valueNumber == double(indexSize + 1));
}
else
{
AstExprConstantNumber* ckey = item.key->as<AstExprConstantNumber>();
indexSize += (ckey && ckey->value == double(indexSize + 1));
}
} }
// we only perform the optimization if we don't have any other []-keys // we only perform the optimization if we don't have any other []-keys
@ -1295,43 +1285,10 @@ struct Compiler
{ {
RegScope rsi(this); RegScope rsi(this);
if (FFlag::LuauCompileTableIndexOpt) LValue lv = compileLValueIndex(reg, key, rsi);
{ uint8_t rv = compileExprAuto(value, rsi);
LValue lv = compileLValueIndex(reg, key, rsi);
uint8_t rv = compileExprAuto(value, rsi);
compileAssign(lv, rv); compileAssign(lv, rv);
}
else
{
// Optimization: use SETTABLEKS/SETTABLEN for literal keys, this happens often as part of usual table construction syntax
if (AstExprConstantString* ckey = key->as<AstExprConstantString>())
{
BytecodeBuilder::StringRef cname = sref(ckey->value);
int32_t cid = bytecode.addConstantString(cname);
if (cid < 0)
CompileError::raise(expr->location, "Exceeded constant limit; simplify the code to compile");
uint8_t rv = compileExprAuto(value, rsi);
bytecode.emitABC(LOP_SETTABLEKS, rv, reg, uint8_t(BytecodeBuilder::getStringHash(cname)));
bytecode.emitAux(cid);
}
else if (AstExprConstantNumber* ckey = key->as<AstExprConstantNumber>();
ckey && ckey->value >= 1 && ckey->value <= 256 && double(int(ckey->value)) == ckey->value)
{
uint8_t rv = compileExprAuto(value, rsi);
bytecode.emitABC(LOP_SETTABLEN, rv, reg, uint8_t(int(ckey->value) - 1));
}
else
{
uint8_t rk = compileExprAuto(key, rsi);
uint8_t rv = compileExprAuto(value, rsi);
bytecode.emitABC(LOP_SETTABLE, rv, reg, rk);
}
}
} }
// items without a key are set using SETLIST so that we can initialize large arrays quickly // items without a key are set using SETLIST so that we can initialize large arrays quickly
else else
@ -1439,8 +1396,7 @@ struct Compiler
uint8_t rt = compileExprAuto(expr->expr, rs); uint8_t rt = compileExprAuto(expr->expr, rs);
uint8_t i = uint8_t(int(cv->valueNumber) - 1); uint8_t i = uint8_t(int(cv->valueNumber) - 1);
if (FFlag::LuauCompileTableIndexOpt) setDebugLine(expr->index);
setDebugLine(expr->index);
bytecode.emitABC(LOP_GETTABLEN, target, rt, i); bytecode.emitABC(LOP_GETTABLEN, target, rt, i);
} }
@ -1453,8 +1409,7 @@ struct Compiler
if (cid < 0) if (cid < 0)
CompileError::raise(expr->location, "Exceeded constant limit; simplify the code to compile"); CompileError::raise(expr->location, "Exceeded constant limit; simplify the code to compile");
if (FFlag::LuauCompileTableIndexOpt) setDebugLine(expr->index);
setDebugLine(expr->index);
bytecode.emitABC(LOP_GETTABLEKS, target, rt, uint8_t(BytecodeBuilder::getStringHash(iname))); bytecode.emitABC(LOP_GETTABLEKS, target, rt, uint8_t(BytecodeBuilder::getStringHash(iname)));
bytecode.emitAux(cid); bytecode.emitAux(cid);
@ -1853,8 +1808,7 @@ struct Compiler
void compileLValueUse(const LValue& lv, uint8_t reg, bool set) void compileLValueUse(const LValue& lv, uint8_t reg, bool set)
{ {
if (FFlag::LuauCompileTableIndexOpt) setDebugLine(lv.location);
setDebugLine(lv.location);
switch (lv.kind) switch (lv.kind)
{ {

View File

@ -193,11 +193,12 @@ if(TARGET Luau.Analyze.CLI)
CLI/Analyze.cpp) CLI/Analyze.cpp)
endif() endif()
if (TARGET Luau.Ast.CLI) if(TARGET Luau.Ast.CLI)
target_sources(Luau.Ast.CLI PRIVATE target_sources(Luau.Ast.CLI PRIVATE
CLI/Ast.cpp
CLI/FileUtils.h CLI/FileUtils.h
CLI/FileUtils.cpp CLI/FileUtils.cpp
CLI/Ast.cpp) )
endif() endif()
if(TARGET Luau.UnitTest) if(TARGET Luau.UnitTest)

View File

@ -1098,7 +1098,7 @@ static int luauF_select(lua_State* L, StkId res, TValue* arg0, int nresults, Stk
int i = int(nvalue(arg0)); int i = int(nvalue(arg0));
// i >= 1 && i <= n // i >= 1 && i <= n
if (unsigned(i - 1) <= unsigned(n)) if (unsigned(i - 1) < unsigned(n))
{ {
setobj2s(L, res, L->base - n + (i - 1)); setobj2s(L, res, L->base - n + (i - 1));
return 1; return 1;

View File

@ -250,6 +250,8 @@ void luaC_validate(lua_State* L)
if (FFlag::LuauGcPagedSweep) if (FFlag::LuauGcPagedSweep)
{ {
validategco(L, NULL, obj2gco(g->mainthread));
luaM_visitgco(L, L, validategco); luaM_visitgco(L, L, validategco);
} }
else else
@ -565,6 +567,8 @@ void luaC_dump(lua_State* L, void* file, const char* (*categoryName)(lua_State*
if (FFlag::LuauGcPagedSweep) if (FFlag::LuauGcPagedSweep)
{ {
dumpgco(f, NULL, obj2gco(g->mainthread));
luaM_visitgco(L, f, dumpgco); luaM_visitgco(L, f, dumpgco);
} }
else else

View File

@ -8,6 +8,76 @@
#include <string.h> #include <string.h>
/*
* Luau heap uses a size-segregated page structure, with individual pages and large allocations
* allocated using system heap (via frealloc callback).
*
* frealloc callback serves as a general, if slow, allocation callback that can allocate, free or
* resize allocations:
*
* void* frealloc(void* ud, void* ptr, size_t oldsize, size_t newsize);
*
* frealloc(ud, NULL, 0, x) creates a new block of size x
* frealloc(ud, p, x, 0) frees the block p (must return NULL)
* frealloc(ud, NULL, 0, 0) does nothing, equivalent to free(NULL)
*
* frealloc returns NULL if it cannot create or reallocate the area
* (any reallocation to an equal or smaller size cannot fail!)
*
* On top of this, Luau implements heap storage which is split into two types of allocations:
*
* - GCO, short for "garbage collected objects"
* - other objects (for example, arrays stored inside table objects)
*
* The heap layout for these two allocation types is a bit different.
*
* All GCO are allocated in pages, which is a block of memory of ~16K in size that has a page header
* (lua_Page). Each page contains 1..N blocks of the same size, where N is selected to fill the page
* completely. This amortizes the allocation cost and increases locality. Each GCO block starts with
* the GC header (GCheader) which contains the object type, mark bits and other GC metadata. If the
* GCO block is free (not used), then it must have the type set to TNIL; in this case the block can
* be part of the per-page free list, the link for that list is stored after the header (freegcolink).
*
* Importantly, the GCO block doesn't have any back references to the page it's allocated in, so it's
* impossible to free it in isolation - GCO blocks are freed by sweeping the pages they belong to,
* using luaM_freegco which must specify the page; this is called by page sweeper that traverses the
* entire page's worth of objects. For this reason it's also important that freed GCO blocks keep the
* GC header intact and accessible (with type = NIL) so that the sweeper can access it.
*
* Some GCOs are too large to fit in a 16K page without excessive fragmentation (the size threshold is
* currently 512 bytes); in this case, we allocate a dedicated small page with just a single block's worth
* storage space, but that requires allocating an extra page header. In effect large GCOs are a little bit
* less memory efficient, but this allows us to uniformly sweep small and large GCOs using page lists.
*
* All GCO pages are linked in a large intrusive linked list (global_State::allgcopages). Additionally,
* for each block size there's a page free list that contains pages that have at least one free block
* (global_State::freegcopages). This free list is used to make sure object allocation is O(1).
*
* Compared to GCOs, regular allocations have two important differences: they can be freed in isolation,
* and they don't start with a GC header. Because of this, each allocation is prefixed with block metadata,
* which contains the pointer to the page for allocated blocks, and the pointer to the next free block
* inside the page for freed blocks.
* For regular allocations that are too large to fit in a page (using the same threshold of 512 bytes),
* we don't allocate a separate page, instead simply using frealloc to allocate a vanilla block of memory.
*
* Just like GCO pages, we store a page free list (global_State::freepages) that allows O(1) allocation;
* there is no global list for non-GCO pages since we never need to traverse them directly.
*
* In both cases, we pick the page by computing the size class from the block size which rounds the block
* size up to reduce the chance that we'll allocate pages that have very few allocated blocks. The size
* class strategy is determined by SizeClassConfig constructor.
*
* Note that when the last block in a page is freed, we immediately free the page with frealloc - the
* memory manager doesn't currently attempt to keep unused memory around. This can result in excessive
* allocation traffic and can be mitigated by adding a page cache in the future.
*
* For both GCO and non-GCO pages, the per-page block allocation combines bump pointer style allocation
* (lua_Page::freeNext) and per-page free list (lua_Page::freeList). We use the bump allocator to allocate
* the contents of the page, and the free list for further reuse; this allows shorter page setup times
* which results in less variance between allocation cost, as well as tighter sweep bounds for newly
* allocated pages.
*/
LUAU_FASTFLAG(LuauGcPagedSweep) LUAU_FASTFLAG(LuauGcPagedSweep)
#ifndef __has_feature #ifndef __has_feature
@ -56,6 +126,7 @@ static_assert(offsetof(GCObject, ts) == 0, "TString data must be located at the
const size_t kSizeClasses = LUA_SIZECLASSES; const size_t kSizeClasses = LUA_SIZECLASSES;
const size_t kMaxSmallSize = 512; const size_t kMaxSmallSize = 512;
const size_t kPageSize = 16 * 1024 - 24; // slightly under 16KB since that results in less fragmentation due to heap metadata const size_t kPageSize = 16 * 1024 - 24; // slightly under 16KB since that results in less fragmentation due to heap metadata
const size_t kBlockHeader = sizeof(double) > sizeof(void*) ? sizeof(double) : sizeof(void*); // suitable for aligning double & void* on all platforms const size_t kBlockHeader = sizeof(double) > sizeof(void*) ? sizeof(double) : sizeof(void*); // suitable for aligning double & void* on all platforms
const size_t kGCOLinkOffset = (sizeof(GCheader) + sizeof(void*) - 1) & ~(sizeof(void*) - 1); // GCO pages contain freelist links after the GC header const size_t kGCOLinkOffset = (sizeof(GCheader) + sizeof(void*) - 1) & ~(sizeof(void*) - 1); // GCO pages contain freelist links after the GC header
@ -107,24 +178,6 @@ const SizeClassConfig kSizeClassConfig;
#define metadata(block) (*(void**)(block)) #define metadata(block) (*(void**)(block))
#define freegcolink(block) (*(void**)((char*)block + kGCOLinkOffset)) #define freegcolink(block) (*(void**)((char*)block + kGCOLinkOffset))
/*
** About the realloc function:
** void * frealloc (void *ud, void *ptr, size_t osize, size_t nsize);
** (`osize' is the old size, `nsize' is the new size)
**
** Lua ensures that (ptr == NULL) iff (osize == 0).
**
** * frealloc(ud, NULL, 0, x) creates a new block of size `x'
**
** * frealloc(ud, p, x, 0) frees the block `p'
** (in this specific case, frealloc must return NULL).
** particularly, frealloc(ud, NULL, 0, 0) does nothing
** (which is equivalent to free(NULL) in ANSI C)
**
** frealloc returns NULL if it cannot create or reallocate the area
** (any reallocation to an equal or smaller size cannot fail!)
*/
struct lua_Page struct lua_Page
{ {
// list of pages with free blocks // list of pages with free blocks
@ -135,13 +188,12 @@ struct lua_Page
lua_Page* gcolistprev; lua_Page* gcolistprev;
lua_Page* gcolistnext; lua_Page* gcolistnext;
int busyBlocks; int pageSize; // page size in bytes, including page header
int blockSize; int blockSize; // block size in bytes, including block header (for non-GCO)
void* freeList; void* freeList; // next free block in this page; linked with metadata()/freegcolink()
int freeNext; int freeNext; // next free block offset in this page, in bytes; when negative, freeList is used instead
int busyBlocks; // number of blocks allocated out of this page
int pageSize;
union union
{ {
@ -177,7 +229,7 @@ static lua_Page* newpageold(lua_State* L, uint8_t sizeClass)
page->gcolistprev = NULL; page->gcolistprev = NULL;
page->gcolistnext = NULL; page->gcolistnext = NULL;
page->busyBlocks = 0; page->pageSize = kPageSize;
page->blockSize = blockSize; page->blockSize = blockSize;
// note: we start with the last block in the page and move downward // note: we start with the last block in the page and move downward
@ -185,6 +237,7 @@ static lua_Page* newpageold(lua_State* L, uint8_t sizeClass)
// additionally, GC stores objects in singly linked lists, and this way the GC lists end up in increasing pointer order // additionally, GC stores objects in singly linked lists, and this way the GC lists end up in increasing pointer order
page->freeList = NULL; page->freeList = NULL;
page->freeNext = (blockCount - 1) * blockSize; page->freeNext = (blockCount - 1) * blockSize;
page->busyBlocks = 0;
// prepend a page to page freelist (which is empty because we only ever allocate a new page when it is!) // prepend a page to page freelist (which is empty because we only ever allocate a new page when it is!)
LUAU_ASSERT(!g->freepages[sizeClass]); LUAU_ASSERT(!g->freepages[sizeClass]);
@ -214,7 +267,7 @@ static lua_Page* newpage(lua_State* L, lua_Page** gcopageset, int pageSize, int
page->gcolistprev = NULL; page->gcolistprev = NULL;
page->gcolistnext = NULL; page->gcolistnext = NULL;
page->busyBlocks = 0; page->pageSize = pageSize;
page->blockSize = blockSize; page->blockSize = blockSize;
// note: we start with the last block in the page and move downward // note: we start with the last block in the page and move downward
@ -222,8 +275,7 @@ static lua_Page* newpage(lua_State* L, lua_Page** gcopageset, int pageSize, int
// additionally, GC stores objects in singly linked lists, and this way the GC lists end up in increasing pointer order // additionally, GC stores objects in singly linked lists, and this way the GC lists end up in increasing pointer order
page->freeList = NULL; page->freeList = NULL;
page->freeNext = (blockCount - 1) * blockSize; page->freeNext = (blockCount - 1) * blockSize;
page->busyBlocks = 0;
page->pageSize = pageSize;
if (gcopageset) if (gcopageset)
{ {
@ -406,8 +458,7 @@ static void* newgcoblock(lua_State* L, int sizeClass)
page->next = NULL; page->next = NULL;
} }
// the user data is right after the metadata return block;
return (char*)block;
} }
static void freeblock(lua_State* L, int sizeClass, void* block) static void freeblock(lua_State* L, int sizeClass, void* block)
@ -421,6 +472,7 @@ static void freeblock(lua_State* L, int sizeClass, void* block)
lua_Page* page = (lua_Page*)metadata(block); lua_Page* page = (lua_Page*)metadata(block);
LUAU_ASSERT(page && page->busyBlocks > 0); LUAU_ASSERT(page && page->busyBlocks > 0);
LUAU_ASSERT(size_t(page->blockSize) == kSizeClassConfig.sizeOfClass[sizeClass] + kBlockHeader); LUAU_ASSERT(size_t(page->blockSize) == kSizeClassConfig.sizeOfClass[sizeClass] + kBlockHeader);
LUAU_ASSERT(block >= page->data && block < (char*)page + page->pageSize);
// if the page wasn't in the page free list, it should be now since it got a block! // if the page wasn't in the page free list, it should be now since it got a block!
if (!page->freeList && page->freeNext < 0) if (!page->freeList && page->freeNext < 0)
@ -455,6 +507,9 @@ static void freeblock(lua_State* L, int sizeClass, void* block)
static void freegcoblock(lua_State* L, int sizeClass, void* block, lua_Page* page) static void freegcoblock(lua_State* L, int sizeClass, void* block, lua_Page* page)
{ {
LUAU_ASSERT(FFlag::LuauGcPagedSweep); LUAU_ASSERT(FFlag::LuauGcPagedSweep);
LUAU_ASSERT(page && page->busyBlocks > 0);
LUAU_ASSERT(page->blockSize == kSizeClassConfig.sizeOfClass[sizeClass]);
LUAU_ASSERT(block >= page->data && block < (char*)page + page->pageSize);
global_State* g = L->global; global_State* g = L->global;
@ -575,6 +630,8 @@ void luaM_freegco_(lua_State* L, GCObject* block, size_t osize, uint8_t memcat,
else else
{ {
LUAU_ASSERT(page->busyBlocks == 1); LUAU_ASSERT(page->busyBlocks == 1);
LUAU_ASSERT(size_t(page->blockSize) == osize);
LUAU_ASSERT((void*)block == page->data);
freepage(L, &g->allgcopages, page); freepage(L, &g->allgcopages, page);
} }
@ -626,8 +683,12 @@ void luaM_getpagewalkinfo(lua_Page* page, char** start, char** end, int* busyBlo
int blockCount = (page->pageSize - offsetof(lua_Page, data)) / page->blockSize; int blockCount = (page->pageSize - offsetof(lua_Page, data)) / page->blockSize;
*start = page->data + page->freeNext + page->blockSize; LUAU_ASSERT(page->freeNext >= -page->blockSize && page->freeNext <= (blockCount - 1) * page->blockSize);
*end = page->data + blockCount * page->blockSize;
char* data = page->data; // silences ubsan when indexing page->data
*start = data + page->freeNext + page->blockSize;
*end = data + blockCount * page->blockSize;
*busyBlocks = page->busyBlocks; *busyBlocks = page->busyBlocks;
*blockSize = page->blockSize; *blockSize = page->blockSize;
} }
@ -675,7 +736,7 @@ void luaM_visitgco(lua_State* L, void* context, bool (*visitor)(void* context, l
for (lua_Page* curr = g->allgcopages; curr;) for (lua_Page* curr = g->allgcopages; curr;)
{ {
lua_Page* next = curr->gcolistnext; // page blockvisit might destroy the page lua_Page* next = curr->gcolistnext; // block visit might destroy the page
luaM_visitpage(curr, context, visitor); luaM_visitpage(curr, context, visitor);

View File

@ -131,7 +131,7 @@ void luaO_chunkid(char* out, const char* source, size_t bufflen)
{ {
size_t l; size_t l;
source++; /* skip the `@' */ source++; /* skip the `@' */
bufflen -= sizeof(" '...' "); bufflen -= sizeof("...");
l = strlen(source); l = strlen(source);
strcpy(out, ""); strcpy(out, "");
if (l > bufflen) if (l > bufflen)
@ -144,7 +144,7 @@ void luaO_chunkid(char* out, const char* source, size_t bufflen)
else else
{ /* out = [string "string"] */ { /* out = [string "string"] */
size_t len = strcspn(source, "\n\r"); /* stop at first newline */ size_t len = strcspn(source, "\n\r"); /* stop at first newline */
bufflen -= sizeof(" [string \"...\"] "); bufflen -= sizeof("[string \"...\"]");
if (len > bufflen) if (len > bufflen)
len = bufflen; len = bufflen;
strcpy(out, "[string \""); strcpy(out, "[string \"");

View File

@ -609,7 +609,8 @@ static void luau_execute(lua_State* L)
if (unsigned(ic) < LUA_VECTOR_SIZE && name[1] == '\0') if (unsigned(ic) < LUA_VECTOR_SIZE && name[1] == '\0')
{ {
setnvalue(ra, rb->value.v[ic]); const float* v = rb->value.v; // silences ubsan when indexing v[]
setnvalue(ra, v[ic]);
VM_NEXT(); VM_NEXT();
} }

View File

@ -605,8 +605,6 @@ RETURN R0 1
TEST_CASE("TableLiteralsIndexConstant") TEST_CASE("TableLiteralsIndexConstant")
{ {
ScopedFastFlag sff("LuauCompileTableIndexOpt", true);
// validate that we use SETTTABLEKS for constant variable keys // validate that we use SETTTABLEKS for constant variable keys
CHECK_EQ("\n" + compileFunction0(R"( CHECK_EQ("\n" + compileFunction0(R"(
local a, b = "key", "value" local a, b = "key", "value"
@ -2483,8 +2481,6 @@ return
TEST_CASE("DebugLineInfoAssignment") TEST_CASE("DebugLineInfoAssignment")
{ {
ScopedFastFlag sff("LuauCompileTableIndexOpt", true);
Luau::BytecodeBuilder bcb; Luau::BytecodeBuilder bcb;
bcb.setDumpFlags(Luau::BytecodeBuilder::Dump_Code | Luau::BytecodeBuilder::Dump_Lines); bcb.setDumpFlags(Luau::BytecodeBuilder::Dump_Code | Luau::BytecodeBuilder::Dump_Lines);
Luau::compileOrThrow(bcb, R"( Luau::compileOrThrow(bcb, R"(

View File

@ -492,8 +492,6 @@ TEST_CASE("DateTime")
TEST_CASE("Debug") TEST_CASE("Debug")
{ {
ScopedFastFlag sffw("LuauBytecodeV2Write", true);
runConformance("debug.lua"); runConformance("debug.lua");
} }

View File

@ -1392,19 +1392,31 @@ TEST_CASE_FIXTURE(Fixture, "DeprecatedApi")
{"DataCost", {typeChecker.numberType, /* deprecated= */ true}}, {"DataCost", {typeChecker.numberType, /* deprecated= */ true}},
{"Wait", {typeChecker.anyType, /* deprecated= */ true}}, {"Wait", {typeChecker.anyType, /* deprecated= */ true}},
}; };
TypeId colorType = typeChecker.globalTypes.addType(TableTypeVar{{}, std::nullopt, typeChecker.globalScope->level, Luau::TableState::Sealed});
getMutable<TableTypeVar>(colorType)->props = {
{"toHSV", {typeChecker.anyType, /* deprecated= */ true, "Color3:ToHSV"} }
};
addGlobalBinding(typeChecker, "Color3", Binding{colorType, {}});
freeze(typeChecker.globalTypes); freeze(typeChecker.globalTypes);
LintResult result = lintTyped(R"( LintResult result = lintTyped(R"(
return function (i: Instance) return function (i: Instance)
i:Wait(1.0) i:Wait(1.0)
print(i.Name) print(i.Name)
print(Color3.toHSV())
print(Color3.doesntexist, i.doesntexist) -- type error, but this verifies we correctly handle non-existent members
return i.DataCost return i.DataCost
end end
)"); )");
REQUIRE_EQ(result.warnings.size(), 2); REQUIRE_EQ(result.warnings.size(), 3);
CHECK_EQ(result.warnings[0].text, "Member 'Instance.Wait' is deprecated"); CHECK_EQ(result.warnings[0].text, "Member 'Instance.Wait' is deprecated");
CHECK_EQ(result.warnings[1].text, "Member 'Instance.DataCost' is deprecated"); CHECK_EQ(result.warnings[1].text, "Member 'toHSV' is deprecated, use 'Color3:ToHSV' instead");
CHECK_EQ(result.warnings[2].text, "Member 'Instance.DataCost' is deprecated");
} }
TEST_CASE_FIXTURE(Fixture, "TableOperations") TEST_CASE_FIXTURE(Fixture, "TableOperations")
@ -1475,9 +1487,11 @@ _ = (true and true) or true
_ = (true and false) and (42 and false) _ = (true and false) and (42 and false)
_ = true and true or false -- no warning since this is is a common pattern used as a ternary replacement _ = true and true or false -- no warning since this is is a common pattern used as a ternary replacement
_ = if true then 1 elseif true then 2 else 3
)"); )");
REQUIRE_EQ(result.warnings.size(), 7); REQUIRE_EQ(result.warnings.size(), 8);
CHECK_EQ(result.warnings[0].text, "Condition has already been checked on line 2"); CHECK_EQ(result.warnings[0].text, "Condition has already been checked on line 2");
CHECK_EQ(result.warnings[0].location.begin.line + 1, 4); CHECK_EQ(result.warnings[0].location.begin.line + 1, 4);
CHECK_EQ(result.warnings[1].text, "Condition has already been checked on column 5"); CHECK_EQ(result.warnings[1].text, "Condition has already been checked on column 5");
@ -1487,6 +1501,7 @@ _ = true and true or false -- no warning since this is is a common pattern used
CHECK_EQ(result.warnings[5].text, "Condition has already been checked on column 6"); CHECK_EQ(result.warnings[5].text, "Condition has already been checked on column 6");
CHECK_EQ(result.warnings[6].text, "Condition has already been checked on column 15"); CHECK_EQ(result.warnings[6].text, "Condition has already been checked on column 15");
CHECK_EQ(result.warnings[6].location.begin.line + 1, 19); CHECK_EQ(result.warnings[6].location.begin.line + 1, 19);
CHECK_EQ(result.warnings[7].text, "Condition has already been checked on column 8");
} }
TEST_CASE_FIXTURE(Fixture, "DuplicateConditionsExpr") TEST_CASE_FIXTURE(Fixture, "DuplicateConditionsExpr")
@ -1528,4 +1543,19 @@ return foo, moo, a1, a2
CHECK_EQ(result.warnings[3].text, "Function parameter 'self' already defined implicitly"); CHECK_EQ(result.warnings[3].text, "Function parameter 'self' already defined implicitly");
} }
TEST_CASE_FIXTURE(Fixture, "MisleadingAndOr")
{
LintResult result = lint(R"(
_ = math.random() < 0.5 and true or 42
_ = math.random() < 0.5 and false or 42 -- misleading
_ = math.random() < 0.5 and nil or 42 -- misleading
_ = math.random() < 0.5 and 0 or 42
_ = (math.random() < 0.5 and false) or 42 -- currently ignored
)");
REQUIRE_EQ(result.warnings.size(), 2);
CHECK_EQ(result.warnings[0].text, "The and-or expression always evaluates to the second alternative because the first alternative is false; consider using if-then-else expression instead");
CHECK_EQ(result.warnings[1].text, "The and-or expression always evaluates to the second alternative because the first alternative is nil; consider using if-then-else expression instead");
}
TEST_SUITE_END(); TEST_SUITE_END();

View File

@ -8,9 +8,22 @@
#include <iostream> #include <iostream>
#include <memory> #include <memory>
#include <set>
#include <string> #include <string>
#include <vector> #include <vector>
struct Completion
{
std::string completion;
std::string display;
bool operator<(Completion const& other) const
{
return std::tie(completion, display) < std::tie(other.completion, other.display);
}
};
using CompletionSet = std::set<Completion>;
class ReplFixture class ReplFixture
{ {
@ -34,6 +47,27 @@ public:
lua_pop(L, 1); lua_pop(L, 1);
return result; return result;
} }
CompletionSet getCompletionSet(const char* inputPrefix)
{
CompletionSet result;
int top = lua_gettop(L);
getCompletions(L, inputPrefix, [&result](const std::string& completion, const std::string& display) {
result.insert(Completion{completion, display});
});
// Ensure that generating completions doesn't change the position of luau's stack top.
CHECK(top == lua_gettop(L));
return result;
}
bool checkCompletion(const CompletionSet& completions, const std::string& prefix, const std::string& expected)
{
std::string expectedDisplay(expected.substr(0, expected.find_first_of('(')));
Completion expectedCompletion{prefix + expected, expectedDisplay};
return completions.count(expectedCompletion) == 1;
}
lua_State* L; lua_State* L;
private: private:
@ -115,3 +149,61 @@ TEST_CASE_FIXTURE(ReplFixture, "MultipleArguments")
} }
TEST_SUITE_END(); TEST_SUITE_END();
TEST_SUITE_BEGIN("ReplCodeCompletion");
TEST_CASE_FIXTURE(ReplFixture, "CompleteGlobalVariables")
{
runCode(L, R"(
myvariable1 = 5
myvariable2 = 5
)");
CompletionSet completions = getCompletionSet("myvar");
std::string prefix = "";
CHECK(completions.size() == 2);
CHECK(checkCompletion(completions, prefix, "myvariable1"));
CHECK(checkCompletion(completions, prefix, "myvariable2"));
}
TEST_CASE_FIXTURE(ReplFixture, "CompleteTableKeys")
{
runCode(L, R"(
t = { color = "red", size = 1, shape = "circle" }
)");
{
CompletionSet completions = getCompletionSet("t.");
std::string prefix = "t.";
CHECK(completions.size() == 3);
CHECK(checkCompletion(completions, prefix, "color"));
CHECK(checkCompletion(completions, prefix, "size"));
CHECK(checkCompletion(completions, prefix, "shape"));
}
{
CompletionSet completions = getCompletionSet("t.s");
std::string prefix = "t.";
CHECK(completions.size() == 2);
CHECK(checkCompletion(completions, prefix, "size"));
CHECK(checkCompletion(completions, prefix, "shape"));
}
}
TEST_CASE_FIXTURE(ReplFixture, "StringMethods")
{
runCode(L, R"(
s = ""
)");
{
CompletionSet completions = getCompletionSet("s:l");
std::string prefix = "s:";
CHECK(completions.size() == 2);
CHECK(checkCompletion(completions, prefix, "len("));
CHECK(checkCompletion(completions, prefix, "lower("));
}
}
TEST_SUITE_END();

View File

@ -595,4 +595,65 @@ TEST_CASE_FIXTURE(Fixture, "generic_typevars_are_not_considered_to_escape_their_
LUAU_REQUIRE_NO_ERRORS(result); LUAU_REQUIRE_NO_ERRORS(result);
} }
/*
* The two-pass alias definition system starts by ascribing a free TypeVar to each alias. It then
* circles back to fill in the actual type later on.
*
* If this free type is unified with something degenerate like `any`, we need to take extra care
* to ensure that the alias actually binds to the type that the user expected.
*/
TEST_CASE_FIXTURE(Fixture, "forward_declared_alias_is_not_clobbered_by_prior_unification_with_any")
{
ScopedFastFlag sff[] = {
{"LuauTwoPassAliasDefinitionFix", true}
};
CheckResult result = check(R"(
local function x()
local y: FutureType = {}::any
return 1
end
type FutureType = { foo: typeof(x()) }
local d: FutureType = { smth = true } -- missing error, 'd' is resolved to 'any'
)");
CHECK_EQ("{| foo: number |}", toString(requireType("d"), {true}));
LUAU_REQUIRE_ERROR_COUNT(1, result);
}
TEST_CASE_FIXTURE(Fixture, "forward_declared_alias_is_not_clobbered_by_prior_unification_with_any_2")
{
ScopedFastFlag sff[] = {
{"LuauTwoPassAliasDefinitionFix", true},
// We also force these two flags because this surfaced an unfortunate interaction.
{"LuauErrorRecoveryType", true},
{"LuauQuantifyInPlace2", true},
};
CheckResult result = check(R"(
local B = {}
B.bar = 4
function B:smth1()
local self: FutureIntersection = self
self.foo = 4
return 4
end
function B:smth2()
local self: FutureIntersection = self
self.bar = 5 -- error, even though we should have B part with bar
end
type A = { foo: typeof(B.smth1({foo=3})) } -- trick toposort into sorting functions before types
type B = typeof(B)
type FutureIntersection = A & B
)");
LUAU_REQUIRE_NO_ERRORS(result);
}
TEST_SUITE_END(); TEST_SUITE_END();

View File

@ -7,8 +7,6 @@
#include "doctest.h" #include "doctest.h"
LUAU_FASTFLAG(LuauFixTonumberReturnType)
using namespace Luau; using namespace Luau;
LUAU_FASTFLAG(LuauUseCommittingTxnLog) LUAU_FASTFLAG(LuauUseCommittingTxnLog)
@ -850,11 +848,8 @@ TEST_CASE_FIXTURE(Fixture, "tonumber_returns_optional_number_type")
local b: number = tonumber('asdf') local b: number = tonumber('asdf')
)"); )");
if (FFlag::LuauFixTonumberReturnType) LUAU_REQUIRE_ERROR_COUNT(1, result);
{ CHECK_EQ("Type 'number?' could not be converted into 'number'", toString(result.errors[0]));
LUAU_REQUIRE_ERROR_COUNT(1, result);
CHECK_EQ("Type 'number?' could not be converted into 'number'", toString(result.errors[0]));
}
} }
TEST_CASE_FIXTURE(Fixture, "tonumber_returns_optional_number_type2") TEST_CASE_FIXTURE(Fixture, "tonumber_returns_optional_number_type2")
@ -893,7 +888,7 @@ TEST_CASE_FIXTURE(Fixture, "assert_removes_falsy_types")
{ {
ScopedFastFlag sff[]{ ScopedFastFlag sff[]{
{"LuauAssertStripsFalsyTypes", true}, {"LuauAssertStripsFalsyTypes", true},
{"LuauDiscriminableUnions", true}, {"LuauDiscriminableUnions2", true},
}; };
CheckResult result = check(R"( CheckResult result = check(R"(
@ -910,7 +905,7 @@ TEST_CASE_FIXTURE(Fixture, "assert_removes_falsy_types_even_from_type_pack_tail_
{ {
ScopedFastFlag sff[]{ ScopedFastFlag sff[]{
{"LuauAssertStripsFalsyTypes", true}, {"LuauAssertStripsFalsyTypes", true},
{"LuauDiscriminableUnions", true}, {"LuauDiscriminableUnions2", true},
}; };
CheckResult result = check(R"( CheckResult result = check(R"(
@ -927,7 +922,7 @@ TEST_CASE_FIXTURE(Fixture, "assert_returns_false_and_string_iff_it_knows_the_fir
{ {
ScopedFastFlag sff[]{ ScopedFastFlag sff[]{
{"LuauAssertStripsFalsyTypes", true}, {"LuauAssertStripsFalsyTypes", true},
{"LuauDiscriminableUnions", true}, {"LuauDiscriminableUnions2", true},
}; };
CheckResult result = check(R"( CheckResult result = check(R"(

View File

@ -262,7 +262,7 @@ TEST_CASE_FIXTURE(Fixture, "lvalue_equals_another_lvalue_with_no_overlap")
// Just needs to fully support equality refinement. Which is annoying without type states. // Just needs to fully support equality refinement. Which is annoying without type states.
TEST_CASE_FIXTURE(Fixture, "discriminate_from_x_not_equal_to_nil") TEST_CASE_FIXTURE(Fixture, "discriminate_from_x_not_equal_to_nil")
{ {
ScopedFastFlag sff{"LuauDiscriminableUnions", true}; ScopedFastFlag sff{"LuauDiscriminableUnions2", true};
CheckResult result = check(R"( CheckResult result = check(R"(
type T = {x: string, y: number} | {x: nil, y: nil} type T = {x: string, y: number} | {x: nil, y: nil}
@ -616,4 +616,76 @@ local a: Self<Table>
CHECK_EQ(toString(requireType("a")), "Table<Table>"); CHECK_EQ(toString(requireType("a")), "Table<Table>");
} }
TEST_CASE_FIXTURE(Fixture, "do_not_ice_when_trying_to_pick_first_of_generic_type_pack")
{
ScopedFastFlag sff[]{
{"LuauQuantifyInPlace2", true},
{"LuauReturnAnyInsteadOfICE", true},
};
// In-place quantification causes these types to have the wrong types but only because of nasty interaction with prototyping.
// The type of f is initially () -> free1...
// Then the prototype iterator advances, and checks the function expression assigned to g, which has the type () -> free2...
// In the body it calls f and returns what f() returns. This binds free2... with free1..., causing f and g to have same types.
// We then quantify g, leaving it with the final type <a...>() -> a...
// Because free1... and free2... were bound, in combination with in-place quantification, f's return type was also turned into a...
// Then the check iterator catches up, and checks the body of f, and attempts to quantify it too.
// Alas, one of the requirements for quantification is that a type must contain free types. () -> a... has no free types.
// Thus the quantification for f was no-op, which explains why f does not have any type parameters.
// Calling f() will attempt to instantiate the function type, which turns generics in type binders into to free types.
// However, instantiations only converts generics contained within the type binders of a function, so instantiation was also no-op.
// Which means that calling f() simply returned a... rather than an instantiation of it. And since the call site was not in tail position,
// picking first element in a... triggers an ICE because calls returning generic packs are unexpected.
CheckResult result = check(R"(
local function f() end
local g = function() return f() end
local x = (f()) -- should error: no return values to assign from the call to f
)");
LUAU_REQUIRE_NO_ERRORS(result);
// f and g should have the type () -> ()
CHECK_EQ("() -> (a...)", toString(requireType("f")));
CHECK_EQ("<a...>() -> (a...)", toString(requireType("g")));
CHECK_EQ("any", toString(requireType("x"))); // any is returned instead of ICE for now
}
TEST_CASE_FIXTURE(Fixture, "specialization_binds_with_prototypes_too_early")
{
CheckResult result = check(R"(
local function id(x) return x end
local n2n: (number) -> number = id
local s2s: (string) -> string = id
)");
LUAU_REQUIRE_ERRORS(result); // Should not have any errors.
}
TEST_CASE_FIXTURE(Fixture, "weird_fail_to_unify_type_pack")
{
ScopedFastFlag sff{"LuauQuantifyInPlace2", true};
CheckResult result = check(R"(
local function f() return end
local g = function() return f() end
)");
LUAU_REQUIRE_ERRORS(result); // Should not have any errors.
}
TEST_CASE_FIXTURE(Fixture, "weird_fail_to_unify_variadic_pack")
{
ScopedFastFlag sff{"LuauQuantifyInPlace2", true};
CheckResult result = check(R"(
--!strict
local function f(...) return ... end
local g = function(...) return f(...) end
)");
LUAU_REQUIRE_ERRORS(result); // Should not have any errors.
}
TEST_SUITE_END(); TEST_SUITE_END();

View File

@ -6,7 +6,7 @@
#include "doctest.h" #include "doctest.h"
LUAU_FASTFLAG(LuauDiscriminableUnions) LUAU_FASTFLAG(LuauDiscriminableUnions2)
LUAU_FASTFLAG(LuauWeakEqConstraint) LUAU_FASTFLAG(LuauWeakEqConstraint)
LUAU_FASTFLAG(LuauQuantifyInPlace2) LUAU_FASTFLAG(LuauQuantifyInPlace2)
@ -262,7 +262,7 @@ TEST_CASE_FIXTURE(Fixture, "typeguard_only_look_up_types_from_global_scope")
end end
)"); )");
if (FFlag::LuauDiscriminableUnions) if (FFlag::LuauDiscriminableUnions2)
{ {
LUAU_REQUIRE_NO_ERRORS(result); LUAU_REQUIRE_NO_ERRORS(result);
@ -435,7 +435,7 @@ TEST_CASE_FIXTURE(Fixture, "lvalue_is_equal_to_a_term")
TEST_CASE_FIXTURE(Fixture, "term_is_equal_to_an_lvalue") TEST_CASE_FIXTURE(Fixture, "term_is_equal_to_an_lvalue")
{ {
ScopedFastFlag sff[] = { ScopedFastFlag sff[] = {
{"LuauDiscriminableUnions", true}, {"LuauDiscriminableUnions2", true},
{"LuauSingletonTypes", true}, {"LuauSingletonTypes", true},
}; };
@ -485,7 +485,7 @@ TEST_CASE_FIXTURE(Fixture, "lvalue_is_not_nil")
TEST_CASE_FIXTURE(Fixture, "free_type_is_equal_to_an_lvalue") TEST_CASE_FIXTURE(Fixture, "free_type_is_equal_to_an_lvalue")
{ {
ScopedFastFlag sff{"LuauDiscriminableUnions", true}; ScopedFastFlag sff{"LuauDiscriminableUnions2", true};
ScopedFastFlag sff2{"LuauWeakEqConstraint", true}; ScopedFastFlag sff2{"LuauWeakEqConstraint", true};
CheckResult result = check(R"( CheckResult result = check(R"(
@ -589,7 +589,7 @@ TEST_CASE_FIXTURE(Fixture, "type_narrow_to_vector")
end end
)"); )");
if (FFlag::LuauDiscriminableUnions) if (FFlag::LuauDiscriminableUnions2)
{ {
LUAU_REQUIRE_NO_ERRORS(result); LUAU_REQUIRE_NO_ERRORS(result);
} }
@ -1002,7 +1002,7 @@ TEST_CASE_FIXTURE(Fixture, "apply_refinements_on_astexprindexexpr_whose_subscrip
TEST_CASE_FIXTURE(Fixture, "discriminate_from_truthiness_of_x") TEST_CASE_FIXTURE(Fixture, "discriminate_from_truthiness_of_x")
{ {
ScopedFastFlag sff[] = { ScopedFastFlag sff[] = {
{"LuauDiscriminableUnions", true}, {"LuauDiscriminableUnions2", true},
{"LuauParseSingletonTypes", true}, {"LuauParseSingletonTypes", true},
{"LuauSingletonTypes", true}, {"LuauSingletonTypes", true},
}; };
@ -1028,7 +1028,7 @@ TEST_CASE_FIXTURE(Fixture, "discriminate_from_truthiness_of_x")
TEST_CASE_FIXTURE(Fixture, "discriminate_tag") TEST_CASE_FIXTURE(Fixture, "discriminate_tag")
{ {
ScopedFastFlag sff[] = { ScopedFastFlag sff[] = {
{"LuauDiscriminableUnions", true}, {"LuauDiscriminableUnions2", true},
{"LuauParseSingletonTypes", true}, {"LuauParseSingletonTypes", true},
{"LuauSingletonTypes", true}, {"LuauSingletonTypes", true},
}; };
@ -1069,7 +1069,7 @@ TEST_CASE_FIXTURE(Fixture, "narrow_boolean_to_true_or_false")
ScopedFastFlag sff[]{ ScopedFastFlag sff[]{
{"LuauParseSingletonTypes", true}, {"LuauParseSingletonTypes", true},
{"LuauSingletonTypes", true}, {"LuauSingletonTypes", true},
{"LuauDiscriminableUnions", true}, {"LuauDiscriminableUnions2", true},
{"LuauAssertStripsFalsyTypes", true}, {"LuauAssertStripsFalsyTypes", true},
}; };
@ -1094,7 +1094,7 @@ TEST_CASE_FIXTURE(Fixture, "discriminate_on_properties_of_disjoint_tables_where_
ScopedFastFlag sff[]{ ScopedFastFlag sff[]{
{"LuauParseSingletonTypes", true}, {"LuauParseSingletonTypes", true},
{"LuauSingletonTypes", true}, {"LuauSingletonTypes", true},
{"LuauDiscriminableUnions", true}, {"LuauDiscriminableUnions2", true},
{"LuauAssertStripsFalsyTypes", true}, {"LuauAssertStripsFalsyTypes", true},
}; };
@ -1118,7 +1118,7 @@ TEST_CASE_FIXTURE(Fixture, "discriminate_on_properties_of_disjoint_tables_where_
TEST_CASE_FIXTURE(RefinementClassFixture, "discriminate_from_isa_of_x") TEST_CASE_FIXTURE(RefinementClassFixture, "discriminate_from_isa_of_x")
{ {
ScopedFastFlag sff[] = { ScopedFastFlag sff[] = {
{"LuauDiscriminableUnions", true}, {"LuauDiscriminableUnions2", true},
{"LuauParseSingletonTypes", true}, {"LuauParseSingletonTypes", true},
{"LuauSingletonTypes", true}, {"LuauSingletonTypes", true},
}; };
@ -1157,7 +1157,7 @@ TEST_CASE_FIXTURE(RefinementClassFixture, "typeguard_cast_free_table_to_vector")
end end
)"); )");
if (FFlag::LuauDiscriminableUnions) if (FFlag::LuauDiscriminableUnions2)
LUAU_REQUIRE_NO_ERRORS(result); LUAU_REQUIRE_NO_ERRORS(result);
else else
{ {

View File

@ -5164,4 +5164,151 @@ function x:Destroy(): () end
LUAU_REQUIRE_ERRORS(result); LUAU_REQUIRE_ERRORS(result);
} }
TEST_CASE_FIXTURE(Fixture, "do_not_modify_imported_types_2")
{
ScopedFastFlag immutableTypes{"LuauImmutableTypes", true};
fileResolver.source["game/A"] = R"(
export type Type = { x: { a: number } }
return {}
)";
fileResolver.source["game/B"] = R"(
local types = require(game.A)
type Type = types.Type
local x: Type = { x = { a = 2 } }
type Rename = typeof(x.x)
)";
CheckResult result = frontend.check("game/B");
LUAU_REQUIRE_NO_ERRORS(result);
}
TEST_CASE_FIXTURE(Fixture, "do_not_modify_imported_types_3")
{
ScopedFastFlag immutableTypes{"LuauImmutableTypes", true};
fileResolver.source["game/A"] = R"(
local y = setmetatable({}, {})
export type Type = { x: typeof(y) }
return { x = y }
)";
fileResolver.source["game/B"] = R"(
local types = require(game.A)
type Type = types.Type
local x: Type = types
type Rename = typeof(x.x)
)";
CheckResult result = frontend.check("game/B");
LUAU_REQUIRE_NO_ERRORS(result);
}
TEST_CASE_FIXTURE(Fixture, "indexing_on_string_singletons")
{
ScopedFastFlag sff[]{
{"LuauDiscriminableUnions2", true},
{"LuauRefactorTypeVarQuestions", true},
{"LuauSingletonTypes", true},
};
CheckResult result = check(R"(
local a: string = "hi"
if a == "hi" then
local x = a:byte()
end
)");
LUAU_REQUIRE_NO_ERRORS(result);
CHECK_EQ(R"("hi")", toString(requireTypeAtPosition({3, 22})));
}
TEST_CASE_FIXTURE(Fixture, "indexing_on_union_of_string_singletons")
{
ScopedFastFlag sff[]{
{"LuauDiscriminableUnions2", true},
{"LuauRefactorTypeVarQuestions", true},
{"LuauSingletonTypes", true},
};
CheckResult result = check(R"(
local a: string = "hi"
if a == "hi" or a == "bye" then
local x = a:byte()
end
)");
LUAU_REQUIRE_NO_ERRORS(result);
CHECK_EQ(R"("bye" | "hi")", toString(requireTypeAtPosition({3, 22})));
}
TEST_CASE_FIXTURE(Fixture, "taking_the_length_of_string_singleton")
{
ScopedFastFlag sff[]{
{"LuauDiscriminableUnions2", true},
{"LuauRefactorTypeVarQuestions", true},
{"LuauSingletonTypes", true},
{"LuauLengthOnCompositeType", true},
};
CheckResult result = check(R"(
local a: string = "hi"
if a == "hi" then
local x = #a
end
)");
LUAU_REQUIRE_NO_ERRORS(result);
CHECK_EQ(R"("hi")", toString(requireTypeAtPosition({3, 23})));
}
TEST_CASE_FIXTURE(Fixture, "taking_the_length_of_union_of_string_singleton")
{
ScopedFastFlag sff[]{
{"LuauDiscriminableUnions2", true},
{"LuauRefactorTypeVarQuestions", true},
{"LuauSingletonTypes", true},
{"LuauLengthOnCompositeType", true},
};
CheckResult result = check(R"(
local a: string = "hi"
if a == "hi" or a == "bye" then
local x = #a
end
)");
LUAU_REQUIRE_NO_ERRORS(result);
CHECK_EQ(R"("bye" | "hi")", toString(requireTypeAtPosition({3, 23})));
}
/*
* When we add new properties to an unsealed table, we should do a level check and promote the property type to be at
* the level of the table.
*/
TEST_CASE_FIXTURE(Fixture, "inferred_properties_of_a_table_should_start_with_the_same_TypeLevel_of_that_table")
{
CheckResult result = check(R"(
--!strict
local T = {}
local function f(prop)
T[1] = {
prop = prop,
}
end
local function g()
local l = T[1].prop
end
)");
LUAU_REQUIRE_NO_ERRORS(result);
}
TEST_SUITE_END(); TEST_SUITE_END();

View File

@ -273,4 +273,21 @@ TEST_CASE_FIXTURE(TryUnifyFixture, "recursive_metatable_getmatchtag")
state.tryUnify(&metatable, &variant); state.tryUnify(&metatable, &variant);
} }
TEST_CASE_FIXTURE(TryUnifyFixture, "cli_50320_follow_in_any_unification")
{
ScopedFastFlag sffs[] = {
{"LuauUseCommittingTxnLog", true},
{"LuauFollowWithCommittingTxnLogInAnyUnification", true},
};
TypePackVar free{FreeTypePack{TypeLevel{}}};
TypePackVar target{TypePack{}};
TypeVar func{FunctionTypeVar{&free, &free}};
state.tryUnify(&free, &target);
// Shouldn't assert or error.
state.tryUnify(&func, typeChecker.anyType);
}
TEST_SUITE_END(); TEST_SUITE_END();

View File

@ -118,9 +118,7 @@ assert((function() return #_G end)() == 0)
assert((function() return #{1,2} end)() == 2) assert((function() return #{1,2} end)() == 2)
assert((function() return #'g' end)() == 1) assert((function() return #'g' end)() == 1)
local ud = newproxy(true) assert((function() local ud = newproxy(true) getmetatable(ud).__len = function() return 42 end return #ud end)() == 42)
getmetatable(ud).__len = function() return 42 end
assert((function() return #ud end)() == 42)
assert((function() local a = 1 a = -a return a end)() == -1) assert((function() local a = 1 a = -a return a end)() == -1)
@ -325,6 +323,10 @@ assert((function() local t = {6, 9, 7} t[4.5] = 10 return t[4.5] end)() == 10)
assert((function() local t = {6, 9, 7} t['a'] = 11 return t['a'] end)() == 11) assert((function() local t = {6, 9, 7} t['a'] = 11 return t['a'] end)() == 11)
assert((function() local t = {6, 9, 7} setmetatable(t, { __newindex = function(t,i,v) rawset(t, i * 10, v) end }) t[1] = 17 t[5] = 1 return concat(t[1],t[5],t[50]) end)() == "17,nil,1") assert((function() local t = {6, 9, 7} setmetatable(t, { __newindex = function(t,i,v) rawset(t, i * 10, v) end }) t[1] = 17 t[5] = 1 return concat(t[1],t[5],t[50]) end)() == "17,nil,1")
-- userdata access
assert((function() local ud = newproxy(true) getmetatable(ud).__index = function(ud,i) return i * 10 end return ud[2] end)() == 20)
assert((function() local ud = newproxy(true) getmetatable(ud).__index = function() return function(self, i) return i * 10 end end return ud:meow(2) end)() == 20)
-- and/or -- and/or
-- rhs is a constant -- rhs is a constant
assert((function() local a = 1 a = a and 2 return a end)() == 2) assert((function() local a = 1 a = a and 2 return a end)() == 2)
@ -462,7 +464,7 @@ assert((function() a = {} b = {} mt = { __eq = function(l, r) return #l == #r en
-- metatable ops -- metatable ops
local function vec3t(x, y, z) local function vec3t(x, y, z)
return setmetatable({ x=x, y=y, z=z}, { return setmetatable({x=x, y=y, z=z}, {
__add = function(l, r) return vec3t(l.x + r.x, l.y + r.y, l.z + r.z) end, __add = function(l, r) return vec3t(l.x + r.x, l.y + r.y, l.z + r.z) end,
__sub = function(l, r) return vec3t(l.x - r.x, l.y - r.y, l.z - r.z) end, __sub = function(l, r) return vec3t(l.x - r.x, l.y - r.y, l.z - r.z) end,
__mul = function(l, r) return type(r) == "number" and vec3t(l.x * r, l.y * r, l.z * r) or vec3t(l.x * r.x, l.y * r.y, l.z * r.z) end, __mul = function(l, r) return type(r) == "number" and vec3t(l.x * r, l.y * r, l.z * r) or vec3t(l.x * r.x, l.y * r.y, l.z * r.z) end,

View File

@ -37,6 +37,7 @@ coroutine.resume(co2, 0 / 0, 42)
assert(debug.traceback(co2) == "debug.lua:31 function halp\n") assert(debug.traceback(co2) == "debug.lua:31 function halp\n")
assert(debug.info(co2, 0, "l") == 31) assert(debug.info(co2, 0, "l") == 31)
assert(debug.info(co2, 0, "f") == halp)
-- info errors -- info errors
function qux(...) function qux(...)

View File

@ -260,8 +260,7 @@ local a,b = loadstring(s)
assert(not a) assert(not a)
--assert(string.find(b, "line 2")) --assert(string.find(b, "line 2"))
-- Test for CLI-28786 -- The xpcall is intentionally going to cause an exception
-- The xpcall is intentially going to cause an exception
-- followed by a forced exception in the error handler. -- followed by a forced exception in the error handler.
-- If the secondary handler isn't trapped, it will cause -- If the secondary handler isn't trapped, it will cause
-- the unit test to fail. If the xpcall captures the -- the unit test to fail. If the xpcall captures the
@ -281,6 +280,19 @@ coroutine.wrap(function()
assert(not pcall(debug.getinfo, coroutine.running(), 0, ">")) assert(not pcall(debug.getinfo, coroutine.running(), 0, ">"))
end)() end)()
-- loadstring chunk truncation
local a,b = loadstring("nope", "@short")
assert(not a and b:match('[^ ]+') == "short:1:")
local a,b = loadstring("nope", "@" .. string.rep("thisisaverylongstringitssolongthatitwontfitintotheinternalbufferprovidedtovariousdebugfacilities", 10))
assert(not a and b:match('[^ ]+') == "...wontfitintotheinternalbufferprovidedtovariousdebugfacilitiesthisisaverylongstringitssolongthatitwontfitintotheinternalbufferprovidedtovariousdebugfacilitiesthisisaverylongstringitssolongthatitwontfitintotheinternalbufferprovidedtovariousdebugfacilities:1:")
local a,b = loadstring("nope", "=short")
assert(not a and b:match('[^ ]+') == "short:1:")
local a,b = loadstring("nope", "=" .. string.rep("thisisaverylongstringitssolongthatitwontfitintotheinternalbufferprovidedtovariousdebugfacilities", 10))
assert(not a and b:match('[^ ]+') == "thisisaverylongstringitssolongthatitwontfitintotheinternalbufferprovidedtovariousdebugfacilitiesthisisaverylongstringitssolongthatitwontfitintotheinternalbufferprovidedtovariousdebugfacilitiesthisisaverylongstringitssolongthatitwontfitintotheinternalbuffe:1:")
-- arith errors -- arith errors
function ecall(fn, ...) function ecall(fn, ...)
local ok, err = pcall(fn, ...) local ok, err = pcall(fn, ...)

View File

@ -180,6 +180,11 @@ x,y,z=nil
collectgarbage() collectgarbage()
assert(next(a) == string.rep('$', 11)) assert(next(a) == string.rep('$', 11))
-- shrinking tables reduce their capacity; confirming the shrinking is difficult but we can at least test the surface level behavior
a = {}; setmetatable(a, {__mode = 'ks'})
for i=1,lim do a[{}] = i end
collectgarbage()
assert(next(a) == nil)
-- testing userdata -- testing userdata
collectgarbage("stop") -- stop collection collectgarbage("stop") -- stop collection
@ -315,8 +320,6 @@ do
end end
collectgarbage() collectgarbage()
end end
return('OK') return('OK')

View File

@ -289,6 +289,7 @@ assert(math.sqrt("4") == 2)
assert(math.tanh("0") == 0) assert(math.tanh("0") == 0)
assert(math.tan("0") == 0) assert(math.tan("0") == 0)
assert(math.clamp("0", 2, 3) == 2) assert(math.clamp("0", 2, 3) == 2)
assert(math.clamp("4", 2, 3) == 3)
assert(math.sign("2") == 1) assert(math.sign("2") == 1)
assert(math.sign("-2") == -1) assert(math.sign("-2") == -1)
assert(math.sign("0") == 0) assert(math.sign("0") == 0)

View File

@ -139,6 +139,12 @@ assert(selectmany(1, 10, 20, 30) == "10,20,30")
assert(selectone(2, 10, 20, 30) == 20) assert(selectone(2, 10, 20, 30) == 20)
assert(selectmany(2, 10, 20, 30) == "20,30") assert(selectmany(2, 10, 20, 30) == "20,30")
assert(selectone(3, 10, 20, 30) == 30)
assert(selectmany(3, 10, 20, 30) == "30")
assert(selectone(4, 10, 20, 30) == nil)
assert(selectmany(4, 10, 20, 30) == "")
assert(selectone(-2, 10, 20, 30) == 20) assert(selectone(-2, 10, 20, 30) == 20)
assert(selectmany(-2, 10, 20, 30) == "20,30") assert(selectmany(-2, 10, 20, 30) == "20,30")

View File

@ -87,9 +87,18 @@ assert(pcall(function() local t = {} rawset(t, vector(0/0, 2, 3), 1) end) == fal
-- make sure we cover both builtin and C impl -- make sure we cover both builtin and C impl
assert(vector(1, 2, 4) == vector("1", "2", "4")) assert(vector(1, 2, 4) == vector("1", "2", "4"))
-- validate component access (both cases)
assert(vector(1, 2, 3).x == 1)
assert(vector(1, 2, 3).X == 1)
assert(vector(1, 2, 3).y == 2)
assert(vector(1, 2, 3).Y == 2)
assert(vector(1, 2, 3).z == 3)
assert(vector(1, 2, 3).Z == 3)
-- additional checks for 4-component vectors -- additional checks for 4-component vectors
if vector_size == 4 then if vector_size == 4 then
assert(vector(1, 2, 3, 4).w == 4) assert(vector(1, 2, 3, 4).w == 4)
assert(vector(1, 2, 3, 4).W == 4)
end end
return 'OK' return 'OK'

View File

@ -7,10 +7,20 @@
# The result of analysis is a .svg file which can be viewed in a browser # The result of analysis is a .svg file which can be viewed in a browser
# To generate these dumps, use luaC_dump, ideally preceded by luaC_fullgc # To generate these dumps, use luaC_dump, ideally preceded by luaC_fullgc
import argparse
import json import json
import sys import sys
import svg import svg
argumentParser = argparse.ArgumentParser(description='Luau heap snapshot analyzer')
argumentParser.add_argument('--split', dest = 'split', type = str, default = 'none', help = 'Perform additional root split using memory categories', choices = ['none', 'custom', 'all'])
argumentParser.add_argument('snapshot')
argumentParser.add_argument('snapshotnew', nargs='?')
arguments = argumentParser.parse_args()
class Node(svg.Node): class Node(svg.Node):
def __init__(self): def __init__(self):
svg.Node.__init__(self) svg.Node.__init__(self)
@ -30,14 +40,14 @@ class Node(svg.Node):
return "{} ({:,} bytes, {:.1%}); self: {:,} bytes in {:,} objects".format(self.name, self.width, self.width / root.width, self.size, self.count) return "{} ({:,} bytes, {:.1%}); self: {:,} bytes in {:,} objects".format(self.name, self.width, self.width / root.width, self.size, self.count)
# load files # load files
if len(sys.argv) == 2: if arguments.snapshotnew == None:
dumpold = None dumpold = None
with open(sys.argv[1]) as f: with open(arguments.snapshot) as f:
dump = json.load(f) dump = json.load(f)
else: else:
with open(sys.argv[1]) as f: with open(arguments.snapshot) as f:
dumpold = json.load(f) dumpold = json.load(f)
with open(sys.argv[2]) as f: with open(arguments.snapshotnew) as f:
dump = json.load(f) dump = json.load(f)
# reachability analysis: how much of the heap is reachable from roots? # reachability analysis: how much of the heap is reachable from roots?
@ -111,12 +121,15 @@ while offset < len(queue):
if "object" in obj: if "object" in obj:
queue.append((obj["object"], node)) queue.append((obj["object"], node))
def annotateContainedCategories(node): def annotateContainedCategories(node, start):
for obj in node.objects: for obj in node.objects:
if obj["cat"] < start:
obj["cat"] = 0
node.categories.add(obj["cat"]) node.categories.add(obj["cat"])
for child in node.children.values(): for child in node.children.values():
annotateContainedCategories(child) annotateContainedCategories(child, start)
for cat in child.categories: for cat in child.categories:
node.categories.add(cat) node.categories.add(cat)
@ -172,9 +185,11 @@ def splitIntoCategories(root):
return result return result
# temporarily disabled because it makes FG harder to read, maybe this should be a separate command line option? if dump["stats"].get("categories") and arguments.split != 'none':
if dump["stats"].get("categories") and False: if arguments.split == 'custom':
annotateContainedCategories(root) annotateContainedCategories(root, 128)
else:
annotateContainedCategories(root, 0)
root = splitIntoCategories(root) root = splitIntoCategories(root)

View File

@ -452,7 +452,7 @@ def display(root, title, colors, flip = False):
.replace("$gradient-start", gradient_start) .replace("$gradient-start", gradient_start)
.replace("$gradient-end", gradient_end) .replace("$gradient-end", gradient_end)
.replace("$height", str(svgheight)) .replace("$height", str(svgheight))
.replace("$status", str(svgheight - 16 + 3)) .replace("$status", str((svgheight - 16 + 3 if flip else 3 * 16 - 3)))
.replace("$flip", str(int(flip))) .replace("$flip", str(int(flip)))
) )