Sync to upstream/release/507-pre (#286)

This doesn't contain all changes for 507 yet but we might want to do the
Luau 0.507 release a bit earlier to end the year sooner.

Changes:

- Type ascription (::) now permits casts between related types in both directions, allowing to refine or loosen the type (RFC #56)
- Fix type definition for tonumber to return number? since the input string isn't guaranteed to contain a valid number
- Fix type refinements for field access via []
- Many stability fixes for type checker
- Provide extra information in error messages for type mismatches in more cases
- Improve performance of type checking for large unions when union members are string literals
- Add coverage reporting support to Repl (--coverage command line argument) and lua_getcoverage C API
- Work around code signing issues during Makefile builds on macOS
- Improve performance of truthiness checks in some cases, particularly on Apple M1, resulting in 10-25% perf gains on qsort benchmark depending on the CPU/compiler
- Fix support for little-endian systems; IBM s390x here we go!
This commit is contained in:
Arseny Kapoulkine 2021-12-10 14:05:05 -08:00 committed by GitHub
parent 88be067c0b
commit f2e6a8f4a5
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
56 changed files with 1695 additions and 639 deletions

View file

@ -277,11 +277,20 @@ struct MissingUnionProperty
bool operator==(const MissingUnionProperty& rhs) const;
};
struct TypesAreUnrelated
{
TypeId left;
TypeId right;
bool operator==(const TypesAreUnrelated& rhs) const;
};
using TypeErrorData = Variant<TypeMismatch, UnknownSymbol, UnknownProperty, NotATable, CannotExtendTable, OnlyTablesCanHaveMethods,
DuplicateTypeDefinition, CountMismatch, FunctionDoesNotTakeSelf, FunctionRequiresSelf, OccursCheckFailed, UnknownRequire,
IncorrectGenericParameterCount, SyntaxError, CodeTooComplex, UnificationTooComplex, UnknownPropButFoundLikeProp, GenericError,
CannotCallNonFunction, ExtraInformation, DeprecatedApiUsed, ModuleHasCyclicDependency, IllegalRequire, FunctionExitsWithoutReturning,
DuplicateGenericParameter, CannotInferBinaryOperation, MissingProperties, SwappedGenericTypeParameter, OptionalValueAccess, MissingUnionProperty>;
DuplicateGenericParameter, CannotInferBinaryOperation, MissingProperties, SwappedGenericTypeParameter, OptionalValueAccess, MissingUnionProperty,
TypesAreUnrelated>;
struct TypeError
{

View file

@ -36,6 +36,10 @@ std::ostream& operator<<(std::ostream& lhs, const IllegalRequire& error);
std::ostream& operator<<(std::ostream& lhs, const ModuleHasCyclicDependency& error);
std::ostream& operator<<(std::ostream& lhs, const DuplicateGenericParameter& error);
std::ostream& operator<<(std::ostream& lhs, const CannotInferBinaryOperation& error);
std::ostream& operator<<(std::ostream& lhs, const SwappedGenericTypeParameter& error);
std::ostream& operator<<(std::ostream& lhs, const OptionalValueAccess& error);
std::ostream& operator<<(std::ostream& lhs, const MissingUnionProperty& error);
std::ostream& operator<<(std::ostream& lhs, const TypesAreUnrelated& error);
std::ostream& operator<<(std::ostream& lhs, const TableState& tv);
std::ostream& operator<<(std::ostream& lhs, const TypeVar& tv);

View file

@ -69,8 +69,8 @@ std::string toStringNamedFunction(const std::string& prefix, const FunctionTypeV
// It could be useful to see the text representation of a type during a debugging session instead of exploring the content of the class
// These functions will dump the type to stdout and can be evaluated in Watch/Immediate windows or as gdb/lldb expression
void dump(TypeId ty);
void dump(TypePackId ty);
std::string dump(TypeId ty);
std::string dump(TypePackId ty);
std::string generateName(size_t n);

View file

@ -156,13 +156,14 @@ struct TypeChecker
// Returns both the type of the lvalue and its binding (if the caller wants to mutate the binding).
// Note: the binding may be null.
// TODO: remove second return value with FFlagLuauUpdateFunctionNameBinding
std::pair<TypeId, TypeId*> checkLValueBinding(const ScopePtr& scope, const AstExpr& expr);
std::pair<TypeId, TypeId*> checkLValueBinding(const ScopePtr& scope, const AstExprLocal& expr);
std::pair<TypeId, TypeId*> checkLValueBinding(const ScopePtr& scope, const AstExprGlobal& expr);
std::pair<TypeId, TypeId*> checkLValueBinding(const ScopePtr& scope, const AstExprIndexName& expr);
std::pair<TypeId, TypeId*> checkLValueBinding(const ScopePtr& scope, const AstExprIndexExpr& expr);
TypeId checkFunctionName(const ScopePtr& scope, AstExpr& funName);
TypeId checkFunctionName(const ScopePtr& scope, AstExpr& funName, TypeLevel level);
std::pair<TypeId, ScopePtr> checkFunctionSignature(const ScopePtr& scope, int subLevel, const AstExprFunction& expr,
std::optional<Location> originalNameLoc, std::optional<TypeId> expectedType);
void checkFunctionBody(const ScopePtr& scope, TypeId type, const AstExprFunction& function);
@ -174,7 +175,7 @@ struct TypeChecker
ExprResult<TypePackId> checkExprPack(const ScopePtr& scope, const AstExprCall& expr);
std::vector<std::optional<TypeId>> getExpectedTypesForCall(const std::vector<TypeId>& overloads, size_t argumentCount, bool selfCall);
std::optional<ExprResult<TypePackId>> checkCallOverload(const ScopePtr& scope, const AstExprCall& expr, TypeId fn, TypePackId retPack,
TypePackId argPack, TypePack* args, const std::vector<Location>& argLocations, const ExprResult<TypePackId>& argListResult,
TypePackId argPack, TypePack* args, const std::vector<Location>* argLocations, const ExprResult<TypePackId>& argListResult,
std::vector<TypeId>& overloadsThatMatchArgCount, std::vector<TypeId>& overloadsThatDont, std::vector<OverloadErrorEntry>& errors);
bool handleSelfCallMismatch(const ScopePtr& scope, const AstExprCall& expr, TypePack* args, const std::vector<Location>& argLocations,
const std::vector<OverloadErrorEntry>& errors);
@ -277,7 +278,7 @@ public:
[[noreturn]] void ice(const std::string& message);
ScopePtr childFunctionScope(const ScopePtr& parent, const Location& location, int subLevel = 0);
ScopePtr childScope(const ScopePtr& parent, const Location& location, int subLevel = 0);
ScopePtr childScope(const ScopePtr& parent, const Location& location);
// Wrapper for merge(l, r, toUnion) but without the lambda junk.
void merge(RefinementMap& l, const RefinementMap& r);

View file

@ -499,6 +499,7 @@ struct SingletonTypes
const TypePackId anyTypePack;
SingletonTypes();
~SingletonTypes();
SingletonTypes(const SingletonTypes&) = delete;
void operator=(const SingletonTypes&) = delete;
@ -509,10 +510,12 @@ struct SingletonTypes
private:
std::unique_ptr<struct TypeArena> arena;
bool debugFreezeArena = false;
TypeId makeStringMetatable();
};
extern SingletonTypes singletonTypes;
SingletonTypes& getSingletonTypes();
void persist(TypeId ty);
void persist(TypePackId tp);
@ -523,9 +526,6 @@ TypeLevel* getMutableLevel(TypeId ty);
const Property* lookupClassProp(const ClassTypeVar* cls, const Name& name);
bool isSubclass(const ClassTypeVar* cls, const ClassTypeVar* parent);
bool hasGeneric(TypeId ty);
bool hasGeneric(TypePackId tp);
TypeVar* asMutable(TypeId ty);
template<typename T>

View file

@ -24,7 +24,7 @@ struct TypeLevel
int level = 0;
int subLevel = 0;
// Returns true if the typelevel "this" is "bigger" than rhs
// Returns true if the level of "this" belongs to an equal or larger scope than that of rhs
bool subsumes(const TypeLevel& rhs) const
{
if (level < rhs.level)
@ -38,6 +38,15 @@ struct TypeLevel
return false;
}
// Returns true if the level of "this" belongs to a larger (not equal) scope than that of rhs
bool subsumesStrict(const TypeLevel& rhs) const
{
if (level == rhs.level && subLevel == rhs.subLevel)
return false;
else
return subsumes(rhs);
}
TypeLevel incr() const
{
TypeLevel result;

View file

@ -91,6 +91,9 @@ private:
[[noreturn]] void ice(const std::string& message, const Location& location);
[[noreturn]] void ice(const std::string& message);
// Available after regular type pack unification errors
std::optional<int> firstPackErrorPos;
};
} // namespace Luau

View file

@ -15,6 +15,7 @@
LUAU_FASTFLAG(LuauIfElseExpressionAnalysisSupport)
LUAU_FASTFLAGVARIABLE(LuauAutocompleteAvoidMutation, false);
LUAU_FASTFLAGVARIABLE(LuauAutocompletePreferToCallFunctions, false);
LUAU_FASTFLAGVARIABLE(LuauAutocompleteFirstArg, false);
static const std::unordered_set<std::string> kStatementStartingKeywords = {
"while", "if", "local", "repeat", "function", "do", "for", "return", "break", "continue", "type", "export"};
@ -190,7 +191,48 @@ static ParenthesesRecommendation getParenRecommendation(TypeId id, const std::ve
return ParenthesesRecommendation::None;
}
static TypeCorrectKind checkTypeCorrectKind(const Module& module, TypeArena* typeArena, AstNode* node, TypeId ty)
static std::optional<TypeId> findExpectedTypeAt(const Module& module, AstNode* node, Position position)
{
LUAU_ASSERT(FFlag::LuauAutocompleteFirstArg);
auto expr = node->asExpr();
if (!expr)
return std::nullopt;
// Extra care for first function call argument location
// When we don't have anything inside () yet, we also don't have an AST node to base our lookup
if (AstExprCall* exprCall = expr->as<AstExprCall>())
{
if (exprCall->args.size == 0 && exprCall->argLocation.contains(position))
{
auto it = module.astTypes.find(exprCall->func);
if (!it)
return std::nullopt;
const FunctionTypeVar* ftv = get<FunctionTypeVar>(follow(*it));
if (!ftv)
return std::nullopt;
auto [head, tail] = flatten(ftv->argTypes);
unsigned index = exprCall->self ? 1 : 0;
if (index < head.size())
return head[index];
return std::nullopt;
}
}
auto it = module.astExpectedTypes.find(expr);
if (!it)
return std::nullopt;
return *it;
}
static TypeCorrectKind checkTypeCorrectKind(const Module& module, TypeArena* typeArena, AstNode* node, Position position, TypeId ty)
{
ty = follow(ty);
@ -220,15 +262,29 @@ static TypeCorrectKind checkTypeCorrectKind(const Module& module, TypeArena* typ
}
};
auto expr = node->asExpr();
if (!expr)
return TypeCorrectKind::None;
TypeId expectedType;
auto it = module.astExpectedTypes.find(expr);
if (!it)
return TypeCorrectKind::None;
if (FFlag::LuauAutocompleteFirstArg)
{
auto typeAtPosition = findExpectedTypeAt(module, node, position);
TypeId expectedType = follow(*it);
if (!typeAtPosition)
return TypeCorrectKind::None;
expectedType = follow(*typeAtPosition);
}
else
{
auto expr = node->asExpr();
if (!expr)
return TypeCorrectKind::None;
auto it = module.astExpectedTypes.find(expr);
if (!it)
return TypeCorrectKind::None;
expectedType = follow(*it);
}
if (FFlag::LuauAutocompletePreferToCallFunctions)
{
@ -333,8 +389,8 @@ static void autocompleteProps(const Module& module, TypeArena* typeArena, TypeId
if (result.count(name) == 0 && name != Parser::errorName)
{
Luau::TypeId type = Luau::follow(prop.type);
TypeCorrectKind typeCorrect =
indexType == PropIndexType::Key ? TypeCorrectKind::Correct : checkTypeCorrectKind(module, typeArena, nodes.back(), type);
TypeCorrectKind typeCorrect = indexType == PropIndexType::Key ? TypeCorrectKind::Correct
: checkTypeCorrectKind(module, typeArena, nodes.back(), {{}, {}}, type);
ParenthesesRecommendation parens =
indexType == PropIndexType::Key ? ParenthesesRecommendation::None : getParenRecommendation(type, nodes, typeCorrect);
@ -692,17 +748,31 @@ std::optional<const T*> returnFirstNonnullOptionOfType(const UnionTypeVar* utv)
return ret;
}
static std::optional<bool> functionIsExpectedAt(const Module& module, AstNode* node)
static std::optional<bool> functionIsExpectedAt(const Module& module, AstNode* node, Position position)
{
auto expr = node->asExpr();
if (!expr)
return std::nullopt;
TypeId expectedType;
auto it = module.astExpectedTypes.find(expr);
if (!it)
return std::nullopt;
if (FFlag::LuauAutocompleteFirstArg)
{
auto typeAtPosition = findExpectedTypeAt(module, node, position);
TypeId expectedType = follow(*it);
if (!typeAtPosition)
return std::nullopt;
expectedType = follow(*typeAtPosition);
}
else
{
auto expr = node->asExpr();
if (!expr)
return std::nullopt;
auto it = module.astExpectedTypes.find(expr);
if (!it)
return std::nullopt;
expectedType = follow(*it);
}
if (get<FunctionTypeVar>(expectedType))
return true;
@ -1171,7 +1241,7 @@ static void autocompleteExpression(const SourceModule& sourceModule, const Modul
std::string n = toString(name);
if (!result.count(n))
{
TypeCorrectKind typeCorrect = checkTypeCorrectKind(module, typeArena, node, binding.typeId);
TypeCorrectKind typeCorrect = checkTypeCorrectKind(module, typeArena, node, position, binding.typeId);
result[n] = {AutocompleteEntryKind::Binding, binding.typeId, binding.deprecated, false, typeCorrect, std::nullopt, std::nullopt,
binding.documentationSymbol, {}, getParenRecommendation(binding.typeId, ancestry, typeCorrect)};
@ -1181,9 +1251,10 @@ static void autocompleteExpression(const SourceModule& sourceModule, const Modul
scope = scope->parent;
}
TypeCorrectKind correctForNil = checkTypeCorrectKind(module, typeArena, node, typeChecker.nilType);
TypeCorrectKind correctForBoolean = checkTypeCorrectKind(module, typeArena, node, typeChecker.booleanType);
TypeCorrectKind correctForFunction = functionIsExpectedAt(module, node).value_or(false) ? TypeCorrectKind::Correct : TypeCorrectKind::None;
TypeCorrectKind correctForNil = checkTypeCorrectKind(module, typeArena, node, position, typeChecker.nilType);
TypeCorrectKind correctForBoolean = checkTypeCorrectKind(module, typeArena, node, position, typeChecker.booleanType);
TypeCorrectKind correctForFunction =
functionIsExpectedAt(module, node, position).value_or(false) ? TypeCorrectKind::Correct : TypeCorrectKind::None;
if (FFlag::LuauIfElseExpressionAnalysisSupport)
result["if"] = {AutocompleteEntryKind::Keyword, std::nullopt, false, false};

View file

@ -217,9 +217,9 @@ void registerBuiltinTypes(TypeChecker& typeChecker)
TypeId genericK = arena.addType(GenericTypeVar{"K"});
TypeId genericV = arena.addType(GenericTypeVar{"V"});
TypeId mapOfKtoV = arena.addType(TableTypeVar{{}, TableIndexer(genericK, genericV), typeChecker.globalScope->level});
TypeId mapOfKtoV = arena.addType(TableTypeVar{{}, TableIndexer(genericK, genericV), typeChecker.globalScope->level, TableState::Generic});
std::optional<TypeId> stringMetatableTy = getMetatable(singletonTypes.stringType);
std::optional<TypeId> stringMetatableTy = getMetatable(getSingletonTypes().stringType);
LUAU_ASSERT(stringMetatableTy);
const TableTypeVar* stringMetatableTable = get<TableTypeVar>(follow(*stringMetatableTy));
LUAU_ASSERT(stringMetatableTable);
@ -271,7 +271,10 @@ void registerBuiltinTypes(TypeChecker& typeChecker)
persist(pair.second.typeId);
if (TableTypeVar* ttv = getMutable<TableTypeVar>(pair.second.typeId))
ttv->name = toString(pair.first);
{
if (!ttv->name)
ttv->name = toString(pair.first);
}
}
attachMagicFunction(getGlobalBinding(typeChecker, "assert"), magicFunctionAssert);

View file

@ -1,6 +1,8 @@
// This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details
#include "Luau/BuiltinDefinitions.h"
LUAU_FASTFLAGVARIABLE(LuauFixTonumberReturnType, false)
namespace Luau
{
@ -113,7 +115,6 @@ 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
@ -204,7 +205,14 @@ declare function gcinfo(): number
std::string getBuiltinDefinitionSource()
{
return kBuiltinDefinitionLuaSrc;
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;
}
} // namespace Luau

View file

@ -58,7 +58,7 @@ struct ErrorConverter
result += "\ncaused by:\n ";
if (!tm.reason.empty())
result += tm.reason + ". ";
result += tm.reason + " ";
result += Luau::toString(*tm.error);
}
@ -410,6 +410,11 @@ struct ErrorConverter
return ss + " in the type '" + toString(e.type) + "'";
}
std::string operator()(const TypesAreUnrelated& e) const
{
return "Cannot cast '" + toString(e.left) + "' into '" + toString(e.right) + "' because the types are unrelated";
}
};
struct InvalidNameChecker
@ -658,6 +663,11 @@ bool MissingUnionProperty::operator==(const MissingUnionProperty& rhs) const
return *type == *rhs.type && key == rhs.key;
}
bool TypesAreUnrelated::operator==(const TypesAreUnrelated& rhs) const
{
return left == rhs.left && right == rhs.right;
}
std::string toString(const TypeError& error)
{
ErrorConverter converter;
@ -793,6 +803,11 @@ void copyError(T& e, TypeArena& destArena, SeenTypes& seenTypes, SeenTypePacks&
for (auto& ty : e.missing)
ty = clone(ty);
}
else if constexpr (std::is_same_v<T, TypesAreUnrelated>)
{
e.left = clone(e.left);
e.right = clone(e.right);
}
else
static_assert(always_false_v<T>, "Non-exhaustive type switch");
}

View file

@ -262,6 +262,12 @@ std::ostream& operator<<(std::ostream& stream, const MissingUnionProperty& error
return stream << " }, key = '" + error.key + "' }";
}
std::ostream& operator<<(std::ostream& stream, const TypesAreUnrelated& error)
{
stream << "TypesAreUnrelated { left = '" + toString(error.left) + "', right = '" + toString(error.right) + "' }";
return stream;
}
std::ostream& operator<<(std::ostream& stream, const TableState& tv)
{
return stream << static_cast<std::underlying_type<TableState>::type>(tv);

View file

@ -379,7 +379,7 @@ struct AstJsonEncoder : public AstVisitor
if (comma)
writeRaw(",");
else
comma = false;
comma = true;
write(prop);
}
});

View file

