Sync to upstream/release/514

This commit is contained in:
Arseny Kapoulkine 2022-02-11 10:43:14 -08:00
parent bbae466006
commit e9bf182585
48 changed files with 1100 additions and 268 deletions

View File

@ -86,6 +86,8 @@ struct OwningAutocompleteResult
};
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);
} // namespace Luau

View File

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

View File

@ -93,7 +93,7 @@ struct Tarjan
// This should never be null; ensure you initialize it before calling
// substitution methods.
const TxnLog* log;
const TxnLog* log = nullptr;
std::vector<TypeId> edgesTy;
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
// over the map when we concatenate.
std::unordered_map<TypeId, std::unique_ptr<PendingType>> typeVarChanges;
std::unordered_map<TypePackId, std::unique_ptr<PendingTypePack>> typePackChanges;
std::unordered_map<TypeId, std::unique_ptr<PendingType>, DenseHashPointer> typeVarChanges;
std::unordered_map<TypePackId, std::unique_ptr<PendingTypePack>, DenseHashPointer> typePackChanges;
TxnLog* parent = nullptr;

View File

@ -103,6 +103,11 @@ struct GenericTypeDefinitions
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
// within a program are borrowed pointers into this set.
struct TypeChecker
@ -411,6 +416,12 @@ public:
private:
int checkRecursionCount = 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

View File

@ -54,9 +54,6 @@ struct TypePackVar
bool persistent = false;
// 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;
};

View File

@ -449,9 +449,6 @@ struct TypeVar final
std::optional<std::string> documentationSymbol;
// 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;
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
#include "Luau/BuiltinDefinitions.h"
LUAU_FASTFLAGVARIABLE(LuauFixTonumberReturnType, false)
namespace Luau
{
@ -115,6 +113,7 @@ declare function gcinfo(): number
declare function error<T>(message: T, level: number?)
declare function tostring<T>(value: T): string
declare function tonumber<T>(value: T, radix: number?): number?
declare function rawequal<T1, T2>(a: T1, b: T2): boolean
declare function rawget<K, V>(tab: {[K]: V}, k: K): V
@ -200,14 +199,7 @@ declare function gcinfo(): number
std::string getBuiltinDefinitionSource()
{
std::string result = 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;
return kBuiltinDefinitionLuaSrc;
}
} // namespace Luau

View File