@ -13,7 +13,6 @@
LUAU_FASTFLAGVARIABLE(DebugLuauFreezeArena, false)
LUAU_FASTFLAGVARIABLE(DebugLuauTrackOwningArena, false)
LUAU_FASTFLAG(LuauCaptureBrokenCommentSpans)
LUAU_FASTINTVARIABLE(LuauTypeCloneRecursionLimit, 0)
namespace Luau
@ -23,7 +22,7 @@ static bool contains(Position pos, Comment comment)
{
if (comment.location.contains(pos))
return true;
else if (FFlag::LuauCaptureBrokenCommentSpans && comment.type == Lexeme::BrokenComment &&
else if (comment.type == Lexeme::BrokenComment &&
comment.location.begin <= pos) // Broken comments are broken specifically because they don't have an end
return true;
else if (comment.type == Lexeme::Comment && comment.location.end == pos)
@ -194,7 +193,7 @@ struct TypePackCloner
{
cloneState.encounteredFreeType = true;
TypePackId err = singletonTypes.errorRecoveryTypePack(singletonTypes.anyTypePack);
TypePackId err = getSingletonTypes().errorRecoveryTypePack(getSingletonTypes().anyTypePack);
TypePackId cloned = dest.addTypePack(*err);
seenTypePacks[typePackId] = cloned;
}
@ -247,7 +246,7 @@ void TypeCloner::defaultClone(const T& t)
void TypeCloner::operator()(const Unifiable::Free& t)
{
cloneState.encounteredFreeType = true;
TypeId err = singletonTypes.errorRecoveryType(singletonTypes.anyType);
TypeId err = getSingletonTypes().errorRecoveryType(getSingletonTypes().anyType);
TypeId cloned = dest.addType(*err);
seenTypes[typeId] = cloned;
}
@ -421,9 +420,6 @@ TypePackId clone(TypePackId tp, TypeArena& dest, SeenTypes& seenTypes, SeenTypeP
Luau::visit(cloner, tp->ty); // Mutates the storage that 'res' points into.
}
if (FFlag::DebugLuauTrackOwningArena)
asMutable(res)->owningArena = &dest;
return res;
}
@ -440,12 +436,11 @@ 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::DebugLuauTrackOwningArena)
asMutable(res)->owningArena = &dest;
return res;
}
@ -508,8 +503,8 @@ bool Module::clonePublicInterface()
if (moduleScope->varargPack)
moduleScope->varargPack = clone(*moduleScope->varargPack, interfaceTypes, seenTypes, seenTypePacks, cloneState);
for (auto& pair : moduleScope->exportedTypeBindings)
pair.second = clone(pair.second, interfaceTypes, seenTypes, seenTypePacks, cloneState);
for (auto& [name, tf] : moduleScope->exportedTypeBindings)
tf = clone(tf, interfaceTypes, seenTypes, seenTypePacks, cloneState);
for (TypeId ty : moduleScope->returnType)
if (get<GenericTypeVar>(follow(ty)))

View file

@ -24,7 +24,7 @@ std::optional<LValue> tryGetLValue(const AstExpr& node)
else if (auto indexexpr = expr->as<AstExprIndexExpr>())
{
if (auto lvalue = tryGetLValue(*indexexpr->expr))
if (auto string = indexexpr->expr->as<AstExprConstantString>())
if (auto string = indexexpr->index->as<AstExprConstantString>())
return Field{std::make_shared<LValue>(*lvalue), std::string(string->value.data, string->value.size)};
}

View file