@ -43,6 +43,7 @@ static const char* kWarningNames[] = {
"DeprecatedApi",
"TableOperations",
"DuplicateCondition",
"MisleadingAndOr",
};
// clang-format on
@ -2040,18 +2041,28 @@ private:
const Property* prop = lookupClassProp(cty, node->index.value);
if (prop && prop->deprecated)
{
if (!prop->deprecatedSuggestion.empty())
emitWarning(*context, LintWarning::Code_DeprecatedApi, node->location, "Member '%s.%s' is deprecated, use '%s' instead",
cty->name.c_str(), node->index.value, prop->deprecatedSuggestion.c_str());
else
emitWarning(*context, LintWarning::Code_DeprecatedApi, node->location, "Member '%s.%s' is deprecated", cty->name.c_str(),
node->index.value);
}
report(node->location, *prop, cty->name.c_str(), node->index.value);
}
else if (const TableTypeVar* tty = get<TableTypeVar>(follow(*ty)))
{
auto prop = tty->props.find(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;
}
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
@ -2257,6 +2268,39 @@ private:
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
{
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)
{
ScopePtr current = env;
@ -2522,6 +2606,9 @@ std::vector<LintWarning> lint(AstStat* root, const AstNameTable& names, const Sc
if (context.warningEnabled(LintWarning::Code_DuplicateLocal))
LintDuplicateLocal::process(context);
if (context.warningEnabled(LintWarning::Code_MisleadingAndOr))
LintMisleadingAndOr::process(context);
std::sort(context.result.begin(), context.result.end(), WarningComparator());
return context.result;

View File

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

View File

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

View File

@ -250,6 +250,10 @@ PendingTypePack* TxnLog::queue(TypePackId tp)
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)
{
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
{
// 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)
{
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(LuauIfElseBranchTypeUnion, false)
LUAU_FASTFLAGVARIABLE(LuauIfElseExpectedType2, false)
LUAU_FASTFLAGVARIABLE(LuauImmutableTypes, false)
LUAU_FASTFLAGVARIABLE(LuauLengthOnCompositeType, false)
LUAU_FASTFLAGVARIABLE(LuauNoSealedTypeMod, false)
LUAU_FASTFLAGVARIABLE(LuauQuantifyInPlace2, false)
LUAU_FASTFLAGVARIABLE(LuauSealExports, false)
LUAU_FASTFLAGVARIABLE(LuauSingletonTypes, false)
LUAU_FASTFLAGVARIABLE(LuauDiscriminableUnions, false)
LUAU_FASTFLAGVARIABLE(LuauDiscriminableUnions2, false)
LUAU_FASTFLAGVARIABLE(LuauTypeAliasDefaults, false)
LUAU_FASTFLAGVARIABLE(LuauExpectedTypesOfProperties, false)
LUAU_FASTFLAGVARIABLE(LuauErrorRecoveryType, false)
@ -47,7 +48,10 @@ LUAU_FASTFLAGVARIABLE(LuauProperTypeLevels, false)
LUAU_FASTFLAGVARIABLE(LuauAscribeCorrectLevelToInferredProperitesOfFreeTables, false)
LUAU_FASTFLAG(LuauUnionTagMatchFix)
LUAU_FASTFLAGVARIABLE(LuauUnsealedTableLiteral, false)
LUAU_FASTFLAGVARIABLE(LuauTwoPassAliasDefinitionFix, false)
LUAU_FASTFLAGVARIABLE(LuauAssertStripsFalsyTypes, false)
LUAU_FASTFLAGVARIABLE(LuauReturnAnyInsteadOfICE, false) // Eventually removed as false.
LUAU_FASTFLAGVARIABLE(LuauAnotherTypeLevelFix, false)
namespace Luau
{
@ -213,6 +217,11 @@ static bool isMetamethod(const Name& name)
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)
: resolver(resolver)
, iceHandler(iceHandler)
@ -225,6 +234,7 @@ TypeChecker::TypeChecker(ModuleResolver* resolver, InternalErrorReporter* iceHan
, anyType(getSingletonTypes().anyType)
, optionalNumberType(getSingletonTypes().optionalNumberType)
, anyTypePack(getSingletonTypes().anyTypePack)
, duplicateTypeAliases{{false, {}}}
{
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();
}
if (FFlag::LuauTwoPassAliasDefinitionFix)
duplicateTypeAliases.clear();
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 (FFlag::LuauTwoPassAliasDefinitionFix && typealias->name == Parser::errorName)
continue;
auto& bindings = typealias->exported ? scope->exportedTypeBindings : scope->privateTypeBindings;
Name name = typealias->name.value;
@ -1176,6 +1192,10 @@ void TypeChecker::check(const ScopePtr& scope, const AstStatTypeAlias& typealias
// Once with forwardDeclare, and once without.
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;
if (auto it = scope->exportedTypeBindings.find(name); it != scope->exportedTypeBindings.end())
binding = it->second;
@ -1192,6 +1212,8 @@ void TypeChecker::check(const ScopePtr& scope, const AstStatTypeAlias& typealias
reportError(TypeError{typealias.location, DuplicateTypeDefinition{name, location}});
bindingsMap[name] = TypeFun{binding->typeParams, binding->typePackParams, errorRecoveryType(anyType)};
if (FFlag::LuauTwoPassAliasDefinitionFix)
duplicateTypeAliases.insert({typealias.exported, name});
}
else
{
@ -1211,6 +1233,11 @@ void TypeChecker::check(const ScopePtr& scope, const AstStatTypeAlias& typealias
}
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)
ice("Not predeclared");
@ -1235,7 +1262,8 @@ void TypeChecker::check(const ScopePtr& scope, const AstStatTypeAlias& typealias
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 (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(),
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
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
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)))
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))
return {vtp->ty, std::move(result.predicates)};
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
ice("Unknown TypePack type!", expr.location);
}
@ -1614,11 +1655,23 @@ std::optional<TypeId> TypeChecker::getIndexTypeFromType(
tablify(type);
const PrimitiveTypeVar* primitiveType = get<PrimitiveTypeVar>(type);
if (primitiveType && primitiveType->type == PrimitiveTypeVar::String)
if (FFlag::LuauDiscriminableUnions2)
{
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;
}
}
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))
@ -2476,7 +2529,7 @@ ExprResult<TypeId> TypeChecker::checkExpr(const ScopePtr& scope, const AstExprBi
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)}}};
}
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);
// 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)}}};
}
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))
return {booleanType, {std::move(*predicate)}};
ExprResult<TypeId> lhs = checkExpr(scope, *expr.left, std::nullopt, /*forceSingleton=*/FFlag::LuauDiscriminableUnions);
ExprResult<TypeId> rhs = checkExpr(scope, *expr.right, 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::LuauDiscriminableUnions2);
PredicateVec predicates;
@ -2785,12 +2838,16 @@ TypeId TypeChecker::checkLValueBinding(const ScopePtr& scope, const AstExprIndex
}
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)};
return resultType;
}
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);
return resultType;
}
@ -4195,6 +4252,9 @@ TypeId TypeChecker::checkRequire(const ScopePtr& scope, const ModuleInfo& module
return errorRecoveryType(scope);
}
if (FFlag::LuauImmutableTypes)
return *moduleType;
SeenTypes seenTypes;
SeenTypePacks seenTypePacks;
CloneState cloneState;
@ -4978,6 +5038,7 @@ TypeId TypeChecker::resolveType(const ScopePtr& scope, const AstType& annotation
if (notEnoughParameters && hasDefaultParameters)
{
// 'applyTypeFunction' is used to substitute default types that reference previous generic types
applyTypeFunction.log = TxnLog::empty();
applyTypeFunction.typeArguments.clear();
applyTypeFunction.typePackArguments.clear();
applyTypeFunction.currentModule = currentModule;
@ -5445,7 +5506,7 @@ GenericTypeDefinitions TypeChecker::createGenericTypes(const ScopePtr& scope, st
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;
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.
@ -5658,7 +5719,7 @@ void TypeChecker::resolve(const TruthyPredicate& truthyP, ErrorVec& errVec, Refi
return std::nullopt;
};
if (FFlag::LuauDiscriminableUnions)
if (FFlag::LuauDiscriminableUnions2)
{
std::optional<TypeId> ty = resolveLValue(refis, scope, truthyP.lvalue);
if (ty && fromOr)
@ -5771,7 +5832,7 @@ void TypeChecker::resolve(const IsAPredicate& isaP, ErrorVec& errVec, Refinement
return res;
};
if (FFlag::LuauDiscriminableUnions)
if (FFlag::LuauDiscriminableUnions2)
{
refineLValue(isaP.lvalue, refis, scope, predicate);
}
@ -5846,7 +5907,7 @@ void TypeChecker::resolve(const TypeGuardPredicate& typeguardP, ErrorVec& errVec
if (auto it = primitives.find(typeguardP.kind); it != primitives.end())
{
if (FFlag::LuauDiscriminableUnions)
if (FFlag::LuauDiscriminableUnions2)
{
refineLValue(typeguardP.lvalue, refis, scope, it->second(sense));
return;
@ -5868,7 +5929,7 @@ void TypeChecker::resolve(const TypeGuardPredicate& typeguardP, ErrorVec& errVec
}
auto fail = [&](const TypeErrorData& err) {
if (!FFlag::LuauDiscriminableUnions)
if (!FFlag::LuauDiscriminableUnions2)
errVec.push_back(TypeError{typeguardP.location, err});
addRefinement(refis, typeguardP.lvalue, errorRecoveryType(scope));
};
@ -5900,7 +5961,7 @@ void TypeChecker::resolve(const EqPredicate& eqP, ErrorVec& errVec, RefinementMa
return {ty};
};
if (FFlag::LuauDiscriminableUnions)
if (FFlag::LuauDiscriminableUnions2)
{
std::vector<TypeId> rhs = options(eqP.type);

View File

@ -28,6 +28,7 @@ LUAU_FASTFLAGVARIABLE(LuauMetatableAreEqualRecursion, false)
LUAU_FASTFLAGVARIABLE(LuauRefactorTypeVarQuestions, false)
LUAU_FASTFLAG(LuauErrorRecoveryType)
LUAU_FASTFLAG(LuauUnionTagMatchFix)
LUAU_FASTFLAG(LuauDiscriminableUnions2)
namespace Luau
{
@ -393,7 +394,8 @@ bool hasLength(TypeId ty, DenseHashSet<TypeId>& seen, int* recursionCount)
if (seen.contains(ty))
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;
if (auto uty = get<UnionTypeVar>(ty))

View File

@ -15,6 +15,7 @@
LUAU_FASTINT(LuauTypeInferRecursionLimit);
LUAU_FASTINT(LuauTypeInferTypePackLoopLimit);
LUAU_FASTFLAGVARIABLE(LuauCommittingTxnLogFreeTpPromote, false)
LUAU_FASTFLAG(LuauImmutableTypes)
LUAU_FASTFLAG(LuauUseCommittingTxnLog)
LUAU_FASTINTVARIABLE(LuauTypeInferIterationLimit, 2000);
LUAU_FASTFLAGVARIABLE(LuauTableSubtypingVariance2, false);
@ -24,6 +25,7 @@ LUAU_FASTFLAG(LuauErrorRecoveryType);
LUAU_FASTFLAG(LuauProperTypeLevels);
LUAU_FASTFLAGVARIABLE(LuauUnifyPackTails, false)
LUAU_FASTFLAGVARIABLE(LuauUnionTagMatchFix, false)
LUAU_FASTFLAGVARIABLE(LuauFollowWithCommittingTxnLogInAnyUnification, false)
namespace Luau
{
@ -32,11 +34,13 @@ struct PromoteTypeLevels
{
DEPRECATED_TxnLog& DEPRECATED_log;
TxnLog& log;
const TypeArena* typeArena = nullptr;
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)
, log(log)
, typeArena(typeArena)
, minLevel(minLevel)
{
}
@ -65,8 +69,12 @@ struct PromoteTypeLevels
}
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;
}
@ -83,12 +91,20 @@ struct PromoteTypeLevels
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));
return true;
}
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)
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};
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};
visitTypeVarOnce(tp, ptl, seen);
}
struct SkipCacheForType
{
SkipCacheForType(const DenseHashMap<TypeId, bool>& skipCacheForType)
SkipCacheForType(const DenseHashMap<TypeId, bool>& skipCacheForType, const TypeArena* typeArena)
: skipCacheForType(skipCacheForType)
, typeArena(typeArena)
{
}
@ -152,6 +177,10 @@ struct SkipCacheForType
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);
if (ttv.boundTo)
@ -172,6 +201,10 @@ struct SkipCacheForType
template<typename 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);
if (prev && *prev)
@ -184,8 +217,12 @@ struct SkipCacheForType
}
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;
}
@ -208,6 +245,7 @@ struct SkipCacheForType
}
const DenseHashMap<TypeId, bool>& skipCacheForType;
const TypeArena* typeArena = nullptr;
bool result = false;
};
@ -422,13 +460,13 @@ void Unifier::tryUnify_(TypeId subTy, TypeId superTy, bool isFunctionCall, bool
{
if (FFlag::LuauUseCommittingTxnLog)
{
promoteTypeLevels(DEPRECATED_log, log, superLevel, subTy);
promoteTypeLevels(DEPRECATED_log, log, types, superLevel, subTy);
log.replace(superTy, BoundTypeVar(subTy));
}
else
{
if (FFlag::LuauProperTypeLevels)
promoteTypeLevels(DEPRECATED_log, log, superLevel, subTy);
promoteTypeLevels(DEPRECATED_log, log, types, superLevel, subTy);
else if (auto subLevel = getMutableLevel(subTy))
{
if (!subLevel->subsumes(superFree->level))
@ -466,13 +504,13 @@ void Unifier::tryUnify_(TypeId subTy, TypeId superTy, bool isFunctionCall, bool
{
if (FFlag::LuauUseCommittingTxnLog)
{
promoteTypeLevels(DEPRECATED_log, log, subLevel, superTy);
promoteTypeLevels(DEPRECATED_log, log, types, subLevel, superTy);
log.replace(subTy, BoundTypeVar(superTy));
}
else
{
if (FFlag::LuauProperTypeLevels)
promoteTypeLevels(DEPRECATED_log, log, subLevel, superTy);
promoteTypeLevels(DEPRECATED_log, log, types, subLevel, superTy);
else if (auto superLevel = getMutableLevel(superTy))
{
if (!superLevel->subsumes(subFree->level))
@ -849,7 +887,7 @@ void Unifier::cacheResult(TypeId subTy, TypeId superTy)
return;
auto skipCacheFor = [this](TypeId ty) {
SkipCacheForType visitor{sharedState.skipCacheForType};
SkipCacheForType visitor{sharedState.skipCacheForType, types};
visitTypeVarOnce(ty, visitor, sharedState.seenAny);
sharedState.skipCacheForType[ty] = visitor.result;
@ -1637,32 +1675,35 @@ void Unifier::tryUnifyFunctions(TypeId subTy, TypeId superTy, bool isFunctionCal
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);
FunctionTypeVar* newSubFtv = getMutable<FunctionTypeVar>(newSubTy);
LUAU_ASSERT(newSubFtv);
newSubFtv->definition = superFunction->definition;
if (superFunction->definition && !subFunction->definition && !subTy->persistent)
{
PendingType* newSubTy = log.queue(subTy);
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);
FunctionTypeVar* newSuperFtv = getMutable<FunctionTypeVar>(newSuperTy);
LUAU_ASSERT(newSuperFtv);
newSuperFtv->definition = subFunction->definition;
}
}
else
{
if (superFunction->definition && !subFunction->definition && !subTy->persistent)
{
subFunction->definition = superFunction->definition;
}
else if (!superFunction->definition && subFunction->definition && !superTy->persistent)
{
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)
{
a = follow(a);
a = FFlag::LuauFollowWithCommittingTxnLogInAnyUnification ? state.log.follow(a) : follow(a);
if (seenTypePacks.find(a))
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,
TypeId anyType, TypePackId anyTypePack)
const TypeArena* typeArena, TypeId anyType, TypePackId anyTypePack)
{
while (!queue.empty())
{
@ -2746,8 +2787,14 @@ static void tryUnifyWithAny(std::vector<TypeId>& queue, Unifier& state, DenseHas
{
TypeId ty = state.log.follow(queue.back());
queue.pop_back();
// Types from other modules don't have free types
if (FFlag::LuauImmutableTypes && ty->owningArena != typeArena)
continue;
if (seen.find(ty))
continue;
seen.insert(ty);
if (state.log.getMutable<FreeTypeVar>(ty))
@ -2853,7 +2900,7 @@ void Unifier::tryUnifyWithAny(TypeId subTy, TypeId anyTy)
sharedState.tempSeenTy.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)
@ -2869,7 +2916,7 @@ void Unifier::tryUnifyWithAny(TypePackId subTy, TypePackId 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)

View File

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

86
CLI/Ast.cpp Normal file
View File

@ -0,0 +1,86 @@
// This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details
#include <optional>
#include "Luau/Common.h"
#include "Luau/Ast.h"
#include "Luau/JsonEncoder.h"
#include "Luau/Parser.h"
#include "Luau/ParseOptions.h"
#include "FileUtils.h"
static void displayHelp(const char* argv0)
{
printf("Usage: %s [file]\n", argv0);
}
static int assertionHandler(const char* expr, const char* file, int line, const char* function)
{
printf("%s(%d): ASSERTION FAILED: %s\n", file, line, expr);
return 1;
}
int main(int argc, char** argv)
{
Luau::assertHandler() = assertionHandler;
for (Luau::FValue<bool>* flag = Luau::FValue<bool>::list; flag; flag = flag->next)
if (strncmp(flag->name, "Luau", 4) == 0)
flag->value = true;
if (argc >= 2 && strcmp(argv[1], "--help") == 0)
{
displayHelp(argv[0]);
return 0;
}
else if (argc < 2)
{
displayHelp(argv[0]);
return 1;
}
const char* name = argv[1];
std::optional<std::string> maybeSource = std::nullopt;
if (strcmp(name, "-") == 0)
{
maybeSource = readStdin();
}
else
{
maybeSource = readFile(name);
}
if (!maybeSource)
{
fprintf(stderr, "Couldn't read source %s\n", name);
return 1;
}
std::string source = *maybeSource;
Luau::Allocator allocator;
Luau::AstNameTable names(allocator);
Luau::ParseOptions options;
options.supportContinueStatement = true;
options.allowTypeAnnotations = true;
options.allowDeclarationSyntax = true;
Luau::ParseResult parseResult = Luau::Parser::parse(source.data(), source.size(), names, allocator, options);
if (parseResult.errors.size() > 0)
{
fprintf(stderr, "Parse errors were encountered:\n");
for (const Luau::ParseError& error : parseResult.errors)
{
fprintf(stderr, " %s - %s\n", toString(error.getLocation()).c_str(), error.getMessage().c_str());
}
fprintf(stderr, "\n");
}
printf("%s", Luau::toJson(parseResult.root).c_str());
return parseResult.errors.size() > 0 ? 1 : 0;
}

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
#include "Repl.h"
#include "lua.h"
#include "lualib.h"
@ -38,13 +40,14 @@ enum class CompileFormat
struct GlobalOptions
{
int optimizationLevel = 1;
int debugLevel = 1;
} globalOptions;
static Luau::CompileOptions copts()
{
Luau::CompileOptions result = {};
result.optimizationLevel = globalOptions.optimizationLevel;
result.debugLevel = 1;
result.debugLevel = globalOptions.debugLevel;
result.coverageLevel = coverageActive() ? 2 : 0;
return result;
@ -240,9 +243,8 @@ std::string runCode(lua_State* L, const std::string& source)
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;
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.
completion += "(";
}
ic_add_completion_ex(cenv, completion.data(), key.data(), nullptr);
addCompletionCallback(completion, std::string(key));
}
}
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
// 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_pushlstring(L, "string", 6);
lua_rawget(L, -2);
lua_remove(L, -2);
lua_remove(L, -2); // Remove the global table
LUAU_ASSERT(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);
}
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)
{
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)
{
auto* L = reinterpret_cast<lua_State*>(ic_completion_arg(cenv));
// 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);
ic_complete_word(cenv, editBuffer, icGetCompletions, isMethodOrFunctionChar);
}
struct LinenoiseScopedHistory
@ -372,19 +387,20 @@ static void runReplImpl(lua_State* L)
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)
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;
}
if (!buffer.empty())
buffer += "\n";
buffer += line;
buffer += line.get();
std::string error = runCode(L, buffer);
@ -400,7 +416,6 @@ static void runReplImpl(lua_State* L)
ic_history_add(buffer.c_str());
buffer.clear();
free((void*)line);
}
}
@ -504,7 +519,7 @@ static bool compileFile(const char* name, CompileFormat format)
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);
}
@ -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(" -h, --help: Display this usage message.\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(" --timetrace: record compiler time tracing information into trace.json\n");
}
@ -620,6 +636,16 @@ int replMain(int argc, char** argv)
}
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)
{
profile = 10000; // default to 10 KHz

View File

@ -3,10 +3,15 @@
#include "lua.h"
#include <functional>
#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
// so they can be included by unit tests.
int replMain(int argc, char** argv);
void setupState(lua_State* L);
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

@ -5,13 +5,20 @@ if(EXT_PLATFORM_STRING)
endif()
cmake_minimum_required(VERSION 3.0)
project(Luau LANGUAGES CXX C)
option(LUAU_BUILD_CLI "Build CLI" ON)
option(LUAU_BUILD_TESTS "Build tests" ON)
option(LUAU_BUILD_WEB "Build Web module" OFF)
option(LUAU_WERROR "Warnings as errors" OFF)
option(LUAU_STATIC_CRT "Link with the static CRT (/MT)" OFF)
if(LUAU_STATIC_CRT)
cmake_minimum_required(VERSION 3.15)
cmake_policy(SET CMP0091 NEW)
set(CMAKE_MSVC_RUNTIME_LIBRARY "MultiThreaded$<$<CONFIG:Debug>:Debug>")
endif()
project(Luau LANGUAGES CXX C)
add_library(Luau.Ast STATIC)
add_library(Luau.Compiler STATIC)
add_library(Luau.Analysis STATIC)
@ -21,10 +28,12 @@ add_library(isocline STATIC)
if(LUAU_BUILD_CLI)
add_executable(Luau.Repl.CLI)
add_executable(Luau.Analyze.CLI)
add_executable(Luau.Ast.CLI)
# This also adds target `name` on Linux/macOS and `name.exe` on Windows
set_target_properties(Luau.Repl.CLI PROPERTIES OUTPUT_NAME luau)
set_target_properties(Luau.Analyze.CLI PROPERTIES OUTPUT_NAME luau-analyze)
set_target_properties(Luau.Ast.CLI PROPERTIES OUTPUT_NAME luau-ast)
endif()
if(LUAU_BUILD_TESTS)
@ -98,6 +107,7 @@ endif()
if(LUAU_BUILD_CLI)
target_compile_options(Luau.Repl.CLI PRIVATE ${LUAU_OPTIONS})
target_compile_options(Luau.Analyze.CLI PRIVATE ${LUAU_OPTIONS})
target_compile_options(Luau.Ast.CLI PRIVATE ${LUAU_OPTIONS})
target_include_directories(Luau.Repl.CLI PRIVATE extern extern/isocline/include)
@ -111,6 +121,8 @@ if(LUAU_BUILD_CLI)
endif()
target_link_libraries(Luau.Analyze.CLI PRIVATE Luau.Analysis)
target_link_libraries(Luau.Ast.CLI PRIVATE Luau.Ast Luau.Analysis)
endif()
if(LUAU_BUILD_TESTS)

View File

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

View File

@ -15,7 +15,6 @@
#include <bitset>
#include <math.h>
LUAU_FASTFLAGVARIABLE(LuauCompileTableIndexOpt, false)
LUAU_FASTFLAG(LuauCompileSelectBuiltin2)
namespace Luau
@ -1182,18 +1181,9 @@ struct Compiler
const AstExprTable::Item& item = expr->items.data[i];
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));
}
else
{
AstExprConstantNumber* ckey = item.key->as<AstExprConstantNumber>();
indexSize += (ckey && ckey->value == double(indexSize + 1));
}
indexSize += (ckey && ckey->type == Constant::Type_Number && ckey->valueNumber == double(indexSize + 1));
}
// we only perform the optimization if we don't have any other []-keys
@ -1295,43 +1285,10 @@ struct Compiler
{
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);
}
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);
}
}
compileAssign(lv, rv);
}
// items without a key are set using SETLIST so that we can initialize large arrays quickly
else
@ -1439,8 +1396,7 @@ struct Compiler
uint8_t rt = compileExprAuto(expr->expr, rs);
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);
}
@ -1453,8 +1409,7 @@ struct Compiler
if (cid < 0)
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.emitAux(cid);
@ -1853,8 +1808,7 @@ struct Compiler
void compileLValueUse(const LValue& lv, uint8_t reg, bool set)
{
if (FFlag::LuauCompileTableIndexOpt)
setDebugLine(lv.location);
setDebugLine(lv.location);
switch (lv.kind)
{

View File

@ -193,6 +193,14 @@ if(TARGET Luau.Analyze.CLI)
CLI/Analyze.cpp)
endif()
if(TARGET Luau.Ast.CLI)
target_sources(Luau.Ast.CLI PRIVATE
CLI/Ast.cpp
CLI/FileUtils.h
CLI/FileUtils.cpp
)
endif()
if(TARGET Luau.UnitTest)
# Luau.UnitTest Sources
target_sources(Luau.UnitTest PRIVATE

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));
// i >= 1 && i <= n
if (unsigned(i - 1) <= unsigned(n))
if (unsigned(i - 1) < unsigned(n))
{
setobj2s(L, res, L->base - n + (i - 1));
return 1;

View File

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

View File

@ -8,6 +8,76 @@
#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)
#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 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 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
@ -107,24 +178,6 @@ const SizeClassConfig kSizeClassConfig;
#define metadata(block) (*(void**)(block))
#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
{
// list of pages with free blocks
@ -135,13 +188,12 @@ struct lua_Page
lua_Page* gcolistprev;
lua_Page* gcolistnext;
int busyBlocks;
int blockSize;
int pageSize; // page size in bytes, including page header
int blockSize; // block size in bytes, including block header (for non-GCO)
void* freeList;
int freeNext;
int pageSize;
void* freeList; // next free block in this page; linked with metadata()/freegcolink()
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
union
{
@ -177,7 +229,7 @@ static lua_Page* newpageold(lua_State* L, uint8_t sizeClass)
page->gcolistprev = NULL;
page->gcolistnext = NULL;
page->busyBlocks = 0;
page->pageSize = kPageSize;
page->blockSize = blockSize;
// 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
page->freeList = NULL;
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!)
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->gcolistnext = NULL;
page->busyBlocks = 0;
page->pageSize = pageSize;
page->blockSize = blockSize;
// 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
page->freeList = NULL;
page->freeNext = (blockCount - 1) * blockSize;
page->pageSize = pageSize;
page->busyBlocks = 0;
if (gcopageset)
{
@ -406,8 +458,7 @@ static void* newgcoblock(lua_State* L, int sizeClass)
page->next = NULL;
}
// the user data is right after the metadata
return (char*)block;
return 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);
LUAU_ASSERT(page && page->busyBlocks > 0);
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 (!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)
{
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;
@ -575,6 +630,8 @@ void luaM_freegco_(lua_State* L, GCObject* block, size_t osize, uint8_t memcat,
else
{
LUAU_ASSERT(page->busyBlocks == 1);
LUAU_ASSERT(size_t(page->blockSize) == osize);
LUAU_ASSERT((void*)block == page->data);
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;
*start = page->data + page->freeNext + page->blockSize;
*end = page->data + blockCount * page->blockSize;
LUAU_ASSERT(page->freeNext >= -page->blockSize && page->freeNext <= (blockCount - 1) * 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;
*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;)
{
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);

View File

@ -131,7 +131,7 @@ void luaO_chunkid(char* out, const char* source, size_t bufflen)
{
size_t l;
source++; /* skip the `@' */
bufflen -= sizeof(" '...' ");
bufflen -= sizeof("...");
l = strlen(source);
strcpy(out, "");
if (l > bufflen)
@ -144,7 +144,7 @@ void luaO_chunkid(char* out, const char* source, size_t bufflen)
else
{ /* out = [string "string"] */
size_t len = strcspn(source, "\n\r"); /* stop at first newline */
bufflen -= sizeof(" [string \"...\"] ");
bufflen -= sizeof("[string \"...\"]");
if (len > bufflen)
len = bufflen;
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')
{
setnvalue(ra, rb->value.v[ic]);
const float* v = rb->value.v; // silences ubsan when indexing v[]
setnvalue(ra, v[ic]);
VM_NEXT();
}

View File

@ -13,7 +13,12 @@
// $ gcc -c src/isocline.c
//-------------------------------------------------------------
#if !defined(IC_SEPARATE_OBJS)
# define _CRT_SECURE_NO_WARNINGS // for msvc
# ifndef _CRT_NONSTDC_NO_WARNINGS
# define _CRT_NONSTDC_NO_WARNINGS // for msvc
# endif
# ifndef _CRT_SECURE_NO_WARNINGS
# define _CRT_SECURE_NO_WARNINGS // for msvc
# endif
# define _XOPEN_SOURCE 700 // for wcwidth
# define _DEFAULT_SOURCE // ensure usleep stays visible with _XOPEN_SOURCE >= 700
# include "attr.c"

View File

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

View File

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

View File

@ -1392,19 +1392,31 @@ TEST_CASE_FIXTURE(Fixture, "DeprecatedApi")
{"DataCost", {typeChecker.numberType, /* 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);
LintResult result = lintTyped(R"(
return function (i: Instance)
i:Wait(1.0)
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
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[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")
@ -1475,9 +1487,11 @@ _ = (true and true) or true
_ = (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
_ = 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].location.begin.line + 1, 4);
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[6].text, "Condition has already been checked on column 15");
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")
@ -1528,4 +1543,19 @@ return foo, moo, a1, a2
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();

View File

@ -8,9 +8,22 @@
#include <iostream>
#include <memory>
#include <set>
#include <string>
#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
{
@ -34,6 +47,27 @@ public:
lua_pop(L, 1);
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;
private:
@ -115,3 +149,61 @@ TEST_CASE_FIXTURE(ReplFixture, "MultipleArguments")
}
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);
}
/*
* 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();

View File

@ -7,8 +7,6 @@
#include "doctest.h"
LUAU_FASTFLAG(LuauFixTonumberReturnType)
using namespace Luau;
LUAU_FASTFLAG(LuauUseCommittingTxnLog)
@ -850,11 +848,8 @@ TEST_CASE_FIXTURE(Fixture, "tonumber_returns_optional_number_type")
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")
@ -893,7 +888,7 @@ TEST_CASE_FIXTURE(Fixture, "assert_removes_falsy_types")
{
ScopedFastFlag sff[]{
{"LuauAssertStripsFalsyTypes", true},
{"LuauDiscriminableUnions", true},
{"LuauDiscriminableUnions2", true},
};
CheckResult result = check(R"(
@ -910,7 +905,7 @@ TEST_CASE_FIXTURE(Fixture, "assert_removes_falsy_types_even_from_type_pack_tail_
{
ScopedFastFlag sff[]{
{"LuauAssertStripsFalsyTypes", true},
{"LuauDiscriminableUnions", true},
{"LuauDiscriminableUnions2", true},
};
CheckResult result = check(R"(
@ -927,7 +922,7 @@ TEST_CASE_FIXTURE(Fixture, "assert_returns_false_and_string_iff_it_knows_the_fir
{
ScopedFastFlag sff[]{
{"LuauAssertStripsFalsyTypes", true},
{"LuauDiscriminableUnions", true},
{"LuauDiscriminableUnions2", true},
};
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.
TEST_CASE_FIXTURE(Fixture, "discriminate_from_x_not_equal_to_nil")
{
ScopedFastFlag sff{"LuauDiscriminableUnions", true};
ScopedFastFlag sff{"LuauDiscriminableUnions2", true};
CheckResult result = check(R"(
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>");
}
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();

View File

@ -6,7 +6,7 @@
#include "doctest.h"
LUAU_FASTFLAG(LuauDiscriminableUnions)
LUAU_FASTFLAG(LuauDiscriminableUnions2)
LUAU_FASTFLAG(LuauWeakEqConstraint)
LUAU_FASTFLAG(LuauQuantifyInPlace2)
@ -262,7 +262,7 @@ TEST_CASE_FIXTURE(Fixture, "typeguard_only_look_up_types_from_global_scope")
end
)");
if (FFlag::LuauDiscriminableUnions)
if (FFlag::LuauDiscriminableUnions2)
{
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")
{
ScopedFastFlag sff[] = {
{"LuauDiscriminableUnions", true},
{"LuauDiscriminableUnions2", 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")
{
ScopedFastFlag sff{"LuauDiscriminableUnions", true};
ScopedFastFlag sff{"LuauDiscriminableUnions2", true};
ScopedFastFlag sff2{"LuauWeakEqConstraint", true};
CheckResult result = check(R"(
@ -589,7 +589,7 @@ TEST_CASE_FIXTURE(Fixture, "type_narrow_to_vector")
end
)");
if (FFlag::LuauDiscriminableUnions)
if (FFlag::LuauDiscriminableUnions2)
{
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")
{
ScopedFastFlag sff[] = {
{"LuauDiscriminableUnions", true},
{"LuauDiscriminableUnions2", true},
{"LuauParseSingletonTypes", true},
{"LuauSingletonTypes", true},
};
@ -1028,7 +1028,7 @@ TEST_CASE_FIXTURE(Fixture, "discriminate_from_truthiness_of_x")
TEST_CASE_FIXTURE(Fixture, "discriminate_tag")
{
ScopedFastFlag sff[] = {
{"LuauDiscriminableUnions", true},
{"LuauDiscriminableUnions2", true},
{"LuauParseSingletonTypes", true},
{"LuauSingletonTypes", true},
};
@ -1069,7 +1069,7 @@ TEST_CASE_FIXTURE(Fixture, "narrow_boolean_to_true_or_false")
ScopedFastFlag sff[]{
{"LuauParseSingletonTypes", true},
{"LuauSingletonTypes", true},
{"LuauDiscriminableUnions", true},
{"LuauDiscriminableUnions2", true},
{"LuauAssertStripsFalsyTypes", true},
};
@ -1094,7 +1094,7 @@ TEST_CASE_FIXTURE(Fixture, "discriminate_on_properties_of_disjoint_tables_where_
ScopedFastFlag sff[]{
{"LuauParseSingletonTypes", true},
{"LuauSingletonTypes", true},
{"LuauDiscriminableUnions", true},
{"LuauDiscriminableUnions2", 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")
{
ScopedFastFlag sff[] = {
{"LuauDiscriminableUnions", true},
{"LuauDiscriminableUnions2", true},
{"LuauParseSingletonTypes", true},
{"LuauSingletonTypes", true},
};
@ -1157,7 +1157,7 @@ TEST_CASE_FIXTURE(RefinementClassFixture, "typeguard_cast_free_table_to_vector")
end
)");
if (FFlag::LuauDiscriminableUnions)
if (FFlag::LuauDiscriminableUnions2)
LUAU_REQUIRE_NO_ERRORS(result);
else
{

View File

@ -5164,4 +5164,151 @@ function x:Destroy(): () end
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();

View File

@ -273,4 +273,21 @@ TEST_CASE_FIXTURE(TryUnifyFixture, "recursive_metatable_getmatchtag")
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();

View File

@ -118,9 +118,7 @@ assert((function() return #_G end)() == 0)
assert((function() return #{1,2} end)() == 2)
assert((function() return #'g' end)() == 1)
local ud = newproxy(true)
getmetatable(ud).__len = function() return 42 end
assert((function() return #ud end)() == 42)
assert((function() local ud = newproxy(true) getmetatable(ud).__len = function() return 42 end return #ud end)() == 42)
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} 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
-- rhs is a constant
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
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,
__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,

View File

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

View File

@ -260,8 +260,7 @@ local a,b = loadstring(s)
assert(not a)
--assert(string.find(b, "line 2"))
-- Test for CLI-28786
-- The xpcall is intentially going to cause an exception
-- The xpcall is intentionally going to cause an exception
-- followed by a forced exception in the error handler.
-- If the secondary handler isn't trapped, it will cause
-- 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, ">"))
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
function ecall(fn, ...)
local ok, err = pcall(fn, ...)

View File

@ -180,6 +180,11 @@ x,y,z=nil
collectgarbage()
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
collectgarbage("stop") -- stop collection
@ -315,8 +320,6 @@ do
end
collectgarbage()
end
return('OK')

View File

@ -289,6 +289,7 @@ assert(math.sqrt("4") == 2)
assert(math.tanh("0") == 0)
assert(math.tan("0") == 0)
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("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(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(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
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
if vector_size == 4 then
assert(vector(1, 2, 3, 4).w == 4)
assert(vector(1, 2, 3, 4).W == 4)
end
return 'OK'

View File

@ -7,10 +7,20 @@
# 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
import argparse
import json
import sys
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):
def __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)
# load files
if len(sys.argv) == 2:
if arguments.snapshotnew == None:
dumpold = None
with open(sys.argv[1]) as f:
with open(arguments.snapshot) as f:
dump = json.load(f)
else:
with open(sys.argv[1]) as f:
with open(arguments.snapshot) as f:
dumpold = json.load(f)
with open(sys.argv[2]) as f:
with open(arguments.snapshotnew) as f:
dump = json.load(f)
# reachability analysis: how much of the heap is reachable from roots?
@ -111,12 +121,15 @@ while offset < len(queue):
if "object" in obj:
queue.append((obj["object"], node))
def annotateContainedCategories(node):
def annotateContainedCategories(node, start):
for obj in node.objects:
if obj["cat"] < start:
obj["cat"] = 0
node.categories.add(obj["cat"])
for child in node.children.values():
annotateContainedCategories(child)
annotateContainedCategories(child, start)
for cat in child.categories:
node.categories.add(cat)
@ -172,9 +185,11 @@ def splitIntoCategories(root):
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 False:
annotateContainedCategories(root)
if dump["stats"].get("categories") and arguments.split != 'none':
if arguments.split == 'custom':
annotateContainedCategories(root, 128)
else:
annotateContainedCategories(root, 0)
root = splitIntoCategories(root)

View File

@ -452,7 +452,7 @@ def display(root, title, colors, flip = False):
.replace("$gradient-start", gradient_start)
.replace("$gradient-end", gradient_end)
.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)))
)