@ -13,6 +13,13 @@
LUAU_FASTFLAG(LuauOccursCheckOkWithRecursiveFunctions)
LUAU_FASTFLAGVARIABLE(LuauFunctionArgumentNameSize, false)
/*
* Prefix generic typenames with gen-
* Additionally, free types will be prefixed with free- and suffixed with their level. eg free-a-4
* Fair warning: Setting this will break a lot of Luau unit tests.
*/
LUAU_FASTFLAGVARIABLE(DebugLuauVerboseTypeNames, false)
namespace Luau
{
@ -290,7 +297,15 @@ struct TypeVarStringifier
void operator()(TypeId ty, const Unifiable::Free& ftv)
{
state.result.invalid = true;
if (FFlag::DebugLuauVerboseTypeNames)
state.emit("free-");
state.emit(state.getName(ty));
if (FFlag::DebugLuauVerboseTypeNames)
{
state.emit("-");
state.emit(std::to_string(ftv.level.level));
}
}
void operator()(TypeId, const BoundTypeVar& btv)
@ -802,6 +817,8 @@ struct TypePackStringifier
void operator()(TypePackId tp, const GenericTypePack& pack)
{
if (FFlag::DebugLuauVerboseTypeNames)
state.emit("gen-");
if (pack.explicitName)
{
state.result.nameMap.typePacks[tp] = pack.name;
@ -817,7 +834,16 @@ struct TypePackStringifier
void operator()(TypePackId tp, const FreeTypePack& pack)
{
state.result.invalid = true;
if (FFlag::DebugLuauVerboseTypeNames)
state.emit("free-");
state.emit(state.getName(tp));
if (FFlag::DebugLuauVerboseTypeNames)
{
state.emit("-");
state.emit(std::to_string(pack.level.level));
}
state.emit("...");
}
@ -1181,20 +1207,24 @@ std::string toStringNamedFunction(const std::string& prefix, const FunctionTypeV
return s;
}
void dump(TypeId ty)
std::string dump(TypeId ty)
{
ToStringOptions opts;
opts.exhaustive = true;
opts.functionTypeArguments = true;
printf("%s\n", toString(ty, opts).c_str());
std::string s = toString(ty, opts);
printf("%s\n", s.c_str());
return s;
}
void dump(TypePackId ty)
std::string dump(TypePackId ty)
{
ToStringOptions opts;
opts.exhaustive = true;
opts.functionTypeArguments = true;
printf("%s\n", toString(ty, opts).c_str());
std::string s = toString(ty, opts);
printf("%s\n", s.c_str());
return s;
}
std::string generateName(size_t i)

View file

@ -9,9 +9,9 @@
#include "Luau/Scope.h"
#include "Luau/Substitution.h"
#include "Luau/TopoSortStatements.h"
#include "Luau/ToString.h"
#include "Luau/TypePack.h"
#include "Luau/TypeUtils.h"
#include "Luau/ToString.h"
#include "Luau/TypeVar.h"
#include "Luau/TimeTrace.h"
@ -29,7 +29,6 @@ LUAU_FASTFLAGVARIABLE(LuauCloneCorrectlyBeforeMutatingTableType, false)
LUAU_FASTFLAGVARIABLE(LuauStoreMatchingOverloadFnType, false)
LUAU_FASTFLAGVARIABLE(LuauRecursiveTypeParameterRestriction, false)
LUAU_FASTFLAGVARIABLE(LuauIfElseExpressionAnalysisSupport, false)
LUAU_FASTFLAGVARIABLE(LuauStrictRequire, false)
LUAU_FASTFLAGVARIABLE(LuauQuantifyInPlace2, false)
LUAU_FASTFLAGVARIABLE(LuauSingletonTypes, false)
LUAU_FASTFLAGVARIABLE(LuauExpectedTypesOfProperties, false)
@ -37,6 +36,12 @@ LUAU_FASTFLAGVARIABLE(LuauErrorRecoveryType, false)
LUAU_FASTFLAGVARIABLE(LuauPropertiesGetExpectedType, false)
LUAU_FASTFLAGVARIABLE(LuauTailArgumentTypeInfo, false)
LUAU_FASTFLAGVARIABLE(LuauModuleRequireErrorPack, false)
LUAU_FASTFLAGVARIABLE(LuauRefiLookupFromIndexExpr, false)
LUAU_FASTFLAGVARIABLE(LuauProperTypeLevels, false)
LUAU_FASTFLAGVARIABLE(LuauAscribeCorrectLevelToInferredProperitesOfFreeTables, false)
LUAU_FASTFLAGVARIABLE(LuauFixRecursiveMetatableCall, false)
LUAU_FASTFLAGVARIABLE(LuauBidirectionalAsExpr, false)
LUAU_FASTFLAGVARIABLE(LuauUpdateFunctionNameBinding, false)
namespace Luau
{
@ -206,14 +211,14 @@ TypeChecker::TypeChecker(ModuleResolver* resolver, InternalErrorReporter* iceHan
: resolver(resolver)
, iceHandler(iceHandler)
, unifierState(iceHandler)
, nilType(singletonTypes.nilType)
, numberType(singletonTypes.numberType)
, stringType(singletonTypes.stringType)
, booleanType(singletonTypes.booleanType)
, threadType(singletonTypes.threadType)
, anyType(singletonTypes.anyType)
, optionalNumberType(singletonTypes.optionalNumberType)
, anyTypePack(singletonTypes.anyTypePack)
, nilType(getSingletonTypes().nilType)
, numberType(getSingletonTypes().numberType)
, stringType(getSingletonTypes().stringType)
, booleanType(getSingletonTypes().booleanType)
, threadType(getSingletonTypes().threadType)
, anyType(getSingletonTypes().anyType)
, optionalNumberType(getSingletonTypes().optionalNumberType)
, anyTypePack(getSingletonTypes().anyTypePack)
{
globalScope = std::make_shared<Scope>(globalTypes.addTypePack(TypePackVar{FreeTypePack{TypeLevel{}}}));
@ -443,7 +448,7 @@ void TypeChecker::checkBlock(const ScopePtr& scope, const AstStatBlock& block)
functionDecls[*protoIter] = pair;
++subLevel;
TypeId leftType = checkFunctionName(scope, *fun->name);
TypeId leftType = checkFunctionName(scope, *fun->name, funScope->level);
unify(leftType, funTy, fun->location);
}
else if (auto fun = (*protoIter)->as<AstStatLocalFunction>())
@ -711,14 +716,15 @@ void TypeChecker::check(const ScopePtr& scope, const AstStatAssign& assign)
}
else if (auto tail = valueIter.tail())
{
if (get<Unifiable::Error>(*tail))
TypePackId tailPack = follow(*tail);
if (get<Unifiable::Error>(tailPack))
right = errorRecoveryType(scope);
else if (auto vtp = get<VariadicTypePack>(*tail))
else if (auto vtp = get<VariadicTypePack>(tailPack))
right = vtp->ty;
else if (get<Unifiable::Free>(*tail))
else if (get<Unifiable::Free>(tailPack))
{
*asMutable(*tail) = TypePack{{left}};
growingPack = getMutable<TypePack>(*tail);
*asMutable(tailPack) = TypePack{{left}};
growingPack = getMutable<TypePack>(tailPack);
}
}
@ -1107,8 +1113,27 @@ void TypeChecker::check(const ScopePtr& scope, TypeId ty, const ScopePtr& funSco
unify(leftType, ty, function.location);
if (leftTypeBinding)
*leftTypeBinding = follow(quantify(funScope, leftType, function.name->location));
if (FFlag::LuauUpdateFunctionNameBinding)
{
LUAU_ASSERT(function.name->is<AstExprIndexName>() || function.name->is<AstExprError>());
if (auto exprIndexName = function.name->as<AstExprIndexName>())
{
if (auto typeIt = currentModule->astTypes.find(exprIndexName->expr))
{
if (auto ttv = getMutableTableType(*typeIt))
{
if (auto it = ttv->props.find(exprIndexName->index.value); it != ttv->props.end())
it->second.type = follow(quantify(funScope, leftType, function.name->location));
}
}
}
}
else
{
if (leftTypeBinding)
*leftTypeBinding = follow(quantify(funScope, leftType, function.name->location));
}
}
}
@ -1148,8 +1173,10 @@ void TypeChecker::check(const ScopePtr& scope, const AstStatTypeAlias& typealias
}
else
{
ScopePtr aliasScope =
FFlag::LuauQuantifyInPlace2 ? childScope(scope, typealias.location, subLevel) : childScope(scope, typealias.location);
ScopePtr aliasScope = childScope(scope, typealias.location);
aliasScope->level = scope->level.incr();
if (FFlag::LuauProperTypeLevels)
aliasScope->level.subLevel = subLevel;
auto [generics, genericPacks] = createGenericTypes(aliasScope, scope->level, typealias, typealias.generics, typealias.genericPacks);
@ -1166,6 +1193,7 @@ void TypeChecker::check(const ScopePtr& scope, const AstStatTypeAlias& typealias
ice("Not predeclared");
ScopePtr aliasScope = childScope(scope, typealias.location);
aliasScope->level = scope->level.incr();
for (TypeId ty : binding->typeParams)
{
@ -1505,9 +1533,9 @@ 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!");
ice("Unexpected abstract type pack!", expr.location);
else
ice("Unknown TypePack type!");
ice("Unknown TypePack type!", expr.location);
}
ExprResult<TypeId> TypeChecker::checkExpr(const ScopePtr& scope, const AstExprIndexName& expr)
@ -1574,7 +1602,7 @@ std::optional<TypeId> TypeChecker::getIndexTypeFromType(
}
else if (tableType->state == TableState::Free)
{
TypeId result = freshType(scope);
TypeId result = FFlag::LuauAscribeCorrectLevelToInferredProperitesOfFreeTables ? freshType(tableType->level) : freshType(scope);
tableType->props[name] = {result};
return result;
}
@ -1738,7 +1766,16 @@ TypeId TypeChecker::stripFromNilAndReport(TypeId ty, const Location& location)
ExprResult<TypeId> TypeChecker::checkExpr(const ScopePtr& scope, const AstExprIndexExpr& expr)
{
return {checkLValue(scope, expr)};
TypeId ty = checkLValue(scope, expr);
if (FFlag::LuauRefiLookupFromIndexExpr)
{
if (std::optional<LValue> lvalue = tryGetLValue(expr))
if (std::optional<TypeId> refiTy = resolveLValue(scope, *lvalue))
return {*refiTy, {TruthyPredicate{std::move(*lvalue), expr.location}}};
}
return {ty};
}
ExprResult<TypeId> TypeChecker::checkExpr(const ScopePtr& scope, const AstExprFunction& expr, std::optional<TypeId> expectedType)
@ -2421,12 +2458,27 @@ ExprResult<TypeId> TypeChecker::checkExpr(const ScopePtr& scope, const AstExprTy
TypeId annotationType = resolveType(scope, *expr.annotation);
ExprResult<TypeId> result = checkExpr(scope, *expr.expr, annotationType);
ErrorVec errorVec = canUnify(result.type, annotationType, expr.location);
reportErrors(errorVec);
if (!errorVec.empty())
annotationType = errorRecoveryType(annotationType);
if (FFlag::LuauBidirectionalAsExpr)
{
// Note: As an optimization, we try 'number <: number | string' first, as that is the more likely case.
if (canUnify(result.type, annotationType, expr.location).empty())
return {annotationType, std::move(result.predicates)};
return {annotationType, std::move(result.predicates)};
if (canUnify(annotationType, result.type, expr.location).empty())
return {annotationType, std::move(result.predicates)};
reportError(expr.location, TypesAreUnrelated{result.type, annotationType});
return {errorRecoveryType(annotationType), std::move(result.predicates)};
}
else
{
ErrorVec errorVec = canUnify(result.type, annotationType, expr.location);
reportErrors(errorVec);
if (!errorVec.empty())
annotationType = errorRecoveryType(annotationType);
return {annotationType, std::move(result.predicates)};
}
}
ExprResult<TypeId> TypeChecker::checkExpr(const ScopePtr& scope, const AstExprError& expr)
@ -2674,8 +2726,15 @@ std::pair<TypeId, TypeId*> TypeChecker::checkLValueBinding(const ScopePtr& scope
// Answers the question: "Can I define another function with this name?"
// Primarily about detecting duplicates.
TypeId TypeChecker::checkFunctionName(const ScopePtr& scope, AstExpr& funName)
TypeId TypeChecker::checkFunctionName(const ScopePtr& scope, AstExpr& funName, TypeLevel level)
{
auto freshTy = [&]() {
if (FFlag::LuauProperTypeLevels)
return freshType(level);
else
return freshType(scope);
};
if (auto globalName = funName.as<AstExprGlobal>())
{
const ScopePtr& globalScope = currentModule->getModuleScope();
@ -2689,7 +2748,7 @@ TypeId TypeChecker::checkFunctionName(const ScopePtr& scope, AstExpr& funName)
}
else
{
TypeId ty = freshType(scope);
TypeId ty = freshTy();
globalScope->bindings[name] = {ty, funName.location};
return ty;
}
@ -2699,7 +2758,7 @@ TypeId TypeChecker::checkFunctionName(const ScopePtr& scope, AstExpr& funName)
Symbol name = localName->local;
Binding& binding = scope->bindings[name];
if (binding.typeId == nullptr)
binding = {freshType(scope), funName.location};
binding = {freshTy(), funName.location};
return binding.typeId;
}
@ -2730,7 +2789,7 @@ TypeId TypeChecker::checkFunctionName(const ScopePtr& scope, AstExpr& funName)
Property& property = ttv->props[name];
property.type = freshType(scope);
property.type = freshTy();
property.location = indexName->indexLocation;
ttv->methodDefinitionLocations[name] = funName.location;
return property.type;
@ -3327,7 +3386,7 @@ ExprResult<TypePackId> TypeChecker::checkExprPack(const ScopePtr& scope, const A
fn = follow(fn);
if (auto ret = checkCallOverload(
scope, expr, fn, retPack, argPack, args, argLocations, argListResult, overloadsThatMatchArgCount, overloadsThatDont, errors))
scope, expr, fn, retPack, argPack, args, &argLocations, argListResult, overloadsThatMatchArgCount, overloadsThatDont, errors))
return *ret;
}
@ -3402,9 +3461,11 @@ std::vector<std::optional<TypeId>> TypeChecker::getExpectedTypesForCall(const st
}
std::optional<ExprResult<TypePackId>> TypeChecker::checkCallOverload(const ScopePtr& scope, const AstExprCall& expr, TypeId fn, TypePackId retPack,
TypePackId argPack, TypePack* args, const std::vector<Location>& argLocations, const ExprResult<TypePackId>& argListResult,
TypePackId argPack, TypePack* args, const std::vector<Location>* argLocations, const ExprResult<TypePackId>& argListResult,
std::vector<TypeId>& overloadsThatMatchArgCount, std::vector<TypeId>& overloadsThatDont, std::vector<OverloadErrorEntry>& errors)
{
LUAU_ASSERT(argLocations);
fn = stripFromNilAndReport(fn, expr.func->location);
if (get<AnyTypeVar>(fn))
@ -3428,31 +3489,44 @@ std::optional<ExprResult<TypePackId>> TypeChecker::checkCallOverload(const Scope
return {{retPack}};
}
const FunctionTypeVar* ftv = get<FunctionTypeVar>(fn);
if (!ftv)
std::vector<Location> metaArgLocations;
// Might be a callable table
if (const MetatableTypeVar* mttv = get<MetatableTypeVar>(fn))
{
// Might be a callable table
if (const MetatableTypeVar* mttv = get<MetatableTypeVar>(fn))
if (std::optional<TypeId> ty = getIndexTypeFromType(scope, mttv->metatable, "__call", expr.func->location, false))
{
if (std::optional<TypeId> ty = getIndexTypeFromType(scope, mttv->metatable, "__call", expr.func->location, false))
// Construct arguments with 'self' added in front
TypePackId metaCallArgPack = addTypePack(TypePackVar(TypePack{args->head, args->tail}));
TypePack* metaCallArgs = getMutable<TypePack>(metaCallArgPack);
metaCallArgs->head.insert(metaCallArgs->head.begin(), fn);
metaArgLocations = *argLocations;
metaArgLocations.insert(metaArgLocations.begin(), expr.func->location);
if (FFlag::LuauFixRecursiveMetatableCall)
{
// Construct arguments with 'self' added in front
TypePackId metaCallArgPack = addTypePack(TypePackVar(TypePack{args->head, args->tail}));
TypePack* metaCallArgs = getMutable<TypePack>(metaCallArgPack);
metaCallArgs->head.insert(metaCallArgs->head.begin(), fn);
std::vector<Location> metaArgLocations = argLocations;
metaArgLocations.insert(metaArgLocations.begin(), expr.func->location);
fn = instantiate(scope, *ty, expr.func->location);
argPack = metaCallArgPack;
args = metaCallArgs;
argLocations = &metaArgLocations;
}
else
{
TypeId fn = *ty;
fn = instantiate(scope, fn, expr.func->location);
return checkCallOverload(scope, expr, fn, retPack, metaCallArgPack, metaCallArgs, metaArgLocations, argListResult,
return checkCallOverload(scope, expr, fn, retPack, metaCallArgPack, metaCallArgs, &metaArgLocations, argListResult,
overloadsThatMatchArgCount, overloadsThatDont, errors);
}
}
}
const FunctionTypeVar* ftv = get<FunctionTypeVar>(fn);
if (!ftv)
{
reportError(TypeError{expr.func->location, CannotCallNonFunction{fn}});
unify(retPack, errorRecoveryTypePack(scope), expr.func->location);
return {{errorRecoveryTypePack(retPack)}};
@ -3477,7 +3551,7 @@ std::optional<ExprResult<TypePackId>> TypeChecker::checkCallOverload(const Scope
return {};
}
checkArgumentList(scope, state, argPack, ftv->argTypes, argLocations);
checkArgumentList(scope, state, argPack, ftv->argTypes, *argLocations);
if (!state.errors.empty())
{
@ -3772,7 +3846,7 @@ TypeId TypeChecker::checkRequire(const ScopePtr& scope, const ModuleInfo& module
if (moduleInfo.name.empty())
{
if (FFlag::LuauStrictRequire && currentModule->mode == Mode::Strict)
if (currentModule->mode == Mode::Strict)
{
reportError(TypeError{location, UnknownRequire{}});
return errorRecoveryType(anyType);
@ -4268,9 +4342,11 @@ ScopePtr TypeChecker::childFunctionScope(const ScopePtr& parent, const Location&
}
// Creates a new Scope and carries forward the varargs from the parent.
ScopePtr TypeChecker::childScope(const ScopePtr& parent, const Location& location, int subLevel)
ScopePtr TypeChecker::childScope(const ScopePtr& parent, const Location& location)
{
ScopePtr scope = std::make_shared<Scope>(parent, subLevel);
ScopePtr scope = std::make_shared<Scope>(parent);
if (FFlag::LuauProperTypeLevels)
scope->level = parent->level;
scope->varargPack = parent->varargPack;
currentModule->scopes.push_back(std::make_pair(location, scope));
@ -4329,22 +4405,22 @@ TypeId TypeChecker::singletonType(std::string value)
TypeId TypeChecker::errorRecoveryType(const ScopePtr& scope)
{
return singletonTypes.errorRecoveryType();
return getSingletonTypes().errorRecoveryType();
}
TypeId TypeChecker::errorRecoveryType(TypeId guess)
{
return singletonTypes.errorRecoveryType(guess);
return getSingletonTypes().errorRecoveryType(guess);
}
TypePackId TypeChecker::errorRecoveryTypePack(const ScopePtr& scope)
{
return singletonTypes.errorRecoveryTypePack();
return getSingletonTypes().errorRecoveryTypePack();
}
TypePackId TypeChecker::errorRecoveryTypePack(TypePackId guess)
{
return singletonTypes.errorRecoveryTypePack(guess);
return getSingletonTypes().errorRecoveryTypePack(guess);
}
std::optional<TypeId> TypeChecker::filterMap(TypeId type, TypeIdPredicate predicate)
@ -4547,6 +4623,7 @@ TypeId TypeChecker::resolveType(const ScopePtr& scope, const AstType& annotation
else if (const auto& func = annotation.as<AstTypeFunction>())
{
ScopePtr funcScope = childScope(scope, func->location);
funcScope->level = scope->level.incr();
auto [generics, genericPacks] = createGenericTypes(funcScope, std::nullopt, annotation, func->generics, func->genericPacks);

View file

@ -19,7 +19,7 @@ std::optional<TypeId> findMetatableEntry(ErrorVec& errors, const ScopePtr& globa
TypeId unwrapped = follow(*metatable);
if (get<AnyTypeVar>(unwrapped))
return singletonTypes.anyType;
return getSingletonTypes().anyType;
const TableTypeVar* mtt = getTableType(unwrapped);
if (!mtt)
@ -61,12 +61,12 @@ std::optional<TypeId> findTablePropertyRespectingMeta(ErrorVec& errors, const Sc
{
std::optional<TypeId> r = first(follow(itf->retType));
if (!r)
return singletonTypes.nilType;
return getSingletonTypes().nilType;
else
return *r;
}
else if (get<AnyTypeVar>(index))
return singletonTypes.anyType;
return getSingletonTypes().anyType;
else
errors.push_back(TypeError{location, GenericError{"__index should either be a function or table. Got " + toString(index)}});

View file

@ -21,6 +21,7 @@ LUAU_FASTINTVARIABLE(LuauTypeMaximumStringifierLength, 500)
LUAU_FASTINTVARIABLE(LuauTableTypeMaximumStringifierLength, 0)
LUAU_FASTFLAGVARIABLE(LuauRefactorTagging, false)
LUAU_FASTFLAG(LuauErrorRecoveryType)
LUAU_FASTFLAG(DebugLuauFreezeArena)
namespace Luau
{
@ -579,11 +580,25 @@ SingletonTypes::SingletonTypes()
, arena(new TypeArena)
{
TypeId stringMetatable = makeStringMetatable();
stringType_.ty = PrimitiveTypeVar{PrimitiveTypeVar::String, makeStringMetatable()};
stringType_.ty = PrimitiveTypeVar{PrimitiveTypeVar::String, stringMetatable};
persist(stringMetatable);
debugFreezeArena = FFlag::DebugLuauFreezeArena;
freeze(*arena);
}
SingletonTypes::~SingletonTypes()
{
// Destroy the arena with the same memory management flags it was created with
bool prevFlag = FFlag::DebugLuauFreezeArena;
FFlag::DebugLuauFreezeArena.value = debugFreezeArena;
unfreeze(*arena);
arena.reset(nullptr);
FFlag::DebugLuauFreezeArena.value = prevFlag;
}
TypeId SingletonTypes::makeStringMetatable()
{
const TypeId optionalNumber = arena->addType(UnionTypeVar{{nilType, numberType}});
@ -641,6 +656,9 @@ TypeId SingletonTypes::makeStringMetatable()
TypeId tableType = arena->addType(TableTypeVar{std::move(stringLib), std::nullopt, TypeLevel{}, TableState::Sealed});
if (TableTypeVar* ttv = getMutable<TableTypeVar>(tableType))
ttv->name = "string";
return arena->addType(TableTypeVar{{{{"__index", {tableType}}}}, std::nullopt, TypeLevel{}, TableState::Sealed});
}
@ -670,7 +688,11 @@ TypePackId SingletonTypes::errorRecoveryTypePack(TypePackId guess)
return &errorTypePack_;
}
SingletonTypes singletonTypes;
SingletonTypes& getSingletonTypes()
{
static SingletonTypes singletonTypes;
return singletonTypes;
}
void persist(TypeId ty)
{
@ -719,6 +741,18 @@ void persist(TypeId ty)
for (TypeId opt : itv->parts)
queue.push_back(opt);
}
else if (auto mtv = get<MetatableTypeVar>(t))
{
queue.push_back(mtv->table);
queue.push_back(mtv->metatable);
}
else if (get<GenericTypeVar>(t) || get<AnyTypeVar>(t) || get<FreeTypeVar>(t) || get<SingletonTypeVar>(t) || get<PrimitiveTypeVar>(t))
{
}
else
{
LUAU_ASSERT(!"TypeId is not supported in a persist call");
}
}
}
@ -736,6 +770,17 @@ void persist(TypePackId tp)
if (p->tail)
persist(*p->tail);
}
else if (auto vtp = get<VariadicTypePack>(tp))
{
persist(vtp->ty);
}
else if (get<GenericTypePack>(tp))
{
}
else
{
LUAU_ASSERT(!"TypePackId is not supported in a persist call");
}
}
const TypeLevel* getLevel(TypeId ty)
@ -757,167 +802,6 @@ TypeLevel* getMutableLevel(TypeId ty)
return const_cast<TypeLevel*>(getLevel(ty));
}
struct QVarFinder
{
mutable DenseHashSet<const void*> seen;
QVarFinder()
: seen(nullptr)
{
}
bool hasSeen(const void* tv) const
{
if (seen.contains(tv))
return true;
seen.insert(tv);
return false;
}
bool hasGeneric(TypeId tid) const
{
if (hasSeen(&tid->ty))
return false;
return Luau::visit(*this, tid->ty);
}
bool hasGeneric(TypePackId tp) const
{
if (hasSeen(&tp->ty))
return false;
return Luau::visit(*this, tp->ty);
}
bool operator()(const Unifiable::Free&) const
{
return false;
}
bool operator()(const Unifiable::Bound<TypeId>& bound) const
{
return hasGeneric(bound.boundTo);
}
bool operator()(const Unifiable::Generic&) const
{
return true;
}
bool operator()(const Unifiable::Error&) const
{
return false;
}
bool operator()(const PrimitiveTypeVar&) const
{
return false;
}
bool operator()(const SingletonTypeVar&) const
{
return false;
}
bool operator()(const FunctionTypeVar& ftv) const
{
if (hasGeneric(ftv.argTypes))
return true;
return hasGeneric(ftv.retType);
}
bool operator()(const TableTypeVar& ttv) const
{
if (ttv.state == TableState::Generic)
return true;
if (ttv.indexer)
{
if (hasGeneric(ttv.indexer->indexType))
return true;
if (hasGeneric(ttv.indexer->indexResultType))
return true;
}
for (const auto& [_name, prop] : ttv.props)
{
if (hasGeneric(prop.type))
return true;
}
return false;
}
bool operator()(const MetatableTypeVar& mtv) const
{
return hasGeneric(mtv.table) || hasGeneric(mtv.metatable);
}
bool operator()(const ClassTypeVar& ctv) const
{
for (const auto& [name, prop] : ctv.props)
{
if (hasGeneric(prop.type))
return true;
}
if (ctv.parent)
return hasGeneric(*ctv.parent);
return false;
}
bool operator()(const AnyTypeVar&) const
{
return false;
}
bool operator()(const UnionTypeVar& utv) const
{
for (TypeId tid : utv.options)
if (hasGeneric(tid))
return true;
return false;
}
bool operator()(const IntersectionTypeVar& utv) const
{
for (TypeId tid : utv.parts)
if (hasGeneric(tid))
return true;
return false;
}
bool operator()(const LazyTypeVar&) const
{
return false;
}
bool operator()(const Unifiable::Bound<TypePackId>& bound) const
{
return hasGeneric(bound.boundTo);
}
bool operator()(const TypePack& pack) const
{
for (TypeId ty : pack.head)
if (hasGeneric(ty))
return true;
if (pack.tail)
return hasGeneric(*pack.tail);
return false;
}
bool operator()(const VariadicTypePack& pack) const
{
return hasGeneric(pack.ty);
}
};
const Property* lookupClassProp(const ClassTypeVar* cls, const Name& name)
{
while (cls)
@ -953,16 +837,6 @@ bool isSubclass(const ClassTypeVar* cls, const ClassTypeVar* parent)
return false;
}
bool hasGeneric(TypeId ty)
{
return Luau::visit(QVarFinder{}, ty->ty);
}
bool hasGeneric(TypePackId tp)
{
return Luau::visit(QVarFinder{}, tp->ty);
}
UnionTypeVarIterator::UnionTypeVarIterator(const UnionTypeVar* utv)
{
LUAU_ASSERT(utv);

View file

@ -14,7 +14,7 @@
LUAU_FASTINT(LuauTypeInferRecursionLimit);
LUAU_FASTINT(LuauTypeInferTypePackLoopLimit);
LUAU_FASTINTVARIABLE(LuauTypeInferIterationLimit, 2000);
LUAU_FASTFLAGVARIABLE(LuauTableSubtypingVariance, false);
LUAU_FASTFLAGVARIABLE(LuauTableSubtypingVariance2, false);
LUAU_FASTFLAGVARIABLE(LuauUnionHeuristic, false)
LUAU_FASTFLAGVARIABLE(LuauTableUnificationEarlyTest, false)
LUAU_FASTFLAGVARIABLE(LuauOccursCheckOkWithRecursiveFunctions, false)
@ -22,9 +22,82 @@ LUAU_FASTFLAGVARIABLE(LuauExtendedTypeMismatchError, false)
LUAU_FASTFLAG(LuauSingletonTypes)
LUAU_FASTFLAGVARIABLE(LuauExtendedClassMismatchError, false)
LUAU_FASTFLAG(LuauErrorRecoveryType);
LUAU_FASTFLAG(LuauProperTypeLevels);
LUAU_FASTFLAGVARIABLE(LuauExtendedUnionMismatchError, false)
LUAU_FASTFLAGVARIABLE(LuauExtendedFunctionMismatchError, false)
namespace Luau
{
struct PromoteTypeLevels
{
TxnLog& log;
TypeLevel minLevel;
explicit PromoteTypeLevels(TxnLog& log, TypeLevel minLevel)
: log(log)
, minLevel(minLevel)
{}
template <typename TID, typename T>
void promote(TID ty, T* t)
{
LUAU_ASSERT(t);
if (minLevel.subsumesStrict(t->level))
{
log(ty);
t->level = minLevel;
}
}
template<typename TID>
void cycle(TID) {}
template<typename TID, typename T>
bool operator()(TID, const T&)
{
return true;
}
bool operator()(TypeId ty, const FreeTypeVar&)
{
promote(ty, getMutable<FreeTypeVar>(ty));
return true;
}
bool operator()(TypeId ty, const FunctionTypeVar&)
{
promote(ty, getMutable<FunctionTypeVar>(ty));
return true;
}
bool operator()(TypeId ty, const TableTypeVar&)
{
promote(ty, getMutable<TableTypeVar>(ty));
return true;
}
bool operator()(TypePackId tp, const FreeTypePack&)
{
promote(tp, getMutable<FreeTypePack>(tp));
return true;
}
};
void promoteTypeLevels(TxnLog& log, TypeLevel minLevel, TypeId ty)
{
PromoteTypeLevels ptl{log, minLevel};
DenseHashSet<void*> seen{nullptr};
visitTypeVarOnce(ty, ptl, seen);
}
void promoteTypeLevels(TxnLog& log, TypeLevel minLevel, TypePackId tp)
{
PromoteTypeLevels ptl{log, minLevel};
DenseHashSet<void*> seen{nullptr};
visitTypeVarOnce(tp, ptl, seen);
}
struct SkipCacheForType
{
SkipCacheForType(const DenseHashMap<TypeId, bool>& skipCacheForType)
@ -127,6 +200,29 @@ static std::optional<TypeError> hasUnificationTooComplex(const ErrorVec& errors)
return *it;
}
// Used for tagged union matching heuristic, returns first singleton type field
static std::optional<std::pair<Luau::Name, const SingletonTypeVar*>> getTableMatchTag(TypeId type)
{
LUAU_ASSERT(FFlag::LuauExtendedUnionMismatchError);
type = follow(type);
if (auto ttv = get<TableTypeVar>(type))
{
for (auto&& [name, prop] : ttv->props)
{
if (auto sing = get<SingletonTypeVar>(follow(prop.type)))
return {{name, sing}};
}
}
else if (auto mttv = get<MetatableTypeVar>(type))
{
return getTableMatchTag(mttv->table);
}
return std::nullopt;
}
Unifier::Unifier(TypeArena* types, Mode mode, ScopePtr globalScope, const Location& location, Variance variance, UnifierSharedState& sharedState)
: types(types)
, mode(mode)
@ -214,9 +310,11 @@ void Unifier::tryUnify_(TypeId superTy, TypeId subTy, bool isFunctionCall, bool
{
occursCheck(superTy, subTy);
TypeLevel superLevel = l->level;
// Unification can't change the level of a generic.
auto rightGeneric = get<GenericTypeVar>(subTy);
if (rightGeneric && !rightGeneric->level.subsumes(l->level))
if (rightGeneric && !rightGeneric->level.subsumes(superLevel))
{
// TODO: a more informative error message? CLI-39912
errors.push_back(TypeError{location, GenericError{"Generic subtype escaping scope"}});
@ -226,7 +324,9 @@ void Unifier::tryUnify_(TypeId superTy, TypeId subTy, bool isFunctionCall, bool
// The occurrence check might have caused superTy no longer to be a free type
if (!get<ErrorTypeVar>(superTy))
{
if (auto rightLevel = getMutableLevel(subTy))
if (FFlag::LuauProperTypeLevels)
promoteTypeLevels(log, superLevel, subTy);
else if (auto rightLevel = getMutableLevel(subTy))
{
if (!rightLevel->subsumes(l->level))
*rightLevel = l->level;
@ -240,6 +340,8 @@ void Unifier::tryUnify_(TypeId superTy, TypeId subTy, bool isFunctionCall, bool
}
else if (r)
{
TypeLevel subLevel = r->level;
occursCheck(subTy, superTy);
// Unification can't change the level of a generic.
@ -253,10 +355,16 @@ void Unifier::tryUnify_(TypeId superTy, TypeId subTy, bool isFunctionCall, bool
if (!get<ErrorTypeVar>(subTy))
{
if (auto leftLevel = getMutableLevel(superTy))
if (FFlag::LuauProperTypeLevels)
promoteTypeLevels(log, subLevel, superTy);
if (auto superLevel = getMutableLevel(superTy))
{
if (!leftLevel->subsumes(r->level))
*leftLevel = r->level;
if (!superLevel->subsumes(r->level))
{
log(superTy);
*superLevel = r->level;
}
}
log(subTy);
@ -327,7 +435,7 @@ void Unifier::tryUnify_(TypeId superTy, TypeId subTy, bool isFunctionCall, bool
else if (failed)
{
if (FFlag::LuauExtendedTypeMismatchError && firstFailedOption)
errors.push_back(TypeError{location, TypeMismatch{superTy, subTy, "Not all union options are compatible", *firstFailedOption}});
errors.push_back(TypeError{location, TypeMismatch{superTy, subTy, "Not all union options are compatible.", *firstFailedOption}});
else
errors.push_back(TypeError{location, TypeMismatch{superTy, subTy}});
}
@ -338,28 +446,46 @@ void Unifier::tryUnify_(TypeId superTy, TypeId subTy, bool isFunctionCall, bool
bool found = false;
std::optional<TypeError> unificationTooComplex;
size_t failedOptionCount = 0;
std::optional<TypeError> failedOption;
bool foundHeuristic = false;
size_t startIndex = 0;
if (FFlag::LuauUnionHeuristic)
{
bool found = false;
const std::string* subName = getName(subTy);
if (subName)
if (const std::string* subName = getName(subTy))
{
for (size_t i = 0; i < uv->options.size(); ++i)
{
const std::string* optionName = getName(uv->options[i]);
if (optionName && *optionName == *subName)
{
found = true;
foundHeuristic = true;
startIndex = i;
break;
}
}
}
if (!found && cacheEnabled)
if (FFlag::LuauExtendedUnionMismatchError)
{
if (auto subMatchTag = getTableMatchTag(subTy))
{
for (size_t i = 0; i < uv->options.size(); ++i)
{
auto optionMatchTag = getTableMatchTag(uv->options[i]);
if (optionMatchTag && optionMatchTag->first == subMatchTag->first && *optionMatchTag->second == *subMatchTag->second)
{
foundHeuristic = true;
startIndex = i;
break;
}
}
}
}
if (!foundHeuristic && cacheEnabled)
{
for (size_t i = 0; i < uv->options.size(); ++i)
{
@ -390,15 +516,27 @@ void Unifier::tryUnify_(TypeId superTy, TypeId subTy, bool isFunctionCall, bool
{
unificationTooComplex = e;
}
else if (FFlag::LuauExtendedUnionMismatchError && !isNil(type))
{
failedOptionCount++;
if (!failedOption)
failedOption = {innerState.errors.front()};
}
innerState.log.rollback();
}
if (unificationTooComplex)
{
errors.push_back(*unificationTooComplex);
}
else if (!found)
{
if (FFlag::LuauExtendedTypeMismatchError)
if (FFlag::LuauExtendedUnionMismatchError && (failedOptionCount == 1 || foundHeuristic) && failedOption)
errors.push_back(
TypeError{location, TypeMismatch{superTy, subTy, "None of the union options are compatible. For example:", *failedOption}});
else if (FFlag::LuauExtendedTypeMismatchError)
errors.push_back(TypeError{location, TypeMismatch{superTy, subTy, "none of the union options are compatible"}});
else
errors.push_back(TypeError{location, TypeMismatch{superTy, subTy}});
@ -431,7 +569,7 @@ void Unifier::tryUnify_(TypeId superTy, TypeId subTy, bool isFunctionCall, bool
if (unificationTooComplex)
errors.push_back(*unificationTooComplex);
else if (firstFailedOption)
errors.push_back(TypeError{location, TypeMismatch{superTy, subTy, "Not all intersection parts are compatible", *firstFailedOption}});
errors.push_back(TypeError{location, TypeMismatch{superTy, subTy, "Not all intersection parts are compatible.", *firstFailedOption}});
}
else
{
@ -771,6 +909,10 @@ void Unifier::tryUnify_(TypePackId superTp, TypePackId subTp, bool isFunctionCal
if (superIter.good() && subIter.good())
{
tryUnify_(*superIter, *subIter);
if (FFlag::LuauExtendedFunctionMismatchError && !errors.empty() && !firstPackErrorPos)
firstPackErrorPos = loopCount;
superIter.advance();
subIter.advance();
continue;
@ -853,13 +995,13 @@ void Unifier::tryUnify_(TypePackId superTp, TypePackId subTp, bool isFunctionCal
while (superIter.good())
{
tryUnify_(singletonTypes.errorRecoveryType(), *superIter);
tryUnify_(getSingletonTypes().errorRecoveryType(), *superIter);
superIter.advance();
}
while (subIter.good())
{
tryUnify_(singletonTypes.errorRecoveryType(), *subIter);
tryUnify_(getSingletonTypes().errorRecoveryType(), *subIter);
subIter.advance();
}
@ -917,14 +1059,22 @@ void Unifier::tryUnifyFunctions(TypeId superTy, TypeId subTy, bool isFunctionCal
if (numGenerics != rf->generics.size())
{
numGenerics = std::min(lf->generics.size(), rf->generics.size());
errors.push_back(TypeError{location, TypeMismatch{superTy, subTy}});
if (FFlag::LuauExtendedFunctionMismatchError)
errors.push_back(TypeError{location, TypeMismatch{superTy, subTy, "different number of generic type parameters"}});
else
errors.push_back(TypeError{location, TypeMismatch{superTy, subTy}});
}
size_t numGenericPacks = lf->genericPacks.size();
if (numGenericPacks != rf->genericPacks.size())
{
numGenericPacks = std::min(lf->genericPacks.size(), rf->genericPacks.size());
errors.push_back(TypeError{location, TypeMismatch{superTy, subTy}});
if (FFlag::LuauExtendedFunctionMismatchError)
errors.push_back(TypeError{location, TypeMismatch{superTy, subTy, "different number of generic type pack parameters"}});
else
errors.push_back(TypeError{location, TypeMismatch{superTy, subTy}});
}
for (size_t i = 0; i < numGenerics; i++)
@ -936,13 +1086,49 @@ void Unifier::tryUnifyFunctions(TypeId superTy, TypeId subTy, bool isFunctionCal
{
Unifier innerState = makeChildUnifier();
ctx = CountMismatch::Arg;
innerState.tryUnify_(rf->argTypes, lf->argTypes, isFunctionCall);
if (FFlag::LuauExtendedFunctionMismatchError)
{
innerState.ctx = CountMismatch::Arg;
innerState.tryUnify_(rf->argTypes, lf->argTypes, isFunctionCall);
ctx = CountMismatch::Result;
innerState.tryUnify_(lf->retType, rf->retType);
bool reported = !innerState.errors.empty();
checkChildUnifierTypeMismatch(innerState.errors, superTy, subTy);
if (auto e = hasUnificationTooComplex(innerState.errors))
errors.push_back(*e);
else if (!innerState.errors.empty() && innerState.firstPackErrorPos)
errors.push_back(
TypeError{location, TypeMismatch{superTy, subTy, format("Argument #%d type is not compatible.", *innerState.firstPackErrorPos),
innerState.errors.front()}});
else if (!innerState.errors.empty())
errors.push_back(TypeError{location, TypeMismatch{superTy, subTy, "", innerState.errors.front()}});
innerState.ctx = CountMismatch::Result;
innerState.tryUnify_(lf->retType, rf->retType);
if (!reported)
{
if (auto e = hasUnificationTooComplex(innerState.errors))
errors.push_back(*e);
else if (!innerState.errors.empty() && size(lf->retType) == 1 && finite(lf->retType))
errors.push_back(TypeError{location, TypeMismatch{superTy, subTy, "Return type is not compatible.", innerState.errors.front()}});
else if (!innerState.errors.empty() && innerState.firstPackErrorPos)
errors.push_back(
TypeError{location, TypeMismatch{superTy, subTy, format("Return #%d type is not compatible.", *innerState.firstPackErrorPos),
innerState.errors.front()}});
else if (!innerState.errors.empty())
errors.push_back(TypeError{location, TypeMismatch{superTy, subTy, "", innerState.errors.front()}});
}
}
else
{
ctx = CountMismatch::Arg;
innerState.tryUnify_(rf->argTypes, lf->argTypes, isFunctionCall);
ctx = CountMismatch::Result;
innerState.tryUnify_(lf->retType, rf->retType);
checkChildUnifierTypeMismatch(innerState.errors, superTy, subTy);
}
log.concat(std::move(innerState.log));
}
@ -994,7 +1180,7 @@ struct Resetter
void Unifier::tryUnifyTables(TypeId left, TypeId right, bool isIntersection)
{
if (!FFlag::LuauTableSubtypingVariance)
if (!FFlag::LuauTableSubtypingVariance2)
return DEPRECATED_tryUnifyTables(left, right, isIntersection);
TableTypeVar* lt = getMutable<TableTypeVar>(left);
@ -1133,7 +1319,7 @@ void Unifier::tryUnifyTables(TypeId left, TypeId right, bool isIntersection)
// TODO: hopefully readonly/writeonly properties will fix this.
Property clone = prop;
clone.type = deeplyOptional(clone.type);
log(lt);
log(left);
lt->props[name] = clone;
}
else if (variance == Covariant)
@ -1146,7 +1332,7 @@ void Unifier::tryUnifyTables(TypeId left, TypeId right, bool isIntersection)
}
else if (lt->state == TableState::Free)
{
log(lt);
log(left);
lt->props[name] = prop;
}
else
@ -1176,7 +1362,7 @@ void Unifier::tryUnifyTables(TypeId left, TypeId right, bool isIntersection)
// e.g. table.insert(t, 1) where t is a non-sealed table and doesn't have an indexer.
// TODO: we only need to do this if the supertype's indexer is read/write
// since that can add indexed elements.
log(rt);
log(right);
rt->indexer = lt->indexer;
}
}
@ -1185,7 +1371,7 @@ void Unifier::tryUnifyTables(TypeId left, TypeId right, bool isIntersection)
// Symmetric if we are invariant
if (lt->state == TableState::Unsealed || lt->state == TableState::Free)
{
log(lt);
log(left);
lt->indexer = rt->indexer;
}
}
@ -1241,15 +1427,15 @@ TypeId Unifier::deeplyOptional(TypeId ty, std::unordered_map<TypeId, TypeId> see
TableTypeVar* resultTtv = getMutable<TableTypeVar>(result);
for (auto& [name, prop] : resultTtv->props)
prop.type = deeplyOptional(prop.type, seen);
return types->addType(UnionTypeVar{{singletonTypes.nilType, result}});
return types->addType(UnionTypeVar{{getSingletonTypes().nilType, result}});
}
else
return types->addType(UnionTypeVar{{singletonTypes.nilType, ty}});
return types->addType(UnionTypeVar{{getSingletonTypes().nilType, ty}});
}
void Unifier::DEPRECATED_tryUnifyTables(TypeId left, TypeId right, bool isIntersection)
{
LUAU_ASSERT(!FFlag::LuauTableSubtypingVariance);
LUAU_ASSERT(!FFlag::LuauTableSubtypingVariance2);
Resetter resetter{&variance};
variance = Invariant;
@ -1467,7 +1653,7 @@ void Unifier::tryUnifySealedTables(TypeId left, TypeId right, bool isIntersectio
}
else if (lt->indexer)
{
innerState.tryUnify_(lt->indexer->indexType, singletonTypes.stringType);
innerState.tryUnify_(lt->indexer->indexType, getSingletonTypes().stringType);
// We already try to unify properties in both tables.
// Skip those and just look for the ones remaining and see if they fit into the indexer.
for (const auto& [name, type] : rt->props)
@ -1636,7 +1822,7 @@ void Unifier::tryUnifyWithClass(TypeId superTy, TypeId subTy, bool reversed)
ok = false;
errors.push_back(TypeError{location, UnknownProperty{superTy, propName}});
if (!FFlag::LuauExtendedClassMismatchError)
tryUnify_(prop.type, singletonTypes.errorRecoveryType());
tryUnify_(prop.type, getSingletonTypes().errorRecoveryType());
}
else
{
@ -1825,7 +2011,7 @@ void Unifier::tryUnifyWithAny(TypeId any, TypeId ty)
if (get<PrimitiveTypeVar>(ty) || get<AnyTypeVar>(ty) || get<ClassTypeVar>(ty))
return;
const TypePackId anyTypePack = types->addTypePack(TypePackVar{VariadicTypePack{singletonTypes.anyType}});
const TypePackId anyTypePack = types->addTypePack(TypePackVar{VariadicTypePack{getSingletonTypes().anyType}});
const TypePackId anyTP = get<AnyTypeVar>(any) ? anyTypePack : types->addTypePack(TypePackVar{Unifiable::Error{}});
@ -1834,14 +2020,14 @@ void Unifier::tryUnifyWithAny(TypeId any, TypeId ty)
sharedState.tempSeenTy.clear();
sharedState.tempSeenTp.clear();
Luau::tryUnifyWithAny(queue, *this, sharedState.tempSeenTy, sharedState.tempSeenTp, singletonTypes.anyType, anyTP);
Luau::tryUnifyWithAny(queue, *this, sharedState.tempSeenTy, sharedState.tempSeenTp, getSingletonTypes().anyType, anyTP);
}
void Unifier::tryUnifyWithAny(TypePackId any, TypePackId ty)
{
LUAU_ASSERT(get<Unifiable::Error>(any));
const TypeId anyTy = singletonTypes.errorRecoveryType();
const TypeId anyTy = getSingletonTypes().errorRecoveryType();
std::vector<TypeId> queue;
@ -1887,7 +2073,7 @@ void Unifier::occursCheck(DenseHashSet<TypeId>& seen, TypeId needle, TypeId hays
{
errors.push_back(TypeError{location, OccursCheckFailed{}});
log(needle);
*asMutable(needle) = *singletonTypes.errorRecoveryType();
*asMutable(needle) = *getSingletonTypes().errorRecoveryType();
return;
}
@ -1951,7 +2137,7 @@ void Unifier::occursCheck(DenseHashSet<TypePackId>& seen, TypePackId needle, Typ
{
errors.push_back(TypeError{location, OccursCheckFailed{}});
log(needle);
*asMutable(needle) = *singletonTypes.errorRecoveryTypePack();
*asMutable(needle) = *getSingletonTypes().errorRecoveryTypePack();
return;
}
@ -2005,7 +2191,7 @@ void Unifier::checkChildUnifierTypeMismatch(const ErrorVec& innerErrors, const s
errors.push_back(*e);
else if (!innerErrors.empty())
errors.push_back(
TypeError{location, TypeMismatch{wantedType, givenType, format("Property '%s' is not compatible", prop.c_str()), innerErrors.front()}});
TypeError{location, TypeMismatch{wantedType, givenType, format("Property '%s' is not compatible.", prop.c_str()), innerErrors.front()}});
}
void Unifier::ice(const std::string& message, const Location& location)

View file

@ -10,7 +10,6 @@
// See docs/SyntaxChanges.md for an explanation.
LUAU_FASTINTVARIABLE(LuauRecursionLimit, 1000)
LUAU_FASTINTVARIABLE(LuauParseErrorLimit, 100)
LUAU_FASTFLAGVARIABLE(LuauCaptureBrokenCommentSpans, false)
LUAU_FASTFLAGVARIABLE(LuauIfElseExpressionBaseSupport, false)
LUAU_FASTFLAGVARIABLE(LuauIfStatementRecursionGuard, false)
LUAU_FASTFLAGVARIABLE(LuauFixAmbiguousErrorRecoveryInAssign, false)
@ -159,7 +158,7 @@ ParseResult Parser::parse(const char* buffer, size_t bufferSize, AstNameTable& n
{
std::vector<std::string> hotcomments;
while (isComment(p.lexer.current()) || (FFlag::LuauCaptureBrokenCommentSpans && p.lexer.current().type == Lexeme::BrokenComment))
while (isComment(p.lexer.current()) || p.lexer.current().type == Lexeme::BrokenComment)
{
const char* text = p.lexer.current().data;
unsigned int length = p.lexer.current().length;
@ -2780,7 +2779,7 @@ const Lexeme& Parser::nextLexeme()
const Lexeme& lexeme = lexer.next(/*skipComments*/ false);
// Subtlety: Broken comments are weird because we record them as comments AND pass them to the parser as a lexeme.
// The parser will turn this into a proper syntax error.
if (FFlag::LuauCaptureBrokenCommentSpans && lexeme.type == Lexeme::BrokenComment)
if (lexeme.type == Lexeme::BrokenComment)
commentLocations.push_back(Comment{lexeme.type, lexeme.location});
if (isComment(lexeme))
commentLocations.push_back(Comment{lexeme.type, lexeme.location});

View file

@ -11,26 +11,33 @@
enum class ReportFormat
{
Default,
Luacheck
Luacheck,
Gnu,
};
static void report(ReportFormat format, const char* name, const Luau::Location& location, const char* type, const char* message)
static void report(ReportFormat format, const char* name, const Luau::Location& loc, const char* type, const char* message)
{
switch (format)
{
case ReportFormat::Default:
fprintf(stderr, "%s(%d,%d): %s: %s\n", name, location.begin.line + 1, location.begin.column + 1, type, message);
fprintf(stderr, "%s(%d,%d): %s: %s\n", name, loc.begin.line + 1, loc.begin.column + 1, type, message);
break;
case ReportFormat::Luacheck:
{
// Note: luacheck's end column is inclusive but our end column is exclusive
// In addition, luacheck doesn't support multi-line messages, so if the error is multiline we'll fake end column as 100 and hope for the best
int columnEnd = (location.begin.line == location.end.line) ? location.end.column : 100;
int columnEnd = (loc.begin.line == loc.end.line) ? loc.end.column : 100;
fprintf(stdout, "%s:%d:%d-%d: (W0) %s: %s\n", name, location.begin.line + 1, location.begin.column + 1, columnEnd, type, message);
// Use stdout to match luacheck behavior
fprintf(stdout, "%s:%d:%d-%d: (W0) %s: %s\n", name, loc.begin.line + 1, loc.begin.column + 1, columnEnd, type, message);
break;
}
case ReportFormat::Gnu:
// Note: GNU end column is inclusive but our end column is exclusive
fprintf(stderr, "%s:%d.%d-%d.%d: %s: %s\n", name, loc.begin.line + 1, loc.begin.column + 1, loc.end.line + 1, loc.end.column, type, message);
break;
}
}
@ -97,6 +104,7 @@ static void displayHelp(const char* argv0)
printf("\n");
printf("Available options:\n");
printf(" --formatter=plain: report analysis errors in Luacheck-compatible format\n");
printf(" --formatter=gnu: report analysis errors in GNU-compatible format\n");
}
static int assertionHandler(const char* expr, const char* file, int line)
@ -201,6 +209,8 @@ int main(int argc, char** argv)
if (strcmp(argv[i], "--formatter=plain") == 0)
format = ReportFormat::Luacheck;
else if (strcmp(argv[i], "--formatter=gnu") == 0)
format = ReportFormat::Gnu;
else if (strcmp(argv[i], "--annotate") == 0)
annotate = true;
}

88
CLI/Coverage.cpp Normal file
View file

@ -0,0 +1,88 @@
// This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details
#include "Coverage.h"
#include "lua.h"
#include <string>
#include <vector>
struct Coverage
{
lua_State* L = nullptr;
std::vector<int> functions;
} gCoverage;
void coverageInit(lua_State* L)
{
gCoverage.L = lua_mainthread(L);
}
bool coverageActive()
{
return gCoverage.L != nullptr;
}
void coverageTrack(lua_State* L, int funcindex)
{
int ref = lua_ref(L, funcindex);
gCoverage.functions.push_back(ref);
}
static void coverageCallback(void* context, const char* function, int linedefined, int depth, const int* hits, size_t size)
{
FILE* f = static_cast<FILE*>(context);
std::string name;
if (depth == 0)
name = "<main>";
else if (function)
name = std::string(function) + ":" + std::to_string(linedefined);
else
name = "<anonymous>:" + std::to_string(linedefined);
fprintf(f, "FN:%d,%s\n", linedefined, name.c_str());
for (size_t i = 0; i < size; ++i)
if (hits[i] != -1)
{
fprintf(f, "FNDA:%d,%s\n", hits[i], name.c_str());
break;
}
for (size_t i = 0; i < size; ++i)
if (hits[i] != -1)
fprintf(f, "DA:%d,%d\n", int(i), hits[i]);
}
void coverageDump(const char* path)
{
lua_State* L = gCoverage.L;
FILE* f = fopen(path, "w");
if (!f)
{
fprintf(stderr, "Error opening coverage %s\n", path);
return;
}
fprintf(f, "TN:\n");
for (int fref: gCoverage.functions)
{
lua_getref(L, fref);
lua_Debug ar = {};
lua_getinfo(L, -1, "s", &ar);
fprintf(f, "SF:%s\n", ar.short_src);
lua_getcoverage(L, -1, f, coverageCallback);
fprintf(f, "end_of_record\n");
lua_pop(L, 1);
}
fclose(f);
printf("Coverage dump written to %s (%d functions)\n", path, int(gCoverage.functions.size()));
}

10
CLI/Coverage.h Normal file
View file

@ -0,0 +1,10 @@
// This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details
#pragma once
struct lua_State;
void coverageInit(lua_State* L);
bool coverageActive();
void coverageTrack(lua_State* L, int funcindex);
void coverageDump(const char* path);

View file

@ -110,12 +110,12 @@ void profilerStop()
gProfiler.thread.join();
}
void profilerDump(const char* name)
void profilerDump(const char* path)
{
FILE* f = fopen(name, "wb");
FILE* f = fopen(path, "wb");
if (!f)
{
fprintf(stderr, "Error opening profile %s\n", name);
fprintf(stderr, "Error opening profile %s\n", path);
return;
}
@ -129,7 +129,7 @@ void profilerDump(const char* name)
fclose(f);
printf("Profiler dump written to %s (total runtime %.3f seconds, %lld samples, %lld stacks)\n", name, double(total) / 1e6,
printf("Profiler dump written to %s (total runtime %.3f seconds, %lld samples, %lld stacks)\n", path, double(total) / 1e6,
static_cast<long long>(gProfiler.samples.load()), static_cast<long long>(gProfiler.data.size()));
uint64_t totalgc = 0;

View file

@ -5,4 +5,4 @@ struct lua_State;
void profilerStart(lua_State* L, int frequency);
void profilerStop();
void profilerDump(const char* name);
void profilerDump(const char* path);

View file

@ -8,6 +8,7 @@
#include "FileUtils.h"
#include "Profiler.h"
#include "Coverage.h"
#include "linenoise.hpp"
@ -24,6 +25,16 @@ enum class CompileFormat
Binary
};
static Luau::CompileOptions copts()
{
Luau::CompileOptions result = {};
result.optimizationLevel = 1;
result.debugLevel = 1;
result.coverageLevel = coverageActive() ? 2 : 0;
return result;
}
static int lua_loadstring(lua_State* L)
{
size_t l = 0;
@ -32,7 +43,7 @@ static int lua_loadstring(lua_State* L)
lua_setsafeenv(L, LUA_ENVIRONINDEX, false);
std::string bytecode = Luau::compile(std::string(s, l));
std::string bytecode = Luau::compile(std::string(s, l), copts());
if (luau_load(L, chunkname, bytecode.data(), bytecode.size(), 0) == 0)
return 1;
@ -79,9 +90,12 @@ static int lua_require(lua_State* L)
luaL_sandboxthread(ML);
// now we can compile & run module on the new thread
std::string bytecode = Luau::compile(*source);
std::string bytecode = Luau::compile(*source, copts());
if (luau_load(ML, chunkname.c_str(), bytecode.data(), bytecode.size(), 0) == 0)
{
if (coverageActive())
coverageTrack(ML, -1);
int status = lua_resume(ML, L, 0);
if (status == 0)
@ -149,7 +163,7 @@ static void setupState(lua_State* L)
static std::string runCode(lua_State* L, const std::string& source)
{
std::string bytecode = Luau::compile(source);
std::string bytecode = Luau::compile(source, copts());
if (luau_load(L, "=stdin", bytecode.data(), bytecode.size(), 0) != 0)
{
@ -329,11 +343,14 @@ static bool runFile(const char* name, lua_State* GL)
std::string chunkname = "=" + std::string(name);
std::string bytecode = Luau::compile(*source);
std::string bytecode = Luau::compile(*source, copts());
int status = 0;
if (luau_load(L, chunkname.c_str(), bytecode.data(), bytecode.size(), 0) == 0)
{
if (coverageActive())
coverageTrack(L, -1);
status = lua_resume(L, NULL, 0);
}
else
@ -437,6 +454,7 @@ static void displayHelp(const char* argv0)
printf("\n");
printf("Available options:\n");
printf(" --profile[=N]: profile the code using N Hz sampling (default 10000) and output results to profile.out\n");
printf(" --coverage: collect code coverage while running the code and output results to coverage.out\n");
}
static int assertionHandler(const char* expr, const char* file, int line)
@ -495,6 +513,7 @@ int main(int argc, char** argv)
setupState(L);
int profile = 0;
bool coverage = false;
for (int i = 1; i < argc; ++i)
{
@ -505,11 +524,16 @@ int main(int argc, char** argv)
profile = 10000; // default to 10 KHz
else if (strncmp(argv[i], "--profile=", 10) == 0)
profile = atoi(argv[i] + 10);
else if (strcmp(argv[i], "--coverage") == 0)
coverage = true;
}
if (profile)
profilerStart(L, profile);
if (coverage)
coverageInit(L);
std::vector<std::string> files = getSourceFiles(argc, argv);
int failed = 0;
@ -523,6 +547,9 @@ int main(int argc, char** argv)
profilerDump("profile.out");
}
if (coverage)
coverageDump("coverage.out");
return failed;
}
}

View file

@ -10,7 +10,6 @@
#include <bitset>
#include <math.h>
LUAU_FASTFLAGVARIABLE(LuauPreloadClosures, false)
LUAU_FASTFLAG(LuauIfElseExpressionBaseSupport)
LUAU_FASTFLAGVARIABLE(LuauBit32CountBuiltin, false)
@ -462,20 +461,17 @@ struct Compiler
bool shared = false;
if (FFlag::LuauPreloadClosures)
// Optimization: when closure has no upvalues, or upvalues are safe to share, instead of allocating it every time we can share closure
// objects (this breaks assumptions about function identity which can lead to setfenv not working as expected, so we disable this when it
// is used)
if (options.optimizationLevel >= 1 && shouldShareClosure(expr) && !setfenvUsed)
{
// Optimization: when closure has no upvalues, or upvalues are safe to share, instead of allocating it every time we can share closure
// objects (this breaks assumptions about function identity which can lead to setfenv not working as expected, so we disable this when it
// is used)
if (options.optimizationLevel >= 1 && shouldShareClosure(expr) && !setfenvUsed)
{
int32_t cid = bytecode.addConstantClosure(f->id);
int32_t cid = bytecode.addConstantClosure(f->id);
if (cid >= 0 && cid < 32768)
{
bytecode.emitAD(LOP_DUPCLOSURE, target, cid);
shared = true;
}
if (cid >= 0 && cid < 32768)
{
bytecode.emitAD(LOP_DUPCLOSURE, target, cid);
shared = true;
}
}

View file

@ -27,7 +27,7 @@ TESTS_SOURCES=$(wildcard tests/*.cpp)
TESTS_OBJECTS=$(TESTS_SOURCES:%=$(BUILD)/%.o)
TESTS_TARGET=$(BUILD)/luau-tests
REPL_CLI_SOURCES=CLI/FileUtils.cpp CLI/Profiler.cpp CLI/Repl.cpp
REPL_CLI_SOURCES=CLI/FileUtils.cpp CLI/Profiler.cpp CLI/Coverage.cpp CLI/Repl.cpp
REPL_CLI_OBJECTS=$(REPL_CLI_SOURCES:%=$(BUILD)/%.o)
REPL_CLI_TARGET=$(BUILD)/luau
@ -128,10 +128,10 @@ luau-size: luau
# executable target aliases
luau: $(REPL_CLI_TARGET)
cp $^ $@
ln -fs $^ $@
luau-analyze: $(ANALYZE_CLI_TARGET)
cp $^ $@
ln -fs $^ $@
# executable targets
$(TESTS_TARGET): $(TESTS_OBJECTS) $(ANALYSIS_TARGET) $(COMPILER_TARGET) $(AST_TARGET) $(VM_TARGET)

View file

@ -133,6 +133,7 @@ target_sources(Luau.VM PRIVATE
VM/src/ltable.cpp
VM/src/ltablib.cpp
VM/src/ltm.cpp
VM/src/ludata.cpp
VM/src/lutf8lib.cpp
VM/src/lvmexecute.cpp
VM/src/lvmload.cpp
@ -152,12 +153,15 @@ target_sources(Luau.VM PRIVATE
VM/src/lstring.h
VM/src/ltable.h
VM/src/ltm.h
VM/src/ludata.h
VM/src/lvm.h
)
if(TARGET Luau.Repl.CLI)
# Luau.Repl.CLI Sources
target_sources(Luau.Repl.CLI PRIVATE
CLI/Coverage.h
CLI/Coverage.cpp
CLI/FileUtils.h
CLI/FileUtils.cpp
CLI/Profiler.h

View file

@ -334,6 +334,10 @@ LUA_API const char* lua_setupvalue(lua_State* L, int funcindex, int n);
LUA_API void lua_singlestep(lua_State* L, int enabled);
LUA_API void lua_breakpoint(lua_State* L, int funcindex, int line, int enabled);
typedef void (*lua_Coverage)(void* context, const char* function, int linedefined, int depth, const int* hits, size_t size);
LUA_API void lua_getcoverage(lua_State* L, int funcindex, void* context, lua_Coverage callback);
/* Warning: this function is not thread-safe since it stores the result in a shared global array! Only use for debugging. */
LUA_API const char* lua_debugtrace(lua_State* L);

View file

@ -8,6 +8,7 @@
#include "lfunc.h"
#include "lgc.h"
#include "ldo.h"
#include "ludata.h"
#include "lvm.h"
#include "lnumutils.h"
@ -43,36 +44,30 @@ static Table* getcurrenv(lua_State* L)
}
}
static LUAU_NOINLINE TValue* index2adrslow(lua_State* L, int idx)
static LUAU_NOINLINE TValue* pseudo2addr(lua_State* L, int idx)
{
api_check(L, idx <= 0);
if (idx > LUA_REGISTRYINDEX)
api_check(L, lua_ispseudo(idx));
switch (idx)
{ /* pseudo-indices */
case LUA_REGISTRYINDEX:
return registry(L);
case LUA_ENVIRONINDEX:
{
api_check(L, idx != 0 && -idx <= L->top - L->base);
return L->top + idx;
sethvalue(L, &L->env, getcurrenv(L));
return &L->env;
}
case LUA_GLOBALSINDEX:
return gt(L);
default:
{
Closure* func = curr_func(L);
idx = LUA_GLOBALSINDEX - idx;
return (idx <= func->nupvalues) ? &func->c.upvals[idx - 1] : cast_to(TValue*, luaO_nilobject);
}
}
else
switch (idx)
{ /* pseudo-indices */
case LUA_REGISTRYINDEX:
return registry(L);
case LUA_ENVIRONINDEX:
{
sethvalue(L, &L->env, getcurrenv(L));
return &L->env;
}
case LUA_GLOBALSINDEX:
return gt(L);
default:
{
Closure* func = curr_func(L);
idx = LUA_GLOBALSINDEX - idx;
return (idx <= func->nupvalues) ? &func->c.upvals[idx - 1] : cast_to(TValue*, luaO_nilobject);
}
}
}
static LUAU_FORCEINLINE TValue* index2adr(lua_State* L, int idx)
static LUAU_FORCEINLINE TValue* index2addr(lua_State* L, int idx)
{
if (idx > 0)
{
@ -83,15 +78,20 @@ static LUAU_FORCEINLINE TValue* index2adr(lua_State* L, int idx)
else
return o;
}
else if (idx > LUA_REGISTRYINDEX)
{
api_check(L, idx != 0 && -idx <= L->top - L->base);
return L->top + idx;
}
else
{
return index2adrslow(L, idx);
return pseudo2addr(L, idx);
}
}
const TValue* luaA_toobject(lua_State* L, int idx)
{
StkId p = index2adr(L, idx);
StkId p = index2addr(L, idx);
return (p == luaO_nilobject) ? NULL : p;
}
@ -145,7 +145,7 @@ void lua_xpush(lua_State* from, lua_State* to, int idx)
{
api_check(from, from->global == to->global);
luaC_checkthreadsleep(to);
setobj2s(to, to->top, index2adr(from, idx));
setobj2s(to, to->top, index2addr(from, idx));
api_incr_top(to);
return;
}
@ -202,7 +202,7 @@ void lua_settop(lua_State* L, int idx)
void lua_remove(lua_State* L, int idx)
{
StkId p = index2adr(L, idx);
StkId p = index2addr(L, idx);
api_checkvalidindex(L, p);
while (++p < L->top)
setobjs2s(L, p - 1, p);
@ -213,7 +213,7 @@ void lua_remove(lua_State* L, int idx)
void lua_insert(lua_State* L, int idx)
{
luaC_checkthreadsleep(L);
StkId p = index2adr(L, idx);
StkId p = index2addr(L, idx);
api_checkvalidindex(L, p);
for (StkId q = L->top; q > p; q--)
setobjs2s(L, q, q - 1);
@ -228,7 +228,7 @@ void lua_replace(lua_State* L, int idx)
luaG_runerror(L, "no calling environment");
api_checknelems(L, 1);
luaC_checkthreadsleep(L);
StkId o = index2adr(L, idx);
StkId o = index2addr(L, idx);
api_checkvalidindex(L, o);
if (idx == LUA_ENVIRONINDEX)
{
@ -250,7 +250,7 @@ void lua_replace(lua_State* L, int idx)
void lua_pushvalue(lua_State* L, int idx)
{
luaC_checkthreadsleep(L);
StkId o = index2adr(L, idx);
StkId o = index2addr(L, idx);
setobj2s(L, L->top, o);
api_incr_top(L);
return;
@ -262,7 +262,7 @@ void lua_pushvalue(lua_State* L, int idx)
int lua_type(lua_State* L, int idx)
{
StkId o = index2adr(L, idx);
StkId o = index2addr(L, idx);
return (o == luaO_nilobject) ? LUA_TNONE : ttype(o);
}
@ -273,20 +273,20 @@ const char* lua_typename(lua_State* L, int t)
int lua_iscfunction(lua_State* L, int idx)
{
StkId o = index2adr(L, idx);
StkId o = index2addr(L, idx);
return iscfunction(o);
}
int lua_isLfunction(lua_State* L, int idx)
{
StkId o = index2adr(L, idx);
StkId o = index2addr(L, idx);
return isLfunction(o);
}
int lua_isnumber(lua_State* L, int idx)
{
TValue n;
const TValue* o = index2adr(L, idx);
const TValue* o = index2addr(L, idx);
return tonumber(o, &n);
}
@ -298,14 +298,14 @@ int lua_isstring(lua_State* L, int idx)
int lua_isuserdata(lua_State* L, int idx)
{
const TValue* o = index2adr(L, idx);
const TValue* o = index2addr(L, idx);
return (ttisuserdata(o) || ttislightuserdata(o));
}
int lua_rawequal(lua_State* L, int index1, int index2)
{
StkId o1 = index2adr(L, index1);
StkId o2 = index2adr(L, index2);
StkId o1 = index2addr(L, index1);
StkId o2 = index2addr(L, index2);
return (o1 == luaO_nilobject || o2 == luaO_nilobject) ? 0 : luaO_rawequalObj(o1, o2);
}
@ -313,8 +313,8 @@ int lua_equal(lua_State* L, int index1, int index2)
{
StkId o1, o2;
int i;
o1 = index2adr(L, index1);
o2 = index2adr(L, index2);
o1 = index2addr(L, index1);
o2 = index2addr(L, index2);
i = (o1 == luaO_nilobject || o2 == luaO_nilobject) ? 0 : equalobj(L, o1, o2);
return i;
}
@ -323,8 +323,8 @@ int lua_lessthan(lua_State* L, int index1, int index2)
{
StkId o1, o2;
int i;
o1 = index2adr(L, index1);
o2 = index2adr(L, index2);
o1 = index2addr(L, index1);
o2 = index2addr(L, index2);
i = (o1 == luaO_nilobject || o2 == luaO_nilobject) ? 0 : luaV_lessthan(L, o1, o2);
return i;
}
@ -332,7 +332,7 @@ int lua_lessthan(lua_State* L, int index1, int index2)
double lua_tonumberx(lua_State* L, int idx, int* isnum)
{
TValue n;
const TValue* o = index2adr(L, idx);
const TValue* o = index2addr(L, idx);
if (tonumber(o, &n))
{
if (isnum)
@ -350,7 +350,7 @@ double lua_tonumberx(lua_State* L, int idx, int* isnum)
int lua_tointegerx(lua_State* L, int idx, int* isnum)
{
TValue n;
const TValue* o = index2adr(L, idx);
const TValue* o = index2addr(L, idx);
if (tonumber(o, &n))
{
int res;
@ -371,7 +371,7 @@ int lua_tointegerx(lua_State* L, int idx, int* isnum)
unsigned lua_tounsignedx(lua_State* L, int idx, int* isnum)
{
TValue n;
const TValue* o = index2adr(L, idx);
const TValue* o = index2addr(L, idx);
if (tonumber(o, &n))
{
unsigned res;
@ -391,13 +391,13 @@ unsigned lua_tounsignedx(lua_State* L, int idx, int* isnum)
int lua_toboolean(lua_State* L, int idx)
{
const TValue* o = index2adr(L, idx);
const TValue* o = index2addr(L, idx);
return !l_isfalse(o);
}
const char* lua_tolstring(lua_State* L, int idx, size_t* len)
{
StkId o = index2adr(L, idx);
StkId o = index2addr(L, idx);
if (!ttisstring(o))
{
luaC_checkthreadsleep(L);
@ -408,7 +408,7 @@ const char* lua_tolstring(lua_State* L, int idx, size_t* len)
return NULL;
}
luaC_checkGC(L);
o = index2adr(L, idx); /* previous call may reallocate the stack */
o = index2addr(L, idx); /* previous call may reallocate the stack */
}
if (len != NULL)
*len = tsvalue(o)->len;
@ -417,7 +417,7 @@ const char* lua_tolstring(lua_State* L, int idx, size_t* len)
const char* lua_tostringatom(lua_State* L, int idx, int* atom)
{
StkId o = index2adr(L, idx);
StkId o = index2addr(L, idx);
if (!ttisstring(o))
return NULL;
const TString* s = tsvalue(o);
@ -438,7 +438,7 @@ const char* lua_namecallatom(lua_State* L, int* atom)
const float* lua_tovector(lua_State* L, int idx)
{
StkId o = index2adr(L, idx);
StkId o = index2addr(L, idx);
if (!ttisvector(o))
{
return NULL;
@ -448,7 +448,7 @@ const float* lua_tovector(lua_State* L, int idx)
int lua_objlen(lua_State* L, int idx)
{
StkId o = index2adr(L, idx);
StkId o = index2addr(L, idx);
switch (ttype(o))
{
case LUA_TSTRING:
@ -469,13 +469,13 @@ int lua_objlen(lua_State* L, int idx)
lua_CFunction lua_tocfunction(lua_State* L, int idx)
{
StkId o = index2adr(L, idx);
StkId o = index2addr(L, idx);
return (!iscfunction(o)) ? NULL : cast_to(lua_CFunction, clvalue(o)->c.f);
}
void* lua_touserdata(lua_State* L, int idx)
{
StkId o = index2adr(L, idx);
StkId o = index2addr(L, idx);
switch (ttype(o))
{
case LUA_TUSERDATA:
@ -489,13 +489,13 @@ void* lua_touserdata(lua_State* L, int idx)
void* lua_touserdatatagged(lua_State* L, int idx, int tag)
{
StkId o = index2adr(L, idx);
StkId o = index2addr(L, idx);
return (ttisuserdata(o) && uvalue(o)->tag == tag) ? uvalue(o)->data : NULL;
}
int lua_userdatatag(lua_State* L, int idx)
{
StkId o = index2adr(L, idx);
StkId o = index2addr(L, idx);
if (ttisuserdata(o))
return uvalue(o)->tag;
return -1;
@ -503,13 +503,13 @@ int lua_userdatatag(lua_State* L, int idx)
lua_State* lua_tothread(lua_State* L, int idx)
{
StkId o = index2adr(L, idx);
StkId o = index2addr(L, idx);
return (!ttisthread(o)) ? NULL : thvalue(o);
}
const void* lua_topointer(lua_State* L, int idx)
{
StkId o = index2adr(L, idx);
StkId o = index2addr(L, idx);
switch (ttype(o))
{
case LUA_TTABLE:
@ -657,7 +657,7 @@ int lua_pushthread(lua_State* L)
void lua_gettable(lua_State* L, int idx)
{
luaC_checkthreadsleep(L);
StkId t = index2adr(L, idx);
StkId t = index2addr(L, idx);
api_checkvalidindex(L, t);
luaV_gettable(L, t, L->top - 1, L->top - 1);
return;
@ -666,7 +666,7 @@ void lua_gettable(lua_State* L, int idx)
void lua_getfield(lua_State* L, int idx, const char* k)
{
luaC_checkthreadsleep(L);
StkId t = index2adr(L, idx);
StkId t = index2addr(L, idx);
api_checkvalidindex(L, t);
TValue key;
setsvalue(L, &key, luaS_new(L, k));
@ -678,7 +678,7 @@ void lua_getfield(lua_State* L, int idx, const char* k)
void lua_rawgetfield(lua_State* L, int idx, const char* k)
{
luaC_checkthreadsleep(L);
StkId t = index2adr(L, idx);
StkId t = index2addr(L, idx);
api_check(L, ttistable(t));
TValue key;
setsvalue(L, &key, luaS_new(L, k));
@ -690,7 +690,7 @@ void lua_rawgetfield(lua_State* L, int idx, const char* k)
void lua_rawget(lua_State* L, int idx)
{
luaC_checkthreadsleep(L);
StkId t = index2adr(L, idx);
StkId t = index2addr(L, idx);
api_check(L, ttistable(t));
setobj2s(L, L->top - 1, luaH_get(hvalue(t), L->top - 1));
return;
@ -699,7 +699,7 @@ void lua_rawget(lua_State* L, int idx)
void lua_rawgeti(lua_State* L, int idx, int n)
{
luaC_checkthreadsleep(L);
StkId t = index2adr(L, idx);
StkId t = index2addr(L, idx);
api_check(L, ttistable(t));
setobj2s(L, L->top, luaH_getnum(hvalue(t), n));
api_incr_top(L);
@ -717,7 +717,7 @@ void lua_createtable(lua_State* L, int narray, int nrec)
void lua_setreadonly(lua_State* L, int objindex, int enabled)
{
const TValue* o = index2adr(L, objindex);
const TValue* o = index2addr(L, objindex);
api_check(L, ttistable(o));
Table* t = hvalue(o);
api_check(L, t != hvalue(registry(L)));
@ -727,7 +727,7 @@ void lua_setreadonly(lua_State* L, int objindex, int enabled)
int lua_getreadonly(lua_State* L, int objindex)
{
const TValue* o = index2adr(L, objindex);
const TValue* o = index2addr(L, objindex);
api_check(L, ttistable(o));
Table* t = hvalue(o);
int res = t->readonly;
@ -736,7 +736,7 @@ int lua_getreadonly(lua_State* L, int objindex)
void lua_setsafeenv(lua_State* L, int objindex, int enabled)
{
const TValue* o = index2adr(L, objindex);
const TValue* o = index2addr(L, objindex);
api_check(L, ttistable(o));
Table* t = hvalue(o);
t->safeenv = bool(enabled);
@ -748,7 +748,7 @@ int lua_getmetatable(lua_State* L, int objindex)
const TValue* obj;
Table* mt = NULL;
int res;
obj = index2adr(L, objindex);
obj = index2addr(L, objindex);
switch (ttype(obj))
{
case LUA_TTABLE:
@ -775,7 +775,7 @@ int lua_getmetatable(lua_State* L, int objindex)
void lua_getfenv(lua_State* L, int idx)
{
StkId o;
o = index2adr(L, idx);
o = index2addr(L, idx);
api_checkvalidindex(L, o);
switch (ttype(o))
{
@ -801,7 +801,7 @@ void lua_settable(lua_State* L, int idx)
{
StkId t;
api_checknelems(L, 2);
t = index2adr(L, idx);
t = index2addr(L, idx);
api_checkvalidindex(L, t);
luaV_settable(L, t, L->top - 2, L->top - 1);
L->top -= 2; /* pop index and value */
@ -813,7 +813,7 @@ void lua_setfield(lua_State* L, int idx, const char* k)
StkId t;
TValue key;
api_checknelems(L, 1);
t = index2adr(L, idx);
t = index2addr(L, idx);
api_checkvalidindex(L, t);
setsvalue(L, &key, luaS_new(L, k));
luaV_settable(L, t, &key, L->top - 1);
@ -825,7 +825,7 @@ void lua_rawset(lua_State* L, int idx)
{
StkId t;
api_checknelems(L, 2);
t = index2adr(L, idx);
t = index2addr(L, idx);
api_check(L, ttistable(t));
if (hvalue(t)->readonly)
luaG_runerror(L, "Attempt to modify a readonly table");
@ -839,7 +839,7 @@ void lua_rawseti(lua_State* L, int idx, int n)
{
StkId o;
api_checknelems(L, 1);
o = index2adr(L, idx);
o = index2addr(L, idx);
api_check(L, ttistable(o));
if (hvalue(o)->readonly)
luaG_runerror(L, "Attempt to modify a readonly table");
@ -854,7 +854,7 @@ int lua_setmetatable(lua_State* L, int objindex)
TValue* obj;
Table* mt;
api_checknelems(L, 1);
obj = index2adr(L, objindex);
obj = index2addr(L, objindex);
api_checkvalidindex(L, obj);
if (ttisnil(L->top - 1))
mt = NULL;
@ -896,7 +896,7 @@ int lua_setfenv(lua_State* L, int idx)
StkId o;
int res = 1;
api_checknelems(L, 1);
o = index2adr(L, idx);
o = index2addr(L, idx);
api_checkvalidindex(L, o);
api_check(L, ttistable(L->top - 1));
switch (ttype(o))
@ -987,7 +987,7 @@ int lua_pcall(lua_State* L, int nargs, int nresults, int errfunc)
func = 0;
else
{
StkId o = index2adr(L, errfunc);
StkId o = index2addr(L, errfunc);
api_checkvalidindex(L, o);
func = savestack(L, o);
}
@ -1150,7 +1150,7 @@ l_noret lua_error(lua_State* L)
int lua_next(lua_State* L, int idx)
{
luaC_checkthreadsleep(L);
StkId t = index2adr(L, idx);
StkId t = index2addr(L, idx);
api_check(L, ttistable(t));
int more = luaH_next(L, hvalue(t), L->top - 1);
if (more)
@ -1187,7 +1187,7 @@ void* lua_newuserdatatagged(lua_State* L, size_t sz, int tag)
api_check(L, unsigned(tag) < LUA_UTAG_LIMIT);
luaC_checkGC(L);
luaC_checkthreadsleep(L);
Udata* u = luaS_newudata(L, sz, tag);
Udata* u = luaU_newudata(L, sz, tag);
setuvalue(L, L->top, u);
api_incr_top(L);
return u->data;
@ -1197,7 +1197,7 @@ void* lua_newuserdatadtor(lua_State* L, size_t sz, void (*dtor)(void*))
{
luaC_checkGC(L);
luaC_checkthreadsleep(L);
Udata* u = luaS_newudata(L, sz + sizeof(dtor), UTAG_IDTOR);
Udata* u = luaU_newudata(L, sz + sizeof(dtor), UTAG_IDTOR);
memcpy(&u->data + sz, &dtor, sizeof(dtor));
setuvalue(L, L->top, u);
api_incr_top(L);
@ -1232,7 +1232,7 @@ const char* lua_getupvalue(lua_State* L, int funcindex, int n)
{
luaC_checkthreadsleep(L);
TValue* val;
const char* name = aux_upvalue(index2adr(L, funcindex), n, &val);
const char* name = aux_upvalue(index2addr(L, funcindex), n, &val);
if (name)
{
setobj2s(L, L->top, val);
@ -1246,7 +1246,7 @@ const char* lua_setupvalue(lua_State* L, int funcindex, int n)
const char* name;
TValue* val;
StkId fi;
fi = index2adr(L, funcindex);
fi = index2addr(L, funcindex);
api_checknelems(L, 1);
name = aux_upvalue(fi, n, &val);
if (name)
@ -1270,7 +1270,7 @@ int lua_ref(lua_State* L, int idx)
api_check(L, idx != LUA_REGISTRYINDEX); /* idx is a stack index for value */
int ref = LUA_REFNIL;
global_State* g = L->global;
StkId p = index2adr(L, idx);
StkId p = index2addr(L, idx);
if (!ttisnil(p))
{
Table* reg = hvalue(registry(L));

View file

@ -370,6 +370,69 @@ void lua_breakpoint(lua_State* L, int funcindex, int line, int enabled)
luaG_breakpoint(L, clvalue(func)->l.p, line, bool(enabled));
}
static int getmaxline(Proto* p)
{
int result = -1;
for (int i = 0; i < p->sizecode; ++i)
{
int line = luaG_getline(p, i);
result = result < line ? line : result;
}
for (int i = 0; i < p->sizep; ++i)
{
int psize = getmaxline(p->p[i]);
result = result < psize ? psize : result;
}
return result;
}
static void getcoverage(Proto* p, int depth, int* buffer, size_t size, void* context, lua_Coverage callback)
{
memset(buffer, -1, size * sizeof(int));
for (int i = 0; i < p->sizecode; ++i)
{
Instruction insn = p->code[i];
if (LUAU_INSN_OP(insn) != LOP_COVERAGE)
continue;
int line = luaG_getline(p, i);
int hits = LUAU_INSN_E(insn);
LUAU_ASSERT(size_t(line) < size);
buffer[line] = buffer[line] < hits ? hits : buffer[line];
}
const char* debugname = p->debugname ? getstr(p->debugname) : NULL;
int linedefined = luaG_getline(p, 0);
callback(context, debugname, linedefined, depth, buffer, size);
for (int i = 0; i < p->sizep; ++i)
getcoverage(p->p[i], depth + 1, buffer, size, context, callback);
}
void lua_getcoverage(lua_State* L, int funcindex, void* context, lua_Coverage callback)
{
const TValue* func = luaA_toobject(L, funcindex);
api_check(L, ttisfunction(func) && !clvalue(func)->isC);
Proto* p = clvalue(func)->l.p;
size_t size = getmaxline(p) + 1;
if (size == 0)
return;
int* buffer = luaM_newarray(L, size, int, 0);
getcoverage(p, 0, buffer, size, context, callback);
luaM_freearray(L, buffer, size, int, 0);
}
static size_t append(char* buf, size_t bufsize, size_t offset, const char* data)
{
size_t size = strlen(data);

View file

@ -8,11 +8,10 @@
#include "lfunc.h"
#include "lstring.h"
#include "ldo.h"
#include "ludata.h"
#include <string.h>
LUAU_FASTFLAGVARIABLE(LuauSeparateAtomic, false)
LUAU_FASTFLAG(LuauArrayBoundary)
#define GC_SWEEPMAX 40
@ -59,10 +58,6 @@ static void recordGcStateTime(global_State* g, int startgcstate, double seconds,
case GCSpropagate:
case GCSpropagateagain:
g->gcstats.currcycle.marktime += seconds;
// atomic step had to be performed during the switch and it's tracked separately
if (!FFlag::LuauSeparateAtomic && g->gcstate == GCSsweepstring)
g->gcstats.currcycle.marktime -= g->gcstats.currcycle.atomictime;
break;
case GCSatomic:
g->gcstats.currcycle.atomictime += seconds;
@ -488,7 +483,7 @@ static void freeobj(lua_State* L, GCObject* o)
luaS_free(L, gco2ts(o));
break;
case LUA_TUSERDATA:
luaS_freeudata(L, gco2u(o));
luaU_freeudata(L, gco2u(o));
break;
default:
LUAU_ASSERT(0);
@ -632,17 +627,9 @@ static size_t remarkupvals(global_State* g)
static size_t atomic(lua_State* L)
{
global_State* g = L->global;
LUAU_ASSERT(g->gcstate == GCSatomic);
size_t work = 0;
if (FFlag::LuauSeparateAtomic)
{
LUAU_ASSERT(g->gcstate == GCSatomic);
}
else
{
g->gcstate = GCSatomic;
}
/* remark occasional upvalues of (maybe) dead threads */
work += remarkupvals(g);
/* traverse objects caught by write barrier and by 'remarkupvals' */
@ -666,11 +653,6 @@ static size_t atomic(lua_State* L)
g->sweepgc = &g->rootgc;
g->gcstate = GCSsweepstring;
if (!FFlag::LuauSeparateAtomic)
{
GC_INTERRUPT(GCSatomic);
}
return work;
}
@ -716,22 +698,7 @@ static size_t gcstep(lua_State* L, size_t limit)
if (!g->gray) /* no more `gray' objects */
{
if (FFlag::LuauSeparateAtomic)
{
g->gcstate = GCSatomic;
}
else
{
double starttimestamp = lua_clock();
g->gcstats.currcycle.atomicstarttimestamp = starttimestamp;
g->gcstats.currcycle.atomicstarttotalsizebytes = g->totalbytes;
atomic(L); /* finish mark phase */
LUAU_ASSERT(g->gcstate == GCSsweepstring);
g->gcstats.currcycle.atomictime += lua_clock() - starttimestamp;
}
g->gcstate = GCSatomic;
}
break;
}
@ -853,7 +820,7 @@ static size_t getheaptrigger(global_State* g, size_t heapgoal)
void luaC_step(lua_State* L, bool assist)
{
global_State* g = L->global;
ptrdiff_t lim = (g->gcstepsize / 100) * g->gcstepmul; /* how much to work */
int lim = (g->gcstepsize / 100) * g->gcstepmul; /* how much to work */
LUAU_ASSERT(g->totalbytes >= g->GCthreshold);
size_t debt = g->totalbytes - g->GCthreshold;
@ -908,7 +875,7 @@ void luaC_fullgc(lua_State* L)
if (g->gcstate == GCSpause)
startGcCycleStats(g);
if (g->gcstate <= (FFlag::LuauSeparateAtomic ? GCSatomic : GCSpropagateagain))
if (g->gcstate <= GCSatomic)
{
/* reset sweep marks to sweep all elements (returning them to white) */
g->sweepstrgc = 0;
@ -1049,7 +1016,7 @@ int64_t luaC_allocationrate(lua_State* L)
global_State* g = L->global;
const double durationthreshold = 1e-3; // avoid measuring intervals smaller than 1ms
if (g->gcstate <= (FFlag::LuauSeparateAtomic ? GCSatomic : GCSpropagateagain))
if (g->gcstate <= GCSatomic)
{
double duration = lua_clock() - g->gcstats.lastcycle.endtimestamp;

View file

@ -7,6 +7,7 @@
#include "ltable.h"
#include "lfunc.h"
#include "lstring.h"
#include "ludata.h"
#include <string.h>
#include <stdio.h>

View file

@ -78,15 +78,7 @@ typedef struct lua_TValue
#define thvalue(o) check_exp(ttisthread(o), &(o)->value.gc->th)
#define upvalue(o) check_exp(ttisupval(o), &(o)->value.gc->uv)
// beware bit magic: a value is false if it's nil or boolean false
// baseline implementation: (ttisnil(o) || (ttisboolean(o) && bvalue(o) == 0))
// we'd like a branchless version of this which helps with performance, and a very fast version
// so our strategy is to always read the boolean value (not using bvalue(o) because that asserts when type isn't boolean)
// we then combine it with type to produce 0/1 as follows:
// - when type is nil (0), & makes the result 0
// - when type is boolean (1), we effectively only look at the bottom bit, so result is 0 iff boolean value is 0
// - when type is different, it must have some of the top bits set - we keep all top bits of boolean value so the result is non-0
#define l_isfalse(o) (!(((o)->value.b | ~1) & ttype(o)))
#define l_isfalse(o) (ttisnil(o) || (ttisboolean(o) && bvalue(o) == 0))
/*
** for internal debug only

View file

@ -206,32 +206,3 @@ void luaS_free(lua_State* L, TString* ts)
L->global->strt.nuse--;
luaM_free(L, ts, sizestring(ts->len), ts->memcat);
}
Udata* luaS_newudata(lua_State* L, size_t s, int tag)
{
if (s > INT_MAX - sizeof(Udata))
luaM_toobig(L);
Udata* u = luaM_new(L, Udata, sizeudata(s), L->activememcat);
luaC_link(L, u, LUA_TUSERDATA);
u->len = int(s);
u->metatable = NULL;
LUAU_ASSERT(tag >= 0 && tag <= 255);
u->tag = uint8_t(tag);
return u;
}
void luaS_freeudata(lua_State* L, Udata* u)
{
LUAU_ASSERT(u->tag < LUA_UTAG_LIMIT || u->tag == UTAG_IDTOR);
void (*dtor)(void*) = nullptr;
if (u->tag == UTAG_IDTOR)
memcpy(&dtor, &u->data + u->len - sizeof(dtor), sizeof(dtor));
else if (u->tag)
dtor = L->global->udatagc[u->tag];
if (dtor)
dtor(u->data);
luaM_free(L, u, sizeudata(u->len), u->memcat);
}

View file

@ -8,11 +8,7 @@
/* string size limit */
#define MAXSSIZE (1 << 30)
/* special tag value is used for user data with inline dtors */
#define UTAG_IDTOR LUA_UTAG_LIMIT
#define sizestring(len) (offsetof(TString, data) + len + 1)
#define sizeudata(len) (offsetof(Udata, data) + len)
#define luaS_new(L, s) (luaS_newlstr(L, s, strlen(s)))
#define luaS_newliteral(L, s) (luaS_newlstr(L, "" s, (sizeof(s) / sizeof(char)) - 1))
@ -26,8 +22,5 @@ LUAI_FUNC void luaS_resize(lua_State* L, int newsize);
LUAI_FUNC TString* luaS_newlstr(lua_State* L, const char* str, size_t l);
LUAI_FUNC void luaS_free(lua_State* L, TString* ts);
LUAI_FUNC Udata* luaS_newudata(lua_State* L, size_t s, int tag);
LUAI_FUNC void luaS_freeudata(lua_State* L, Udata* u);
LUAI_FUNC TString* luaS_bufstart(lua_State* L, size_t size);
LUAI_FUNC TString* luaS_buffinish(lua_State* L, TString* ts);

37
VM/src/ludata.cpp Normal file
View file

@ -0,0 +1,37 @@
// This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details
// This code is based on Lua 5.x implementation licensed under MIT License; see lua_LICENSE.txt for details
#include "ludata.h"
#include "lgc.h"
#include "lmem.h"
#include <string.h>
Udata* luaU_newudata(lua_State* L, size_t s, int tag)
{
if (s > INT_MAX - sizeof(Udata))
luaM_toobig(L);
Udata* u = luaM_new(L, Udata, sizeudata(s), L->activememcat);
luaC_link(L, u, LUA_TUSERDATA);
u->len = int(s);
u->metatable = NULL;
LUAU_ASSERT(tag >= 0 && tag <= 255);
u->tag = uint8_t(tag);
return u;
}
void luaU_freeudata(lua_State* L, Udata* u)
{
LUAU_ASSERT(u->tag < LUA_UTAG_LIMIT || u->tag == UTAG_IDTOR);
void (*dtor)(void*) = nullptr;
if (u->tag == UTAG_IDTOR)
memcpy(&dtor, &u->data + u->len - sizeof(dtor), sizeof(dtor));
else if (u->tag)
dtor = L->global->udatagc[u->tag];
if (dtor)
dtor(u->data);
luaM_free(L, u, sizeudata(u->len), u->memcat);
}

13
VM/src/ludata.h Normal file
View file

@ -0,0 +1,13 @@
// This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details
// This code is based on Lua 5.x implementation licensed under MIT License; see lua_LICENSE.txt for details
#pragma once
#include "lobject.h"
/* special tag value is used for user data with inline dtors */
#define UTAG_IDTOR LUA_UTAG_LIMIT
#define sizeudata(len) (offsetof(Udata, data) + len)
LUAI_FUNC Udata* luaU_newudata(lua_State* L, size_t s, int tag);
LUAI_FUNC void luaU_freeudata(lua_State* L, Udata* u);

View file

@ -63,7 +63,8 @@
#define VM_KV(i) (LUAU_ASSERT(unsigned(i) < unsigned(cl->l.p->sizek)), &k[i])
#define VM_UV(i) (LUAU_ASSERT(unsigned(i) < unsigned(cl->nupvalues)), &cl->l.uprefs[i])
#define VM_PATCH_C(pc, slot) ((uint8_t*)(pc))[3] = uint8_t(slot)
#define VM_PATCH_C(pc, slot) *const_cast<Instruction*>(pc) = ((uint8_t(slot) << 24) | (0x00ffffffu & *(pc)))
#define VM_PATCH_E(pc, slot) *const_cast<Instruction*>(pc) = ((uint32_t(slot) << 8) | (0x000000ffu & *(pc)))
// NOTE: If debugging the Luau code, disable this macro to prevent timeouts from
// occurring when tracing code in Visual Studio / XCode
@ -120,7 +121,7 @@
*/
#if VM_USE_CGOTO
#define VM_CASE(op) CASE_##op:
#define VM_NEXT() goto*(SingleStep ? &&dispatch : kDispatchTable[*(uint8_t*)pc])
#define VM_NEXT() goto*(SingleStep ? &&dispatch : kDispatchTable[LUAU_INSN_OP(*pc)])
#define VM_CONTINUE(op) goto* kDispatchTable[uint8_t(op)]
#else
#define VM_CASE(op) case op:
@ -325,7 +326,7 @@ static void luau_execute(lua_State* L)
// ... and singlestep logic :)
if (SingleStep)
{
if (L->global->cb.debugstep && !luau_skipstep(*(uint8_t*)pc))
if (L->global->cb.debugstep && !luau_skipstep(LUAU_INSN_OP(*pc)))
{
VM_PROTECT(luau_callhook(L, L->global->cb.debugstep, NULL));
@ -335,13 +336,12 @@ static void luau_execute(lua_State* L)
}
#if VM_USE_CGOTO
VM_CONTINUE(*(uint8_t*)pc);
VM_CONTINUE(LUAU_INSN_OP(*pc));
#endif
}
#if !VM_USE_CGOTO
// Note: this assumes that LUAU_INSN_OP() decodes the first byte (aka least significant byte in the little endian encoding)
size_t dispatchOp = *(uint8_t*)pc;
size_t dispatchOp = LUAU_INSN_OP(*pc);
dispatchContinue:
switch (dispatchOp)
@ -2577,7 +2577,7 @@ static void luau_execute(lua_State* L)
// update hits with saturated add and patch the instruction in place
hits = (hits < (1 << 23) - 1) ? hits + 1 : hits;
((uint32_t*)pc)[-1] = LOP_COVERAGE | (uint32_t(hits) << 8);
VM_PATCH_E(pc - 1, hits);
VM_NEXT();
}

View file

@ -451,15 +451,16 @@ function raytraceScene()
end
function arrayToCanvasCommands(pixels)
local s = '<!DOCTYPE html><html><head><title>Test</title></head><body><canvas id="renderCanvas" width="' .. size .. 'px" height="' .. size .. 'px"></canvas><scr' .. 'ipt>\nvar pixels = [';
local s = {};
table.insert(s, '<!DOCTYPE html><html><head><title>Test</title></head><body><canvas id="renderCanvas" width="' .. size .. 'px" height="' .. size .. 'px"></canvas><scr' .. 'ipt>\nvar pixels = [');
for y = 0,size-1 do
s = s .. "[";
table.insert(s, "[");
for x = 0,size-1 do
s = s .. "[" .. math.floor(pixels[y + 1][x + 1][1] * 255) .. "," .. math.floor(pixels[y + 1][x + 1][2] * 255) .. "," .. math.floor(pixels[y + 1][x + 1][3] * 255) .. "],";
table.insert(s, "[" .. math.floor(pixels[y + 1][x + 1][1] * 255) .. "," .. math.floor(pixels[y + 1][x + 1][2] * 255) .. "," .. math.floor(pixels[y + 1][x + 1][3] * 255) .. "],");
end
s = s .. "],";
table.insert(s, "],");
end
s = s .. '];\n var canvas = document.getElementById("renderCanvas").getContext("2d");\n\
table.insert(s, '];\n var canvas = document.getElementById("renderCanvas").getContext("2d");\n\
\n\
\n\
var size = ' .. size .. ';\n\
@ -479,9 +480,9 @@ for (var y = 0; y < size; y++) {\n\
canvas.setFillColor(l[0], l[1], l[2], 1);\n\
canvas.fillRect(x, y, 1, 1);\n\
}\n\
}</scr' .. 'ipt></body></html>';
}</script></body></html>');
return s;
return table.concat(s);
end
testOutput = arrayToCanvasCommands(raytraceScene());

View file

@ -513,8 +513,6 @@ TEST_CASE_FIXTURE(ACFixture, "dont_offer_any_suggestions_from_the_end_of_a_comme
TEST_CASE_FIXTURE(ACFixture, "dont_offer_any_suggestions_from_within_a_broken_comment")
{
ScopedFastFlag sff{"LuauCaptureBrokenCommentSpans", true};
check(R"(
--[[ @1
)");
@ -526,8 +524,6 @@ TEST_CASE_FIXTURE(ACFixture, "dont_offer_any_suggestions_from_within_a_broken_co
TEST_CASE_FIXTURE(ACFixture, "dont_offer_any_suggestions_from_within_a_broken_comment_at_the_very_end_of_the_file")
{
ScopedFastFlag sff{"LuauCaptureBrokenCommentSpans", true};
check("--[[@1");
auto ac = autocomplete('1');
@ -2625,4 +2621,55 @@ local a: A<(number, s@1>
CHECK(ac.entryMap.count("string"));
}
TEST_CASE_FIXTURE(ACFixture, "autocomplete_first_function_arg_expected_type")
{
ScopedFastFlag luauAutocompleteAvoidMutation("LuauAutocompleteAvoidMutation", true);
ScopedFastFlag luauAutocompleteFirstArg("LuauAutocompleteFirstArg", true);
check(R"(
local function foo1() return 1 end
local function foo2() return "1" end
local function bar0() return "got" .. a end
local function bar1(a: number) return "got " .. a end
local function bar2(a: number, b: string) return "got " .. a .. b end
local t = {}
function t:bar1(a: number) return "got " .. a end
local r1 = bar0(@1)
local r2 = bar1(@2)
local r3 = bar2(@3)
local r4 = t:bar1(@4)
)");
auto ac = autocomplete('1');
REQUIRE(ac.entryMap.count("foo1"));
CHECK(ac.entryMap["foo1"].typeCorrect == TypeCorrectKind::None);
REQUIRE(ac.entryMap.count("foo2"));
CHECK(ac.entryMap["foo2"].typeCorrect == TypeCorrectKind::None);
ac = autocomplete('2');
REQUIRE(ac.entryMap.count("foo1"));
CHECK(ac.entryMap["foo1"].typeCorrect == TypeCorrectKind::CorrectFunctionResult);
REQUIRE(ac.entryMap.count("foo2"));
CHECK(ac.entryMap["foo2"].typeCorrect == TypeCorrectKind::None);
ac = autocomplete('3');
REQUIRE(ac.entryMap.count("foo1"));
CHECK(ac.entryMap["foo1"].typeCorrect == TypeCorrectKind::CorrectFunctionResult);
REQUIRE(ac.entryMap.count("foo2"));
CHECK(ac.entryMap["foo2"].typeCorrect == TypeCorrectKind::None);
ac = autocomplete('4');
REQUIRE(ac.entryMap.count("foo1"));
CHECK(ac.entryMap["foo1"].typeCorrect == TypeCorrectKind::CorrectFunctionResult);
REQUIRE(ac.entryMap.count("foo2"));
CHECK(ac.entryMap["foo2"].typeCorrect == TypeCorrectKind::None);
}
TEST_SUITE_END();

View file

@ -10,9 +10,6 @@
#include <sstream>
#include <string_view>
LUAU_FASTFLAG(LuauPreloadClosures)
LUAU_FASTFLAG(LuauGenericSpecialGlobals)
using namespace Luau;
static std::string compileFunction(const char* source, uint32_t id)
@ -74,20 +71,10 @@ TEST_CASE("BasicFunction")
bcb.setDumpFlags(Luau::BytecodeBuilder::Dump_Code);
Luau::compileOrThrow(bcb, "local function foo(a, b) return b end");
if (FFlag::LuauPreloadClosures)
{
CHECK_EQ("\n" + bcb.dumpFunction(1), R"(
CHECK_EQ("\n" + bcb.dumpFunction(1), R"(
DUPCLOSURE R0 K0
RETURN R0 0
)");
}
else
{
CHECK_EQ("\n" + bcb.dumpFunction(1), R"(
NEWCLOSURE R0 P0
RETURN R0 0
)");
}
CHECK_EQ("\n" + bcb.dumpFunction(0), R"(
RETURN R1 1
@ -2859,47 +2846,35 @@ CAPTURE UPVAL U1
RETURN R0 1
)");
if (FFlag::LuauPreloadClosures)
{
// recursive capture
CHECK_EQ("\n" + compileFunction("local function foo() return foo() end", 1), R"(
// recursive capture
CHECK_EQ("\n" + compileFunction("local function foo() return foo() end", 1), R"(
DUPCLOSURE R0 K0
CAPTURE VAL R0
RETURN R0 0
)");
// multi-level recursive capture
CHECK_EQ("\n" + compileFunction("local function foo() return function() return foo() end end", 1), R"(
// multi-level recursive capture
CHECK_EQ("\n" + compileFunction("local function foo() return function() return foo() end end", 1), R"(
DUPCLOSURE R0 K0
CAPTURE UPVAL U0
RETURN R0 1
)");
// multi-level recursive capture where function isn't top-level
// note: this should probably be optimized to DUPCLOSURE but doing that requires a different upval tracking flow in the compiler
CHECK_EQ("\n" + compileFunction(R"(
// multi-level recursive capture where function isn't top-level
// note: this should probably be optimized to DUPCLOSURE but doing that requires a different upval tracking flow in the compiler
CHECK_EQ("\n" + compileFunction(R"(
local function foo()
local function bar()
return function() return bar() end
end
end
)",
1),
R"(
1),
R"(
NEWCLOSURE R0 P0
CAPTURE UPVAL U0
RETURN R0 1
)");
}
else
{
// recursive capture
CHECK_EQ("\n" + compileFunction("local function foo() return foo() end", 1), R"(
NEWCLOSURE R0 P0
CAPTURE VAL R0
RETURN R0 0
)");
}
}
TEST_CASE("OutOfLocals")
@ -3504,8 +3479,6 @@ local t = {
TEST_CASE("ConstantClosure")
{
ScopedFastFlag sff("LuauPreloadClosures", true);
// closures without upvalues are created when bytecode is loaded
CHECK_EQ("\n" + compileFunction(R"(
return function() end
@ -3570,8 +3543,6 @@ RETURN R0 1
TEST_CASE("SharedClosure")
{
ScopedFastFlag sff1("LuauPreloadClosures", true);
// closures can be shared even if functions refer to upvalues, as long as upvalues are top-level
CHECK_EQ("\n" + compileFunction(R"(
local val = ...

View file

@ -123,8 +123,8 @@ int lua_silence(lua_State* L)
using StateRef = std::unique_ptr<lua_State, void (*)(lua_State*)>;
static StateRef runConformance(
const char* name, void (*setup)(lua_State* L) = nullptr, void (*yield)(lua_State* L) = nullptr, lua_State* initialLuaState = nullptr)
static StateRef runConformance(const char* name, void (*setup)(lua_State* L) = nullptr, void (*yield)(lua_State* L) = nullptr,
lua_State* initialLuaState = nullptr, lua_CompileOptions* copts = nullptr)
{
std::string path = __FILE__;
path.erase(path.find_last_of("\\/"));
@ -180,13 +180,8 @@ static StateRef runConformance(
std::string chunkname = "=" + std::string(name);
lua_CompileOptions copts = {};
copts.optimizationLevel = 1; // default
copts.debugLevel = 2; // for debugger tests
copts.vectorCtor = "vector"; // for vector tests
size_t bytecodeSize = 0;
char* bytecode = luau_compile(source.data(), source.size(), &copts, &bytecodeSize);
char* bytecode = luau_compile(source.data(), source.size(), copts, &bytecodeSize);
int result = luau_load(L, chunkname.c_str(), bytecode, bytecodeSize, 0);
free(bytecode);
@ -373,29 +368,37 @@ TEST_CASE("Vector")
{
ScopedFastFlag sff{"LuauIfElseExpressionBaseSupport", true};
runConformance("vector.lua", [](lua_State* L) {
lua_pushcfunction(L, lua_vector, "vector");
lua_setglobal(L, "vector");
lua_CompileOptions copts = {};
copts.optimizationLevel = 1;
copts.debugLevel = 1;
copts.vectorCtor = "vector";
runConformance(
"vector.lua",
[](lua_State* L) {
lua_pushcfunction(L, lua_vector, "vector");
lua_setglobal(L, "vector");
#if LUA_VECTOR_SIZE == 4
lua_pushvector(L, 0.0f, 0.0f, 0.0f, 0.0f);
lua_pushvector(L, 0.0f, 0.0f, 0.0f, 0.0f);
#else
lua_pushvector(L, 0.0f, 0.0f, 0.0f);
lua_pushvector(L, 0.0f, 0.0f, 0.0f);
#endif
luaL_newmetatable(L, "vector");
luaL_newmetatable(L, "vector");
lua_pushstring(L, "__index");
lua_pushcfunction(L, lua_vector_index, nullptr);
lua_settable(L, -3);
lua_pushstring(L, "__index");
lua_pushcfunction(L, lua_vector_index, nullptr);
lua_settable(L, -3);
lua_pushstring(L, "__namecall");
lua_pushcfunction(L, lua_vector_namecall, nullptr);
lua_settable(L, -3);
lua_pushstring(L, "__namecall");
lua_pushcfunction(L, lua_vector_namecall, nullptr);
lua_settable(L, -3);
lua_setreadonly(L, -1, true);
lua_setmetatable(L, -2);
lua_pop(L, 1);
});
lua_setreadonly(L, -1, true);
lua_setmetatable(L, -2);
lua_pop(L, 1);
},
nullptr, nullptr, &copts);
}
static void populateRTTI(lua_State* L, Luau::TypeId type)
@ -499,6 +502,10 @@ TEST_CASE("Debugger")
breakhits = 0;
interruptedthread = nullptr;
lua_CompileOptions copts = {};
copts.optimizationLevel = 1;
copts.debugLevel = 2;
runConformance(
"debugger.lua",
[](lua_State* L) {
@ -614,7 +621,8 @@ TEST_CASE("Debugger")
lua_resume(interruptedthread, nullptr, 0);
interruptedthread = nullptr;
}
});
},
nullptr, &copts);
CHECK(breakhits == 10); // 2 hits per breakpoint
}
@ -863,4 +871,46 @@ TEST_CASE("TagMethodError")
});
}
TEST_CASE("Coverage")
{
lua_CompileOptions copts = {};
copts.optimizationLevel = 1;
copts.debugLevel = 1;
copts.coverageLevel = 2;
runConformance(
"coverage.lua",
[](lua_State* L) {
lua_pushcfunction(
L,
[](lua_State* L) -> int {
luaL_argexpected(L, lua_isLfunction(L, 1), 1, "function");
lua_newtable(L);
lua_getcoverage(L, 1, L, [](void* context, const char* function, int linedefined, int depth, const int* hits, size_t size) {
lua_State* L = static_cast<lua_State*>(context);
lua_newtable(L);
lua_pushstring(L, function);
lua_setfield(L, -2, "name");
for (size_t i = 0; i < size; ++i)
if (hits[i] != -1)
{
lua_pushinteger(L, hits[i]);
lua_rawseti(L, -2, int(i));
}
lua_rawseti(L, -2, lua_objlen(L, -2) + 1);
});
return 1;
},
"getcoverage");
lua_setglobal(L, "getcoverage");
},
nullptr, nullptr, &copts);
}
TEST_SUITE_END();

View file

@ -2303,8 +2303,6 @@ TEST_CASE_FIXTURE(Fixture, "capture_comments")
TEST_CASE_FIXTURE(Fixture, "capture_broken_comment_at_the_start_of_the_file")
{
ScopedFastFlag sff{"LuauCaptureBrokenCommentSpans", true};
ParseOptions options;
options.captureComments = true;
@ -2319,8 +2317,6 @@ TEST_CASE_FIXTURE(Fixture, "capture_broken_comment_at_the_start_of_the_file")
TEST_CASE_FIXTURE(Fixture, "capture_broken_comment")
{
ScopedFastFlag sff{"LuauCaptureBrokenCommentSpans", true};
ParseOptions options;
options.captureComments = true;

View file

@ -207,6 +207,36 @@ TEST_CASE_FIXTURE(Fixture, "as_expr_does_not_propagate_type_info")
CHECK_EQ("number", toString(requireType("b")));
}
TEST_CASE_FIXTURE(Fixture, "as_expr_is_bidirectional")
{
ScopedFastFlag sff{"LuauBidirectionalAsExpr", true};
CheckResult result = check(R"(
local a = 55 :: number?
local b = a :: number
)");
LUAU_REQUIRE_NO_ERRORS(result);
CHECK_EQ("number?", toString(requireType("a")));
CHECK_EQ("number", toString(requireType("b")));
}
TEST_CASE_FIXTURE(Fixture, "as_expr_warns_on_unrelated_cast")
{
ScopedFastFlag sff{"LuauBidirectionalAsExpr", true};
ScopedFastFlag sff2{"LuauErrorRecoveryType", true};
CheckResult result = check(R"(
local a = 55 :: string
)");
LUAU_REQUIRE_ERROR_COUNT(1, result);
CHECK_EQ("Cannot cast 'number' into 'string' because the types are unrelated", toString(result.errors[0]));
CHECK_EQ("string", toString(requireType("a")));
}
TEST_CASE_FIXTURE(Fixture, "type_annotations_inside_function_bodies")
{
CheckResult result = check(R"(

View file

@ -7,6 +7,8 @@
#include "doctest.h"
LUAU_FASTFLAG(LuauFixTonumberReturnType)
using namespace Luau;
TEST_SUITE_BEGIN("BuiltinTests");
@ -814,6 +816,30 @@ TEST_CASE_FIXTURE(Fixture, "string_format_report_all_type_errors_at_correct_posi
CHECK_EQ(TypeErrorData(TypeMismatch{stringType, booleanType}), result.errors[2].data);
}
TEST_CASE_FIXTURE(Fixture, "tonumber_returns_optional_number_type")
{
CheckResult result = check(R"(
--!strict
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]));
}
}
TEST_CASE_FIXTURE(Fixture, "tonumber_returns_optional_number_type2")
{
CheckResult result = check(R"(
--!strict
local b: number = tonumber('asdf') or 1
)");
LUAU_REQUIRE_NO_ERRORS(result);
}
TEST_CASE_FIXTURE(Fixture, "dont_add_definitions_to_persistent_types")
{
CheckResult result = check(R"(

View file

@ -9,6 +9,8 @@
using namespace Luau;
LUAU_FASTFLAG(LuauExtendedFunctionMismatchError)
TEST_SUITE_BEGIN("GenericsTests");
TEST_CASE_FIXTURE(Fixture, "check_generic_function")
@ -644,4 +646,42 @@ f(1, 2, 3)
CHECK_EQ(toString(*ty, opts), "(a: number, number, number) -> ()");
}
TEST_CASE_FIXTURE(Fixture, "error_detailed_function_mismatch_generic_types")
{
CheckResult result = check(R"(
type C = () -> ()
type D = <T>() -> ()
local c: C
local d: D = c
)");
LUAU_REQUIRE_ERROR_COUNT(1, result);
if (FFlag::LuauExtendedFunctionMismatchError)
CHECK_EQ(
toString(result.errors[0]), R"(Type '() -> ()' could not be converted into '<T>() -> ()'; different number of generic type parameters)");
else
CHECK_EQ(toString(result.errors[0]), R"(Type '() -> ()' could not be converted into '<T>() -> ()')");
}
TEST_CASE_FIXTURE(Fixture, "error_detailed_function_mismatch_generic_pack")
{
CheckResult result = check(R"(
type C = () -> ()
type D = <T...>() -> ()
local c: C
local d: D = c
)");
LUAU_REQUIRE_ERROR_COUNT(1, result);
if (FFlag::LuauExtendedFunctionMismatchError)
CHECK_EQ(toString(result.errors[0]),
R"(Type '() -> ()' could not be converted into '<T...>() -> ()'; different number of generic type pack parameters)");
else
CHECK_EQ(toString(result.errors[0]), R"(Type '() -> ()' could not be converted into '<T...>() -> ()')");
}
TEST_SUITE_END();

View file

@ -279,7 +279,7 @@ TEST_CASE_FIXTURE(Fixture, "assert_non_binary_expressions_actually_resolve_const
TEST_CASE_FIXTURE(Fixture, "assign_table_with_refined_property_with_a_similar_type_is_illegal")
{
ScopedFastFlag luauTableSubtypingVariance{"LuauTableSubtypingVariance", true};
ScopedFastFlag LuauTableSubtypingVariance2{"LuauTableSubtypingVariance2", true};
ScopedFastFlag luauExtendedTypeMismatchError{"LuauExtendedTypeMismatchError", true};
CheckResult result = check(R"(
@ -1085,4 +1085,19 @@ TEST_CASE_FIXTURE(Fixture, "type_comparison_ifelse_expression")
CHECK_EQ("any", toString(requireTypeAtPosition({6, 66})));
}
TEST_CASE_FIXTURE(Fixture, "apply_refinements_on_astexprindexexpr_whose_subscript_expr_is_constant_string")
{
ScopedFastFlag sff{"LuauRefiLookupFromIndexExpr", true};
CheckResult result = check(R"(
type T = { [string]: { prop: number }? }
local t: T = {}
if t["hello"] then
local foo = t["hello"].prop
end
)");
LUAU_REQUIRE_NO_ERRORS(result);
}
TEST_SUITE_END();

View file

@ -374,4 +374,54 @@ TEST_CASE_FIXTURE(Fixture, "table_properties_type_error_escapes")
toString(result.errors[0]));
}
TEST_CASE_FIXTURE(Fixture, "error_detailed_tagged_union_mismatch_string")
{
ScopedFastFlag sffs[] = {
{"LuauSingletonTypes", true},
{"LuauParseSingletonTypes", true},
{"LuauUnionHeuristic", true},
{"LuauExpectedTypesOfProperties", true},
{"LuauExtendedUnionMismatchError", true},
};
CheckResult result = check(R"(
type Cat = { tag: 'cat', catfood: string }
type Dog = { tag: 'dog', dogfood: string }
type Animal = Cat | Dog
local a: Animal = { tag = 'cat', cafood = 'something' }
)");
LUAU_REQUIRE_ERROR_COUNT(1, result);
CHECK_EQ(R"(Type 'a' could not be converted into 'Cat | Dog'
caused by:
None of the union options are compatible. For example: Table type 'a' not compatible with type 'Cat' because the former is missing field 'catfood')",
toString(result.errors[0]));
}
TEST_CASE_FIXTURE(Fixture, "error_detailed_tagged_union_mismatch_bool")
{
ScopedFastFlag sffs[] = {
{"LuauSingletonTypes", true},
{"LuauParseSingletonTypes", true},
{"LuauUnionHeuristic", true},
{"LuauExpectedTypesOfProperties", true},
{"LuauExtendedUnionMismatchError", true},
};
CheckResult result = check(R"(
type Good = { success: true, result: string }
type Bad = { success: false, error: string }
type Result = Good | Bad
local a: Result = { success = false, result = 'something' }
)");
LUAU_REQUIRE_ERROR_COUNT(1, result);
CHECK_EQ(R"(Type 'a' could not be converted into 'Bad | Good'
caused by:
None of the union options are compatible. For example: Table type 'a' not compatible with type 'Bad' because the former is missing field 'error')",
toString(result.errors[0]));
}
TEST_SUITE_END();

View file

@ -12,6 +12,8 @@
using namespace Luau;
LUAU_FASTFLAG(LuauExtendedFunctionMismatchError)
TEST_SUITE_BEGIN("TableTests");
TEST_CASE_FIXTURE(Fixture, "basic")
@ -275,7 +277,7 @@ TEST_CASE_FIXTURE(Fixture, "open_table_unification")
TEST_CASE_FIXTURE(Fixture, "open_table_unification_2")
{
ScopedFastFlag sff{"LuauTableSubtypingVariance", true};
ScopedFastFlag sff{"LuauTableSubtypingVariance2", true};
CheckResult result = check(R"(
local a = {}
@ -346,7 +348,7 @@ TEST_CASE_FIXTURE(Fixture, "table_param_row_polymorphism_1")
TEST_CASE_FIXTURE(Fixture, "table_param_row_polymorphism_2")
{
ScopedFastFlag sff{"LuauTableSubtypingVariance", true};
ScopedFastFlag sff{"LuauTableSubtypingVariance2", true};
CheckResult result = check(R"(
--!strict
@ -369,7 +371,7 @@ TEST_CASE_FIXTURE(Fixture, "table_param_row_polymorphism_2")
TEST_CASE_FIXTURE(Fixture, "table_param_row_polymorphism_3")
{
ScopedFastFlag sff{"LuauTableSubtypingVariance", true};
ScopedFastFlag sff{"LuauTableSubtypingVariance2", true};
CheckResult result = check(R"(
local T = {}
@ -476,7 +478,7 @@ TEST_CASE_FIXTURE(Fixture, "ok_to_add_property_to_free_table")
TEST_CASE_FIXTURE(Fixture, "okay_to_add_property_to_unsealed_tables_by_assignment")
{
ScopedFastFlag sff{"LuauTableSubtypingVariance", true};
ScopedFastFlag sff{"LuauTableSubtypingVariance2", true};
CheckResult result = check(R"(
--!strict
@ -511,7 +513,7 @@ TEST_CASE_FIXTURE(Fixture, "okay_to_add_property_to_unsealed_tables_by_function_
TEST_CASE_FIXTURE(Fixture, "width_subtyping")
{
ScopedFastFlag sff{"LuauTableSubtypingVariance", true};
ScopedFastFlag sff{"LuauTableSubtypingVariance2", true};
CheckResult result = check(R"(
--!strict
@ -771,7 +773,7 @@ TEST_CASE_FIXTURE(Fixture, "infer_indexer_for_left_unsealed_table_from_right_han
TEST_CASE_FIXTURE(Fixture, "sealed_table_value_can_infer_an_indexer")
{
ScopedFastFlag sff{"LuauTableSubtypingVariance", true};
ScopedFastFlag sff{"LuauTableSubtypingVariance2", true};
CheckResult result = check(R"(
local t: { a: string, [number]: string } = { a = "foo" }
@ -782,7 +784,7 @@ TEST_CASE_FIXTURE(Fixture, "sealed_table_value_can_infer_an_indexer")
TEST_CASE_FIXTURE(Fixture, "array_factory_function")
{
ScopedFastFlag sff{"LuauTableSubtypingVariance", true};
ScopedFastFlag sff{"LuauTableSubtypingVariance2", true};
CheckResult result = check(R"(
function empty() return {} end
@ -1465,7 +1467,7 @@ TEST_CASE_FIXTURE(Fixture, "casting_tables_with_props_into_table_with_indexer2")
TEST_CASE_FIXTURE(Fixture, "casting_tables_with_props_into_table_with_indexer3")
{
ScopedFastFlag sff{"LuauTableSubtypingVariance", true};
ScopedFastFlag sff{"LuauTableSubtypingVariance2", true};
CheckResult result = check(R"(
local function foo(a: {[string]: number, a: string}) end
@ -1550,7 +1552,7 @@ TEST_CASE_FIXTURE(Fixture, "table_subtyping_with_extra_props_dont_report_multipl
TEST_CASE_FIXTURE(Fixture, "table_subtyping_with_extra_props_is_ok")
{
ScopedFastFlag sff{"LuauTableSubtypingVariance", true};
ScopedFastFlag sff{"LuauTableSubtypingVariance2", true};
CheckResult result = check(R"(
local vec3 = {x = 1, y = 2, z = 3}
@ -1937,7 +1939,7 @@ TEST_CASE_FIXTURE(Fixture, "table_insert_should_cope_with_optional_properties_in
TEST_CASE_FIXTURE(Fixture, "table_insert_should_cope_with_optional_properties_in_strict")
{
ScopedFastFlag sff{"LuauTableSubtypingVariance", true};
ScopedFastFlag sff{"LuauTableSubtypingVariance2", true};
CheckResult result = check(R"(
--!strict
@ -1952,7 +1954,7 @@ TEST_CASE_FIXTURE(Fixture, "table_insert_should_cope_with_optional_properties_in
TEST_CASE_FIXTURE(Fixture, "error_detailed_prop")
{
ScopedFastFlag luauTableSubtypingVariance{"LuauTableSubtypingVariance", true}; // Only for new path
ScopedFastFlag LuauTableSubtypingVariance2{"LuauTableSubtypingVariance2", true}; // Only for new path
ScopedFastFlag luauExtendedTypeMismatchError{"LuauExtendedTypeMismatchError", true};
CheckResult result = check(R"(
@ -1971,7 +1973,7 @@ caused by:
TEST_CASE_FIXTURE(Fixture, "error_detailed_prop_nested")
{
ScopedFastFlag luauTableSubtypingVariance{"LuauTableSubtypingVariance", true}; // Only for new path
ScopedFastFlag LuauTableSubtypingVariance2{"LuauTableSubtypingVariance2", true}; // Only for new path
ScopedFastFlag luauExtendedTypeMismatchError{"LuauExtendedTypeMismatchError", true};
CheckResult result = check(R"(
@ -1995,7 +1997,7 @@ caused by:
TEST_CASE_FIXTURE(Fixture, "error_detailed_metatable_prop")
{
ScopedFastFlag luauTableSubtypingVariance{"LuauTableSubtypingVariance", true}; // Only for new path
ScopedFastFlag LuauTableSubtypingVariance2{"LuauTableSubtypingVariance2", true}; // Only for new path
ScopedFastFlag luauExtendedTypeMismatchError{"LuauExtendedTypeMismatchError", true};
CheckResult result = check(R"(
@ -2015,11 +2017,22 @@ caused by:
caused by:
Property 'y' is not compatible. Type 'string' could not be converted into 'number')");
CHECK_EQ(toString(result.errors[1]), R"(Type 'b2' could not be converted into 'a2'
if (FFlag::LuauExtendedFunctionMismatchError)
{
CHECK_EQ(toString(result.errors[1]), R"(Type 'b2' could not be converted into 'a2'
caused by:
Type '{| __call: (a, b) -> () |}' could not be converted into '{| __call: <a>(a) -> () |}'
caused by:
Property '__call' is not compatible. Type '(a, b) -> ()' could not be converted into '<a>(a) -> ()'; different number of generic type parameters)");
}
else
{
CHECK_EQ(toString(result.errors[1]), R"(Type 'b2' could not be converted into 'a2'
caused by:
Type '{| __call: (a, b) -> () |}' could not be converted into '{| __call: <a>(a) -> () |}'
caused by:
Property '__call' is not compatible. Type '(a, b) -> ()' could not be converted into '<a>(a) -> ()')");
}
}
TEST_CASE_FIXTURE(Fixture, "explicitly_typed_table")
@ -2027,7 +2040,7 @@ TEST_CASE_FIXTURE(Fixture, "explicitly_typed_table")
ScopedFastFlag sffs[] {
{"LuauPropertiesGetExpectedType", true},
{"LuauExpectedTypesOfProperties", true},
{"LuauTableSubtypingVariance", true},
{"LuauTableSubtypingVariance2", true},
};
CheckResult result = check(R"(
@ -2048,7 +2061,7 @@ TEST_CASE_FIXTURE(Fixture, "explicitly_typed_table_error")
ScopedFastFlag sffs[] {
{"LuauPropertiesGetExpectedType", true},
{"LuauExpectedTypesOfProperties", true},
{"LuauTableSubtypingVariance", true},
{"LuauTableSubtypingVariance2", true},
{"LuauExtendedTypeMismatchError", true},
};
@ -2076,7 +2089,7 @@ TEST_CASE_FIXTURE(Fixture, "explicitly_typed_table_with_indexer")
ScopedFastFlag sffs[] {
{"LuauPropertiesGetExpectedType", true},
{"LuauExpectedTypesOfProperties", true},
{"LuauTableSubtypingVariance", true},
{"LuauTableSubtypingVariance2", true},
};
CheckResult result = check(R"(
@ -2092,4 +2105,18 @@ a.p = { x = 9 }
LUAU_REQUIRE_NO_ERRORS(result);
}
TEST_CASE_FIXTURE(Fixture, "recursive_metatable_type_call")
{
ScopedFastFlag luauFixRecursiveMetatableCall{"LuauFixRecursiveMetatableCall", true};
CheckResult result = check(R"(
local b
b = setmetatable({}, {__call = b})
b()
)");
LUAU_REQUIRE_ERROR_COUNT(1, result);
CHECK_EQ(toString(result.errors[0]), R"(Cannot call non-function t1 where t1 = { @metatable {| __call: t1 |}, { } })");
}
TEST_SUITE_END();

View file

@ -16,6 +16,7 @@
LUAU_FASTFLAG(LuauFixLocationSpanTableIndexExpr)
LUAU_FASTFLAG(LuauEqConstraint)
LUAU_FASTFLAG(LuauExtendedFunctionMismatchError)
using namespace Luau;
@ -2084,7 +2085,7 @@ TEST_CASE_FIXTURE(Fixture, "primitive_arith_no_metatable")
{
CheckResult result = check(R"(
function add(a: number, b: string)
return a + tonumber(b), a .. b
return a + (tonumber(b) :: number), a .. b
end
local n, s = add(2,"3")
)");
@ -2485,7 +2486,7 @@ TEST_CASE_FIXTURE(Fixture, "inferring_crazy_table_should_also_be_quick")
CheckResult result = check(R"(
--!strict
function f(U)
U(w:s(an):c()():c():U(s):c():c():U(s):c():U(s):cU()):c():U(s):c():U(s):c():c():U(s):c():U(s):cU()
U(w:s(an):c()():c():U(s):c():c():U(s):c():U(s):cU()):c():U(s):c():U(s):c():c():U(s):c():U(s):cU()
end
)");
@ -3329,7 +3330,7 @@ TEST_CASE_FIXTURE(Fixture, "relation_op_on_any_lhs_where_rhs_maybe_has_metatable
{
CheckResult result = check(R"(
local x
print((x == true and (x .. "y")) .. 1)
print((x == true and (x .. "y")) .. 1)
)");
LUAU_REQUIRE_ERROR_COUNT(1, result);
@ -4473,7 +4474,18 @@ f(function(a, b, c, ...) return a + b end)
)");
LUAU_REQUIRE_ERRORS(result);
CHECK_EQ("Type '(number, number, a) -> number' could not be converted into '(number, number) -> number'", toString(result.errors[0]));
if (FFlag::LuauExtendedFunctionMismatchError)
{
CHECK_EQ(R"(Type '(number, number, a) -> number' could not be converted into '(number, number) -> number'
caused by:
Argument count mismatch. Function expects 3 arguments, but only 2 are specified)",
toString(result.errors[0]));
}
else
{
CHECK_EQ(R"(Type '(number, number, a) -> number' could not be converted into '(number, number) -> number')", toString(result.errors[0]));
}
// Infer from variadic packs into elements
result = check(R"(
@ -4604,7 +4616,17 @@ local c = sumrec(function(x, y, f) return f(x, y) end) -- type binders are not i
)");
LUAU_REQUIRE_ERRORS(result);
CHECK_EQ("Type '(a, b, (a, b) -> (c...)) -> (c...)' could not be converted into '<a>(a, a, (a, a) -> a) -> a'", toString(result.errors[0]));
if (FFlag::LuauExtendedFunctionMismatchError)
{
CHECK_EQ(
"Type '(a, b, (a, b) -> (c...)) -> (c...)' could not be converted into '<a>(a, a, (a, a) -> a) -> a'; different number of generic type "
"parameters",
toString(result.errors[0]));
}
else
{
CHECK_EQ("Type '(a, b, (a, b) -> (c...)) -> (c...)' could not be converted into '<a>(a, a, (a, a) -> a) -> a'", toString(result.errors[0]));
}
}
TEST_CASE_FIXTURE(Fixture, "infer_return_value_type")
@ -4799,4 +4821,211 @@ local ModuleA = require(game.A)
CHECK_EQ("*unknown*", toString(*oty));
}
/*
* If it wasn't instantly obvious, we have the fuzzer to thank for this gem of a test.
*
* We had an issue here where the scope for the `if` block here would
* have an elevated TypeLevel even though there is no function nesting going on.
* This would result in a free typevar for the type of _ that was much higher than
* it should be. This type would be erroneously quantified in the definition of `aaa`.
* This in turn caused an ice when evaluating `_()` in the while loop.
*/
TEST_CASE_FIXTURE(Fixture, "free_typevars_introduced_within_control_flow_constructs_do_not_get_an_elevated_TypeLevel")
{
check(R"(
--!strict
if _ then
_[_], _ = nil
_()
end
local aaa = function():typeof(_) return 1 end
if aaa then
while _() do
end
end
)");
// No ice()? No problem.
}
/*
* This is a bit elaborate. Bear with me.
*
* The type of _ becomes free with the first statement. With the second, we unify it with a function.
*
* At this point, it is important that the newly created fresh types of this new function type are promoted
* to the same level as the original free type. If we do not, they are incorrectly ascribed the level of the
* containing function.
*
* If this is allowed to happen, the final lambda erroneously quantifies the type of _ to something ridiculous
* just before we typecheck the invocation to _.
*/
TEST_CASE_FIXTURE(Fixture, "fuzzer_found_this")
{
check(R"(
l0, _ = nil
local function p()
_()
end
a = _(
function():(typeof(p),typeof(_))
end
)[nil]
)");
}
/*
* We had an issue where part of the type of pairs() was an unsealed table.
* This test depends on FFlagDebugLuauFreezeArena to trigger it.
*/
TEST_CASE_FIXTURE(Fixture, "pairs_parameters_are_not_unsealed_tables")
{
check(R"(
function _(l0:{n0:any})
_ = pairs
end
)");
}
TEST_CASE_FIXTURE(Fixture, "inferred_methods_of_free_tables_have_the_same_level_as_the_enclosing_table")
{
check(R"(
function Base64FileReader(data)
local reader = {}
local index: number
function reader:PeekByte()
return data:byte(index)
end
function reader:Byte()
return data:byte(index - 1)
end
return reader
end
Base64FileReader()
function ReadMidiEvents(data)
local reader = Base64FileReader(data)
while reader:HasMore() do
(reader:Byte() % 128)
end
end
)");
}
TEST_CASE_FIXTURE(Fixture, "error_detailed_function_mismatch_arg_count")
{
ScopedFastFlag luauExtendedFunctionMismatchError{"LuauExtendedFunctionMismatchError", true};
CheckResult result = check(R"(
type A = (number, number) -> string
type B = (number) -> string
local a: A
local b: B = a
)");
LUAU_REQUIRE_ERROR_COUNT(1, result);
CHECK_EQ(toString(result.errors[0]), R"(Type '(number, number) -> string' could not be converted into '(number) -> string'
caused by:
Argument count mismatch. Function expects 2 arguments, but only 1 is specified)");
}
TEST_CASE_FIXTURE(Fixture, "error_detailed_function_mismatch_arg")
{
ScopedFastFlag luauExtendedFunctionMismatchError{"LuauExtendedFunctionMismatchError", true};
CheckResult result = check(R"(
type A = (number, number) -> string
type B = (number, string) -> string
local a: A
local b: B = a
)");
LUAU_REQUIRE_ERROR_COUNT(1, result);
CHECK_EQ(toString(result.errors[0]), R"(Type '(number, number) -> string' could not be converted into '(number, string) -> string'
caused by:
Argument #2 type is not compatible. Type 'string' could not be converted into 'number')");
}
TEST_CASE_FIXTURE(Fixture, "error_detailed_function_mismatch_ret_count")
{
ScopedFastFlag luauExtendedFunctionMismatchError{"LuauExtendedFunctionMismatchError", true};
CheckResult result = check(R"(
type A = (number, number) -> (number)
type B = (number, number) -> (number, boolean)
local a: A
local b: B = a
)");
LUAU_REQUIRE_ERROR_COUNT(1, result);
CHECK_EQ(toString(result.errors[0]), R"(Type '(number, number) -> number' could not be converted into '(number, number) -> (number, boolean)'
caused by:
Function only returns 1 value. 2 are required here)");
}
TEST_CASE_FIXTURE(Fixture, "error_detailed_function_mismatch_ret")
{
ScopedFastFlag luauExtendedFunctionMismatchError{"LuauExtendedFunctionMismatchError", true};
CheckResult result = check(R"(
type A = (number, number) -> string
type B = (number, number) -> number
local a: A
local b: B = a
)");
LUAU_REQUIRE_ERROR_COUNT(1, result);
CHECK_EQ(toString(result.errors[0]), R"(Type '(number, number) -> string' could not be converted into '(number, number) -> number'
caused by:
Return type is not compatible. Type 'string' could not be converted into 'number')");
}
TEST_CASE_FIXTURE(Fixture, "error_detailed_function_mismatch_ret_mult")
{
ScopedFastFlag luauExtendedFunctionMismatchError{"LuauExtendedFunctionMismatchError", true};
CheckResult result = check(R"(
type A = (number, number) -> (number, string)
type B = (number, number) -> (number, boolean)
local a: A
local b: B = a
)");
LUAU_REQUIRE_ERROR_COUNT(1, result);
CHECK_EQ(
toString(result.errors[0]), R"(Type '(number, number) -> (number, string)' could not be converted into '(number, number) -> (number, boolean)'
caused by:
Return #2 type is not compatible. Type 'string' could not be converted into 'boolean')");
}
TEST_CASE_FIXTURE(Fixture, "table_function_check_use_after_free")
{
ScopedFastFlag luauUnifyFunctionCheckResult{"LuauUpdateFunctionNameBinding", true};
CheckResult result = check(R"(
local t = {}
function t.x(value)
for k,v in pairs(t) do end
end
)");
LUAU_REQUIRE_NO_ERRORS(result);
}
TEST_SUITE_END();

View file

@ -214,4 +214,32 @@ TEST_CASE_FIXTURE(TryUnifyFixture, "cli_41095_concat_log_in_sealed_table_unifica
CHECK_EQ(toString(result.errors[1]), "Available overloads: ({a}, a) -> (); and ({a}, number, a) -> ()");
}
TEST_CASE_FIXTURE(TryUnifyFixture, "undo_new_prop_on_unsealed_table")
{
ScopedFastFlag flags[] = {
{"LuauTableSubtypingVariance2", true},
};
// I am not sure how to make this happen in Luau code.
TypeId unsealedTable = arena.addType(TableTypeVar{TableState::Unsealed, TypeLevel{}});
TypeId sealedTable = arena.addType(TableTypeVar{
{{"prop", Property{getSingletonTypes().numberType}}},
std::nullopt,
TypeLevel{},
TableState::Sealed
});
const TableTypeVar* ttv = get<TableTypeVar>(unsealedTable);
REQUIRE(ttv);
state.tryUnify(unsealedTable, sealedTable);
// To be honest, it's really quite spooky here that we're amending an unsealed table in this case.
CHECK(!ttv->props.empty());
state.log.rollback();
CHECK(ttv->props.empty());
}
TEST_SUITE_END();

View file

@ -462,4 +462,20 @@ local a: XYZ = { w = 4 }
CHECK_EQ(toString(result.errors[0]), R"(Type 'a' could not be converted into 'X | Y | Z'; none of the union options are compatible)");
}
TEST_CASE_FIXTURE(Fixture, "error_detailed_optional")
{
ScopedFastFlag luauExtendedUnionMismatchError{"LuauExtendedUnionMismatchError", true};
CheckResult result = check(R"(
type X = { x: number }
local a: X? = { w = 4 }
)");
LUAU_REQUIRE_ERROR_COUNT(1, result);
CHECK_EQ(toString(result.errors[0]), R"(Type 'a' could not be converted into 'X?'
caused by:
None of the union options are compatible. For example: Table type 'a' not compatible with type 'X' because the former is missing field 'x')");
}
TEST_SUITE_END();

View file

@ -0,0 +1,64 @@
-- This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details
print("testing coverage")
function foo()
local x = 1
local y = 2
assert(x + y)
end
function bar()
local function one(x)
return x
end
local two = function(x)
return x
end
one(1)
end
function validate(stats, hits, misses)
local checked = {}
for _,l in ipairs(hits) do
if not (stats[l] and stats[l] > 0) then
return false, string.format("expected line %d to be hit", l)
end
checked[l] = true
end
for _,l in ipairs(misses) do
if not (stats[l] and stats[l] == 0) then
return false, string.format("expected line %d to be missed", l)
end
checked[l] = true
end
for k,v in pairs(stats) do
if type(k) == "number" and not checked[k] then
return false, string.format("expected line %d to be absent", k)
end
end
return true
end
foo()
c = getcoverage(foo)
assert(#c == 1)
assert(c[1].name == "foo")
assert(validate(c[1], {5, 6, 7}, {}))
bar()
c = getcoverage(bar)
assert(#c == 3)
assert(c[1].name == "bar")
assert(validate(c[1], {11, 15, 19}, {}))
assert(c[2].name == "one")
assert(validate(c[2], {12}, {}))
assert(c[3].name == nil)
assert(validate(c[3], {}, {16}))
return 'OK'