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; 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, using TypeErrorData = Variant<TypeMismatch, UnknownSymbol, UnknownProperty, NotATable, CannotExtendTable, OnlyTablesCanHaveMethods,
DuplicateTypeDefinition, CountMismatch, FunctionDoesNotTakeSelf, FunctionRequiresSelf, OccursCheckFailed, UnknownRequire, DuplicateTypeDefinition, CountMismatch, FunctionDoesNotTakeSelf, FunctionRequiresSelf, OccursCheckFailed, UnknownRequire,
IncorrectGenericParameterCount, SyntaxError, CodeTooComplex, UnificationTooComplex, UnknownPropButFoundLikeProp, GenericError, IncorrectGenericParameterCount, SyntaxError, CodeTooComplex, UnificationTooComplex, UnknownPropButFoundLikeProp, GenericError,
CannotCallNonFunction, ExtraInformation, DeprecatedApiUsed, ModuleHasCyclicDependency, IllegalRequire, FunctionExitsWithoutReturning, CannotCallNonFunction, ExtraInformation, DeprecatedApiUsed, ModuleHasCyclicDependency, IllegalRequire, FunctionExitsWithoutReturning,
DuplicateGenericParameter, CannotInferBinaryOperation, MissingProperties, SwappedGenericTypeParameter, OptionalValueAccess, MissingUnionProperty>; DuplicateGenericParameter, CannotInferBinaryOperation, MissingProperties, SwappedGenericTypeParameter, OptionalValueAccess, MissingUnionProperty,
TypesAreUnrelated>;
struct TypeError 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 ModuleHasCyclicDependency& error);
std::ostream& operator<<(std::ostream& lhs, const DuplicateGenericParameter& 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 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 TableState& tv);
std::ostream& operator<<(std::ostream& lhs, const TypeVar& 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 // 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 // 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); std::string dump(TypeId ty);
void dump(TypePackId ty); std::string dump(TypePackId ty);
std::string generateName(size_t n); 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). // Returns both the type of the lvalue and its binding (if the caller wants to mutate the binding).
// Note: the binding may be null. // 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 AstExpr& expr);
std::pair<TypeId, TypeId*> checkLValueBinding(const ScopePtr& scope, const AstExprLocal& 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 AstExprGlobal& expr);
std::pair<TypeId, TypeId*> checkLValueBinding(const ScopePtr& scope, const AstExprIndexName& expr); std::pair<TypeId, TypeId*> checkLValueBinding(const ScopePtr& scope, const AstExprIndexName& expr);
std::pair<TypeId, TypeId*> checkLValueBinding(const ScopePtr& scope, const AstExprIndexExpr& 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::pair<TypeId, ScopePtr> checkFunctionSignature(const ScopePtr& scope, int subLevel, const AstExprFunction& expr,
std::optional<Location> originalNameLoc, std::optional<TypeId> expectedType); std::optional<Location> originalNameLoc, std::optional<TypeId> expectedType);
void checkFunctionBody(const ScopePtr& scope, TypeId type, const AstExprFunction& function); 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); 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::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, 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); 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, bool handleSelfCallMismatch(const ScopePtr& scope, const AstExprCall& expr, TypePack* args, const std::vector<Location>& argLocations,
const std::vector<OverloadErrorEntry>& errors); const std::vector<OverloadErrorEntry>& errors);
@ -277,7 +278,7 @@ public:
[[noreturn]] void ice(const std::string& message); [[noreturn]] void ice(const std::string& message);
ScopePtr childFunctionScope(const ScopePtr& parent, const Location& location, int subLevel = 0); 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. // Wrapper for merge(l, r, toUnion) but without the lambda junk.
void merge(RefinementMap& l, const RefinementMap& r); void merge(RefinementMap& l, const RefinementMap& r);

View file

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

View file

@ -24,7 +24,7 @@ struct TypeLevel
int level = 0; int level = 0;
int subLevel = 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 bool subsumes(const TypeLevel& rhs) const
{ {
if (level < rhs.level) if (level < rhs.level)
@ -38,6 +38,15 @@ struct TypeLevel
return false; 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 incr() const
{ {
TypeLevel result; 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, const Location& location);
[[noreturn]] void ice(const std::string& message); [[noreturn]] void ice(const std::string& message);
// Available after regular type pack unification errors
std::optional<int> firstPackErrorPos;
}; };
} // namespace Luau } // namespace Luau

View file

@ -15,6 +15,7 @@
LUAU_FASTFLAG(LuauIfElseExpressionAnalysisSupport) LUAU_FASTFLAG(LuauIfElseExpressionAnalysisSupport)
LUAU_FASTFLAGVARIABLE(LuauAutocompleteAvoidMutation, false); LUAU_FASTFLAGVARIABLE(LuauAutocompleteAvoidMutation, false);
LUAU_FASTFLAGVARIABLE(LuauAutocompletePreferToCallFunctions, false); LUAU_FASTFLAGVARIABLE(LuauAutocompletePreferToCallFunctions, false);
LUAU_FASTFLAGVARIABLE(LuauAutocompleteFirstArg, false);
static const std::unordered_set<std::string> kStatementStartingKeywords = { static const std::unordered_set<std::string> kStatementStartingKeywords = {
"while", "if", "local", "repeat", "function", "do", "for", "return", "break", "continue", "type", "export"}; "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; 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); ty = follow(ty);
@ -220,15 +262,29 @@ static TypeCorrectKind checkTypeCorrectKind(const Module& module, TypeArena* typ
} }
}; };
auto expr = node->asExpr(); TypeId expectedType;
if (!expr)
return TypeCorrectKind::None;
auto it = module.astExpectedTypes.find(expr); if (FFlag::LuauAutocompleteFirstArg)
if (!it) {
return TypeCorrectKind::None; 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) if (FFlag::LuauAutocompletePreferToCallFunctions)
{ {
@ -333,8 +389,8 @@ static void autocompleteProps(const Module& module, TypeArena* typeArena, TypeId
if (result.count(name) == 0 && name != Parser::errorName) if (result.count(name) == 0 && name != Parser::errorName)
{ {
Luau::TypeId type = Luau::follow(prop.type); Luau::TypeId type = Luau::follow(prop.type);
TypeCorrectKind typeCorrect = TypeCorrectKind typeCorrect = indexType == PropIndexType::Key ? TypeCorrectKind::Correct
indexType == PropIndexType::Key ? TypeCorrectKind::Correct : checkTypeCorrectKind(module, typeArena, nodes.back(), type); : checkTypeCorrectKind(module, typeArena, nodes.back(), {{}, {}}, type);
ParenthesesRecommendation parens = ParenthesesRecommendation parens =
indexType == PropIndexType::Key ? ParenthesesRecommendation::None : getParenRecommendation(type, nodes, typeCorrect); indexType == PropIndexType::Key ? ParenthesesRecommendation::None : getParenRecommendation(type, nodes, typeCorrect);
@ -692,17 +748,31 @@ std::optional<const T*> returnFirstNonnullOptionOfType(const UnionTypeVar* utv)
return ret; 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(); TypeId expectedType;
if (!expr)
return std::nullopt;
auto it = module.astExpectedTypes.find(expr); if (FFlag::LuauAutocompleteFirstArg)
if (!it) {
return std::nullopt; 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)) if (get<FunctionTypeVar>(expectedType))
return true; return true;
@ -1171,7 +1241,7 @@ static void autocompleteExpression(const SourceModule& sourceModule, const Modul
std::string n = toString(name); std::string n = toString(name);
if (!result.count(n)) 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, result[n] = {AutocompleteEntryKind::Binding, binding.typeId, binding.deprecated, false, typeCorrect, std::nullopt, std::nullopt,
binding.documentationSymbol, {}, getParenRecommendation(binding.typeId, ancestry, typeCorrect)}; binding.documentationSymbol, {}, getParenRecommendation(binding.typeId, ancestry, typeCorrect)};
@ -1181,9 +1251,10 @@ static void autocompleteExpression(const SourceModule& sourceModule, const Modul
scope = scope->parent; scope = scope->parent;
} }
TypeCorrectKind correctForNil = checkTypeCorrectKind(module, typeArena, node, typeChecker.nilType); TypeCorrectKind correctForNil = checkTypeCorrectKind(module, typeArena, node, position, typeChecker.nilType);
TypeCorrectKind correctForBoolean = checkTypeCorrectKind(module, typeArena, node, typeChecker.booleanType); TypeCorrectKind correctForBoolean = checkTypeCorrectKind(module, typeArena, node, position, typeChecker.booleanType);
TypeCorrectKind correctForFunction = functionIsExpectedAt(module, node).value_or(false) ? TypeCorrectKind::Correct : TypeCorrectKind::None; TypeCorrectKind correctForFunction =
functionIsExpectedAt(module, node, position).value_or(false) ? TypeCorrectKind::Correct : TypeCorrectKind::None;
if (FFlag::LuauIfElseExpressionAnalysisSupport) if (FFlag::LuauIfElseExpressionAnalysisSupport)
result["if"] = {AutocompleteEntryKind::Keyword, std::nullopt, false, false}; 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 genericK = arena.addType(GenericTypeVar{"K"});
TypeId genericV = arena.addType(GenericTypeVar{"V"}); 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); LUAU_ASSERT(stringMetatableTy);
const TableTypeVar* stringMetatableTable = get<TableTypeVar>(follow(*stringMetatableTy)); const TableTypeVar* stringMetatableTable = get<TableTypeVar>(follow(*stringMetatableTy));
LUAU_ASSERT(stringMetatableTable); LUAU_ASSERT(stringMetatableTable);
@ -271,7 +271,10 @@ void registerBuiltinTypes(TypeChecker& typeChecker)
persist(pair.second.typeId); persist(pair.second.typeId);
if (TableTypeVar* ttv = getMutable<TableTypeVar>(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); 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 // This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details
#include "Luau/BuiltinDefinitions.h" #include "Luau/BuiltinDefinitions.h"
LUAU_FASTFLAGVARIABLE(LuauFixTonumberReturnType, false)
namespace Luau namespace Luau
{ {
@ -113,7 +115,6 @@ declare function gcinfo(): number
declare function error<T>(message: T, level: number?) declare function error<T>(message: T, level: number?)
declare function tostring<T>(value: T): string declare function tostring<T>(value: T): string
declare function tonumber<T>(value: T, radix: number?): number
declare function rawequal<T1, T2>(a: T1, b: T2): boolean declare function rawequal<T1, T2>(a: T1, b: T2): boolean
declare function rawget<K, V>(tab: {[K]: V}, k: K): V declare function rawget<K, V>(tab: {[K]: V}, k: K): V
@ -204,7 +205,14 @@ declare function gcinfo(): number
std::string getBuiltinDefinitionSource() 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 } // namespace Luau

View file

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

View file

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

View file

@ -13,7 +13,6 @@
LUAU_FASTFLAGVARIABLE(DebugLuauFreezeArena, false) LUAU_FASTFLAGVARIABLE(DebugLuauFreezeArena, false)
LUAU_FASTFLAGVARIABLE(DebugLuauTrackOwningArena, false) LUAU_FASTFLAGVARIABLE(DebugLuauTrackOwningArena, false)
LUAU_FASTFLAG(LuauCaptureBrokenCommentSpans)
LUAU_FASTINTVARIABLE(LuauTypeCloneRecursionLimit, 0) LUAU_FASTINTVARIABLE(LuauTypeCloneRecursionLimit, 0)
namespace Luau namespace Luau
@ -23,7 +22,7 @@ static bool contains(Position pos, Comment comment)
{ {
if (comment.location.contains(pos)) if (comment.location.contains(pos))
return true; 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 comment.location.begin <= pos) // Broken comments are broken specifically because they don't have an end
return true; return true;
else if (comment.type == Lexeme::Comment && comment.location.end == pos) else if (comment.type == Lexeme::Comment && comment.location.end == pos)
@ -194,7 +193,7 @@ struct TypePackCloner
{ {
cloneState.encounteredFreeType = true; cloneState.encounteredFreeType = true;
TypePackId err = singletonTypes.errorRecoveryTypePack(singletonTypes.anyTypePack); TypePackId err = getSingletonTypes().errorRecoveryTypePack(getSingletonTypes().anyTypePack);
TypePackId cloned = dest.addTypePack(*err); TypePackId cloned = dest.addTypePack(*err);
seenTypePacks[typePackId] = cloned; seenTypePacks[typePackId] = cloned;
} }
@ -247,7 +246,7 @@ void TypeCloner::defaultClone(const T& t)
void TypeCloner::operator()(const Unifiable::Free& t) void TypeCloner::operator()(const Unifiable::Free& t)
{ {
cloneState.encounteredFreeType = true; cloneState.encounteredFreeType = true;
TypeId err = singletonTypes.errorRecoveryType(singletonTypes.anyType); TypeId err = getSingletonTypes().errorRecoveryType(getSingletonTypes().anyType);
TypeId cloned = dest.addType(*err); TypeId cloned = dest.addType(*err);
seenTypes[typeId] = cloned; 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. Luau::visit(cloner, tp->ty); // Mutates the storage that 'res' points into.
} }
if (FFlag::DebugLuauTrackOwningArena)
asMutable(res)->owningArena = &dest;
return res; return res;
} }
@ -440,12 +436,11 @@ TypeId clone(TypeId typeId, TypeArena& dest, SeenTypes& seenTypes, SeenTypePacks
{ {
TypeCloner cloner{dest, typeId, seenTypes, seenTypePacks, cloneState}; TypeCloner cloner{dest, typeId, seenTypes, seenTypePacks, cloneState};
Luau::visit(cloner, typeId->ty); // Mutates the storage that 'res' points into. Luau::visit(cloner, typeId->ty); // Mutates the storage that 'res' points into.
// TODO: Make this work when the arena of 'res' might be frozen
asMutable(res)->documentationSymbol = typeId->documentationSymbol; asMutable(res)->documentationSymbol = typeId->documentationSymbol;
} }
if (FFlag::DebugLuauTrackOwningArena)
asMutable(res)->owningArena = &dest;
return res; return res;
} }
@ -508,8 +503,8 @@ bool Module::clonePublicInterface()
if (moduleScope->varargPack) if (moduleScope->varargPack)
moduleScope->varargPack = clone(*moduleScope->varargPack, interfaceTypes, seenTypes, seenTypePacks, cloneState); moduleScope->varargPack = clone(*moduleScope->varargPack, interfaceTypes, seenTypes, seenTypePacks, cloneState);
for (auto& pair : moduleScope->exportedTypeBindings) for (auto& [name, tf] : moduleScope->exportedTypeBindings)
pair.second = clone(pair.second, interfaceTypes, seenTypes, seenTypePacks, cloneState); tf = clone(tf, interfaceTypes, seenTypes, seenTypePacks, cloneState);
for (TypeId ty : moduleScope->returnType) for (TypeId ty : moduleScope->returnType)
if (get<GenericTypeVar>(follow(ty))) 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>()) else if (auto indexexpr = expr->as<AstExprIndexExpr>())
{ {
if (auto lvalue = tryGetLValue(*indexexpr->expr)) 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)}; 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_FASTFLAG(LuauOccursCheckOkWithRecursiveFunctions)
LUAU_FASTFLAGVARIABLE(LuauFunctionArgumentNameSize, false) 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 namespace Luau
{ {
@ -290,7 +297,15 @@ struct TypeVarStringifier
void operator()(TypeId ty, const Unifiable::Free& ftv) void operator()(TypeId ty, const Unifiable::Free& ftv)
{ {
state.result.invalid = true; state.result.invalid = true;
if (FFlag::DebugLuauVerboseTypeNames)
state.emit("free-");
state.emit(state.getName(ty)); state.emit(state.getName(ty));
if (FFlag::DebugLuauVerboseTypeNames)
{
state.emit("-");
state.emit(std::to_string(ftv.level.level));
}
} }
void operator()(TypeId, const BoundTypeVar& btv) void operator()(TypeId, const BoundTypeVar& btv)
@ -802,6 +817,8 @@ struct TypePackStringifier
void operator()(TypePackId tp, const GenericTypePack& pack) void operator()(TypePackId tp, const GenericTypePack& pack)
{ {
if (FFlag::DebugLuauVerboseTypeNames)
state.emit("gen-");
if (pack.explicitName) if (pack.explicitName)
{ {
state.result.nameMap.typePacks[tp] = pack.name; state.result.nameMap.typePacks[tp] = pack.name;
@ -817,7 +834,16 @@ struct TypePackStringifier
void operator()(TypePackId tp, const FreeTypePack& pack) void operator()(TypePackId tp, const FreeTypePack& pack)
{ {
state.result.invalid = true; state.result.invalid = true;
if (FFlag::DebugLuauVerboseTypeNames)
state.emit("free-");
state.emit(state.getName(tp)); state.emit(state.getName(tp));
if (FFlag::DebugLuauVerboseTypeNames)
{
state.emit("-");
state.emit(std::to_string(pack.level.level));
}
state.emit("..."); state.emit("...");
} }
@ -1181,20 +1207,24 @@ std::string toStringNamedFunction(const std::string& prefix, const FunctionTypeV
return s; return s;
} }
void dump(TypeId ty) std::string dump(TypeId ty)
{ {
ToStringOptions opts; ToStringOptions opts;
opts.exhaustive = true; opts.exhaustive = true;
opts.functionTypeArguments = 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; ToStringOptions opts;
opts.exhaustive = true; opts.exhaustive = true;
opts.functionTypeArguments = 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) std::string generateName(size_t i)

View file

@ -9,9 +9,9 @@
#include "Luau/Scope.h" #include "Luau/Scope.h"
#include "Luau/Substitution.h" #include "Luau/Substitution.h"
#include "Luau/TopoSortStatements.h" #include "Luau/TopoSortStatements.h"
#include "Luau/ToString.h"
#include "Luau/TypePack.h" #include "Luau/TypePack.h"
#include "Luau/TypeUtils.h" #include "Luau/TypeUtils.h"
#include "Luau/ToString.h"
#include "Luau/TypeVar.h" #include "Luau/TypeVar.h"
#include "Luau/TimeTrace.h" #include "Luau/TimeTrace.h"
@ -29,7 +29,6 @@ LUAU_FASTFLAGVARIABLE(LuauCloneCorrectlyBeforeMutatingTableType, false)
LUAU_FASTFLAGVARIABLE(LuauStoreMatchingOverloadFnType, false) LUAU_FASTFLAGVARIABLE(LuauStoreMatchingOverloadFnType, false)
LUAU_FASTFLAGVARIABLE(LuauRecursiveTypeParameterRestriction, false) LUAU_FASTFLAGVARIABLE(LuauRecursiveTypeParameterRestriction, false)
LUAU_FASTFLAGVARIABLE(LuauIfElseExpressionAnalysisSupport, false) LUAU_FASTFLAGVARIABLE(LuauIfElseExpressionAnalysisSupport, false)
LUAU_FASTFLAGVARIABLE(LuauStrictRequire, false)
LUAU_FASTFLAGVARIABLE(LuauQuantifyInPlace2, false) LUAU_FASTFLAGVARIABLE(LuauQuantifyInPlace2, false)
LUAU_FASTFLAGVARIABLE(LuauSingletonTypes, false) LUAU_FASTFLAGVARIABLE(LuauSingletonTypes, false)
LUAU_FASTFLAGVARIABLE(LuauExpectedTypesOfProperties, false) LUAU_FASTFLAGVARIABLE(LuauExpectedTypesOfProperties, false)
@ -37,6 +36,12 @@ LUAU_FASTFLAGVARIABLE(LuauErrorRecoveryType, false)
LUAU_FASTFLAGVARIABLE(LuauPropertiesGetExpectedType, false) LUAU_FASTFLAGVARIABLE(LuauPropertiesGetExpectedType, false)
LUAU_FASTFLAGVARIABLE(LuauTailArgumentTypeInfo, false) LUAU_FASTFLAGVARIABLE(LuauTailArgumentTypeInfo, false)
LUAU_FASTFLAGVARIABLE(LuauModuleRequireErrorPack, 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 namespace Luau
{ {
@ -206,14 +211,14 @@ TypeChecker::TypeChecker(ModuleResolver* resolver, InternalErrorReporter* iceHan
: resolver(resolver) : resolver(resolver)
, iceHandler(iceHandler) , iceHandler(iceHandler)
, unifierState(iceHandler) , unifierState(iceHandler)
, nilType(singletonTypes.nilType) , nilType(getSingletonTypes().nilType)
, numberType(singletonTypes.numberType) , numberType(getSingletonTypes().numberType)
, stringType(singletonTypes.stringType) , stringType(getSingletonTypes().stringType)
, booleanType(singletonTypes.booleanType) , booleanType(getSingletonTypes().booleanType)
, threadType(singletonTypes.threadType) , threadType(getSingletonTypes().threadType)
, anyType(singletonTypes.anyType) , anyType(getSingletonTypes().anyType)
, optionalNumberType(singletonTypes.optionalNumberType) , optionalNumberType(getSingletonTypes().optionalNumberType)
, anyTypePack(singletonTypes.anyTypePack) , anyTypePack(getSingletonTypes().anyTypePack)
{ {
globalScope = std::make_shared<Scope>(globalTypes.addTypePack(TypePackVar{FreeTypePack{TypeLevel{}}})); 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; functionDecls[*protoIter] = pair;
++subLevel; ++subLevel;
TypeId leftType = checkFunctionName(scope, *fun->name); TypeId leftType = checkFunctionName(scope, *fun->name, funScope->level);
unify(leftType, funTy, fun->location); unify(leftType, funTy, fun->location);
} }
else if (auto fun = (*protoIter)->as<AstStatLocalFunction>()) 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()) else if (auto tail = valueIter.tail())
{ {
if (get<Unifiable::Error>(*tail)) TypePackId tailPack = follow(*tail);
if (get<Unifiable::Error>(tailPack))
right = errorRecoveryType(scope); right = errorRecoveryType(scope);
else if (auto vtp = get<VariadicTypePack>(*tail)) else if (auto vtp = get<VariadicTypePack>(tailPack))
right = vtp->ty; right = vtp->ty;
else if (get<Unifiable::Free>(*tail)) else if (get<Unifiable::Free>(tailPack))
{ {
*asMutable(*tail) = TypePack{{left}}; *asMutable(tailPack) = TypePack{{left}};
growingPack = getMutable<TypePack>(*tail); growingPack = getMutable<TypePack>(tailPack);
} }
} }
@ -1107,8 +1113,27 @@ void TypeChecker::check(const ScopePtr& scope, TypeId ty, const ScopePtr& funSco
unify(leftType, ty, function.location); unify(leftType, ty, function.location);
if (leftTypeBinding) if (FFlag::LuauUpdateFunctionNameBinding)
*leftTypeBinding = follow(quantify(funScope, leftType, function.name->location)); {
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 else
{ {
ScopePtr aliasScope = ScopePtr aliasScope = childScope(scope, typealias.location);
FFlag::LuauQuantifyInPlace2 ? childScope(scope, typealias.location, subLevel) : 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); 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"); ice("Not predeclared");
ScopePtr aliasScope = childScope(scope, typealias.location); ScopePtr aliasScope = childScope(scope, typealias.location);
aliasScope->level = scope->level.incr();
for (TypeId ty : binding->typeParams) 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)) else if (auto vtp = get<VariadicTypePack>(retPack))
return {vtp->ty, std::move(result.predicates)}; return {vtp->ty, std::move(result.predicates)};
else if (get<Unifiable::Generic>(retPack)) else if (get<Unifiable::Generic>(retPack))
ice("Unexpected abstract type pack!"); ice("Unexpected abstract type pack!", expr.location);
else else
ice("Unknown TypePack type!"); ice("Unknown TypePack type!", expr.location);
} }
ExprResult<TypeId> TypeChecker::checkExpr(const ScopePtr& scope, const AstExprIndexName& expr) 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) else if (tableType->state == TableState::Free)
{ {
TypeId result = freshType(scope); TypeId result = FFlag::LuauAscribeCorrectLevelToInferredProperitesOfFreeTables ? freshType(tableType->level) : freshType(scope);
tableType->props[name] = {result}; tableType->props[name] = {result};
return 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) 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) 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); TypeId annotationType = resolveType(scope, *expr.annotation);
ExprResult<TypeId> result = checkExpr(scope, *expr.expr, annotationType); ExprResult<TypeId> result = checkExpr(scope, *expr.expr, annotationType);
ErrorVec errorVec = canUnify(result.type, annotationType, expr.location); if (FFlag::LuauBidirectionalAsExpr)
reportErrors(errorVec); {
if (!errorVec.empty()) // Note: As an optimization, we try 'number <: number | string' first, as that is the more likely case.
annotationType = errorRecoveryType(annotationType); 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) 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?" // Answers the question: "Can I define another function with this name?"
// Primarily about detecting duplicates. // 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>()) if (auto globalName = funName.as<AstExprGlobal>())
{ {
const ScopePtr& globalScope = currentModule->getModuleScope(); const ScopePtr& globalScope = currentModule->getModuleScope();
@ -2689,7 +2748,7 @@ TypeId TypeChecker::checkFunctionName(const ScopePtr& scope, AstExpr& funName)
} }
else else
{ {
TypeId ty = freshType(scope); TypeId ty = freshTy();
globalScope->bindings[name] = {ty, funName.location}; globalScope->bindings[name] = {ty, funName.location};
return ty; return ty;
} }
@ -2699,7 +2758,7 @@ TypeId TypeChecker::checkFunctionName(const ScopePtr& scope, AstExpr& funName)
Symbol name = localName->local; Symbol name = localName->local;
Binding& binding = scope->bindings[name]; Binding& binding = scope->bindings[name];
if (binding.typeId == nullptr) if (binding.typeId == nullptr)
binding = {freshType(scope), funName.location}; binding = {freshTy(), funName.location};
return binding.typeId; return binding.typeId;
} }
@ -2730,7 +2789,7 @@ TypeId TypeChecker::checkFunctionName(const ScopePtr& scope, AstExpr& funName)
Property& property = ttv->props[name]; Property& property = ttv->props[name];
property.type = freshType(scope); property.type = freshTy();
property.location = indexName->indexLocation; property.location = indexName->indexLocation;
ttv->methodDefinitionLocations[name] = funName.location; ttv->methodDefinitionLocations[name] = funName.location;
return property.type; return property.type;
@ -3327,7 +3386,7 @@ ExprResult<TypePackId> TypeChecker::checkExprPack(const ScopePtr& scope, const A
fn = follow(fn); fn = follow(fn);
if (auto ret = checkCallOverload( 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; 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, 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) std::vector<TypeId>& overloadsThatMatchArgCount, std::vector<TypeId>& overloadsThatDont, std::vector<OverloadErrorEntry>& errors)
{ {
LUAU_ASSERT(argLocations);
fn = stripFromNilAndReport(fn, expr.func->location); fn = stripFromNilAndReport(fn, expr.func->location);
if (get<AnyTypeVar>(fn)) if (get<AnyTypeVar>(fn))
@ -3428,31 +3489,44 @@ std::optional<ExprResult<TypePackId>> TypeChecker::checkCallOverload(const Scope
return {{retPack}}; return {{retPack}};
} }
const FunctionTypeVar* ftv = get<FunctionTypeVar>(fn); std::vector<Location> metaArgLocations;
if (!ftv)
// Might be a callable table
if (const MetatableTypeVar* mttv = get<MetatableTypeVar>(fn))
{ {
// Might be a callable table if (std::optional<TypeId> ty = getIndexTypeFromType(scope, mttv->metatable, "__call", expr.func->location, false))
if (const MetatableTypeVar* mttv = get<MetatableTypeVar>(fn))
{ {
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 fn = instantiate(scope, *ty, expr.func->location);
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);
argPack = metaCallArgPack;
args = metaCallArgs;
argLocations = &metaArgLocations;
}
else
{
TypeId fn = *ty; TypeId fn = *ty;
fn = instantiate(scope, fn, expr.func->location); 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); overloadsThatMatchArgCount, overloadsThatDont, errors);
} }
} }
}
const FunctionTypeVar* ftv = get<FunctionTypeVar>(fn);
if (!ftv)
{
reportError(TypeError{expr.func->location, CannotCallNonFunction{fn}}); reportError(TypeError{expr.func->location, CannotCallNonFunction{fn}});
unify(retPack, errorRecoveryTypePack(scope), expr.func->location); unify(retPack, errorRecoveryTypePack(scope), expr.func->location);
return {{errorRecoveryTypePack(retPack)}}; return {{errorRecoveryTypePack(retPack)}};
@ -3477,7 +3551,7 @@ std::optional<ExprResult<TypePackId>> TypeChecker::checkCallOverload(const Scope
return {}; return {};
} }
checkArgumentList(scope, state, argPack, ftv->argTypes, argLocations); checkArgumentList(scope, state, argPack, ftv->argTypes, *argLocations);
if (!state.errors.empty()) if (!state.errors.empty())
{ {
@ -3772,7 +3846,7 @@ TypeId TypeChecker::checkRequire(const ScopePtr& scope, const ModuleInfo& module
if (moduleInfo.name.empty()) if (moduleInfo.name.empty())
{ {
if (FFlag::LuauStrictRequire && currentModule->mode == Mode::Strict) if (currentModule->mode == Mode::Strict)
{ {
reportError(TypeError{location, UnknownRequire{}}); reportError(TypeError{location, UnknownRequire{}});
return errorRecoveryType(anyType); 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. // 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; scope->varargPack = parent->varargPack;
currentModule->scopes.push_back(std::make_pair(location, scope)); 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) TypeId TypeChecker::errorRecoveryType(const ScopePtr& scope)
{ {
return singletonTypes.errorRecoveryType(); return getSingletonTypes().errorRecoveryType();
} }
TypeId TypeChecker::errorRecoveryType(TypeId guess) TypeId TypeChecker::errorRecoveryType(TypeId guess)
{ {
return singletonTypes.errorRecoveryType(guess); return getSingletonTypes().errorRecoveryType(guess);
} }
TypePackId TypeChecker::errorRecoveryTypePack(const ScopePtr& scope) TypePackId TypeChecker::errorRecoveryTypePack(const ScopePtr& scope)
{ {
return singletonTypes.errorRecoveryTypePack(); return getSingletonTypes().errorRecoveryTypePack();
} }
TypePackId TypeChecker::errorRecoveryTypePack(TypePackId guess) TypePackId TypeChecker::errorRecoveryTypePack(TypePackId guess)
{ {
return singletonTypes.errorRecoveryTypePack(guess); return getSingletonTypes().errorRecoveryTypePack(guess);
} }
std::optional<TypeId> TypeChecker::filterMap(TypeId type, TypeIdPredicate predicate) 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>()) else if (const auto& func = annotation.as<AstTypeFunction>())
{ {
ScopePtr funcScope = childScope(scope, func->location); ScopePtr funcScope = childScope(scope, func->location);
funcScope->level = scope->level.incr();
auto [generics, genericPacks] = createGenericTypes(funcScope, std::nullopt, annotation, func->generics, func->genericPacks); 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); TypeId unwrapped = follow(*metatable);
if (get<AnyTypeVar>(unwrapped)) if (get<AnyTypeVar>(unwrapped))
return singletonTypes.anyType; return getSingletonTypes().anyType;
const TableTypeVar* mtt = getTableType(unwrapped); const TableTypeVar* mtt = getTableType(unwrapped);
if (!mtt) if (!mtt)
@ -61,12 +61,12 @@ std::optional<TypeId> findTablePropertyRespectingMeta(ErrorVec& errors, const Sc
{ {
std::optional<TypeId> r = first(follow(itf->retType)); std::optional<TypeId> r = first(follow(itf->retType));
if (!r) if (!r)
return singletonTypes.nilType; return getSingletonTypes().nilType;
else else
return *r; return *r;
} }
else if (get<AnyTypeVar>(index)) else if (get<AnyTypeVar>(index))
return singletonTypes.anyType; return getSingletonTypes().anyType;
else else
errors.push_back(TypeError{location, GenericError{"__index should either be a function or table. Got " + toString(index)}}); 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_FASTINTVARIABLE(LuauTableTypeMaximumStringifierLength, 0)
LUAU_FASTFLAGVARIABLE(LuauRefactorTagging, false) LUAU_FASTFLAGVARIABLE(LuauRefactorTagging, false)
LUAU_FASTFLAG(LuauErrorRecoveryType) LUAU_FASTFLAG(LuauErrorRecoveryType)
LUAU_FASTFLAG(DebugLuauFreezeArena)
namespace Luau namespace Luau
{ {
@ -579,11 +580,25 @@ SingletonTypes::SingletonTypes()
, arena(new TypeArena) , arena(new TypeArena)
{ {
TypeId stringMetatable = makeStringMetatable(); TypeId stringMetatable = makeStringMetatable();
stringType_.ty = PrimitiveTypeVar{PrimitiveTypeVar::String, makeStringMetatable()}; stringType_.ty = PrimitiveTypeVar{PrimitiveTypeVar::String, stringMetatable};
persist(stringMetatable); persist(stringMetatable);
debugFreezeArena = FFlag::DebugLuauFreezeArena;
freeze(*arena); 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() TypeId SingletonTypes::makeStringMetatable()
{ {
const TypeId optionalNumber = arena->addType(UnionTypeVar{{nilType, numberType}}); 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}); 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}); return arena->addType(TableTypeVar{{{{"__index", {tableType}}}}, std::nullopt, TypeLevel{}, TableState::Sealed});
} }
@ -670,7 +688,11 @@ TypePackId SingletonTypes::errorRecoveryTypePack(TypePackId guess)
return &errorTypePack_; return &errorTypePack_;
} }
SingletonTypes singletonTypes; SingletonTypes& getSingletonTypes()
{
static SingletonTypes singletonTypes;
return singletonTypes;
}
void persist(TypeId ty) void persist(TypeId ty)
{ {
@ -719,6 +741,18 @@ void persist(TypeId ty)
for (TypeId opt : itv->parts) for (TypeId opt : itv->parts)
queue.push_back(opt); 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) if (p->tail)
persist(*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) const TypeLevel* getLevel(TypeId ty)
@ -757,167 +802,6 @@ TypeLevel* getMutableLevel(TypeId ty)
return const_cast<TypeLevel*>(getLevel(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) const Property* lookupClassProp(const ClassTypeVar* cls, const Name& name)
{ {
while (cls) while (cls)
@ -953,16 +837,6 @@ bool isSubclass(const ClassTypeVar* cls, const ClassTypeVar* parent)
return false; 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) UnionTypeVarIterator::UnionTypeVarIterator(const UnionTypeVar* utv)
{ {
LUAU_ASSERT(utv); LUAU_ASSERT(utv);

View file

@ -14,7 +14,7 @@
LUAU_FASTINT(LuauTypeInferRecursionLimit); LUAU_FASTINT(LuauTypeInferRecursionLimit);
LUAU_FASTINT(LuauTypeInferTypePackLoopLimit); LUAU_FASTINT(LuauTypeInferTypePackLoopLimit);
LUAU_FASTINTVARIABLE(LuauTypeInferIterationLimit, 2000); LUAU_FASTINTVARIABLE(LuauTypeInferIterationLimit, 2000);
LUAU_FASTFLAGVARIABLE(LuauTableSubtypingVariance, false); LUAU_FASTFLAGVARIABLE(LuauTableSubtypingVariance2, false);
LUAU_FASTFLAGVARIABLE(LuauUnionHeuristic, false) LUAU_FASTFLAGVARIABLE(LuauUnionHeuristic, false)
LUAU_FASTFLAGVARIABLE(LuauTableUnificationEarlyTest, false) LUAU_FASTFLAGVARIABLE(LuauTableUnificationEarlyTest, false)
LUAU_FASTFLAGVARIABLE(LuauOccursCheckOkWithRecursiveFunctions, false) LUAU_FASTFLAGVARIABLE(LuauOccursCheckOkWithRecursiveFunctions, false)
@ -22,9 +22,82 @@ LUAU_FASTFLAGVARIABLE(LuauExtendedTypeMismatchError, false)
LUAU_FASTFLAG(LuauSingletonTypes) LUAU_FASTFLAG(LuauSingletonTypes)
LUAU_FASTFLAGVARIABLE(LuauExtendedClassMismatchError, false) LUAU_FASTFLAGVARIABLE(LuauExtendedClassMismatchError, false)
LUAU_FASTFLAG(LuauErrorRecoveryType); LUAU_FASTFLAG(LuauErrorRecoveryType);
LUAU_FASTFLAG(LuauProperTypeLevels);
LUAU_FASTFLAGVARIABLE(LuauExtendedUnionMismatchError, false)
LUAU_FASTFLAGVARIABLE(LuauExtendedFunctionMismatchError, false)
namespace Luau 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 struct SkipCacheForType
{ {
SkipCacheForType(const DenseHashMap<TypeId, bool>& skipCacheForType) SkipCacheForType(const DenseHashMap<TypeId, bool>& skipCacheForType)
@ -127,6 +200,29 @@ static std::optional<TypeError> hasUnificationTooComplex(const ErrorVec& errors)
return *it; 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) Unifier::Unifier(TypeArena* types, Mode mode, ScopePtr globalScope, const Location& location, Variance variance, UnifierSharedState& sharedState)
: types(types) : types(types)
, mode(mode) , mode(mode)
@ -214,9 +310,11 @@ void Unifier::tryUnify_(TypeId superTy, TypeId subTy, bool isFunctionCall, bool
{ {
occursCheck(superTy, subTy); occursCheck(superTy, subTy);
TypeLevel superLevel = l->level;
// Unification can't change the level of a generic. // Unification can't change the level of a generic.
auto rightGeneric = get<GenericTypeVar>(subTy); 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 // TODO: a more informative error message? CLI-39912
errors.push_back(TypeError{location, GenericError{"Generic subtype escaping scope"}}); 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 // The occurrence check might have caused superTy no longer to be a free type
if (!get<ErrorTypeVar>(superTy)) 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)) if (!rightLevel->subsumes(l->level))
*rightLevel = l->level; *rightLevel = l->level;
@ -240,6 +340,8 @@ void Unifier::tryUnify_(TypeId superTy, TypeId subTy, bool isFunctionCall, bool
} }
else if (r) else if (r)
{ {
TypeLevel subLevel = r->level;
occursCheck(subTy, superTy); occursCheck(subTy, superTy);
// Unification can't change the level of a generic. // 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 (!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)) if (!superLevel->subsumes(r->level))
*leftLevel = r->level; {
log(superTy);
*superLevel = r->level;
}
} }
log(subTy); log(subTy);
@ -327,7 +435,7 @@ void Unifier::tryUnify_(TypeId superTy, TypeId subTy, bool isFunctionCall, bool
else if (failed) else if (failed)
{ {
if (FFlag::LuauExtendedTypeMismatchError && firstFailedOption) 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 else
errors.push_back(TypeError{location, TypeMismatch{superTy, subTy}}); 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; bool found = false;
std::optional<TypeError> unificationTooComplex; std::optional<TypeError> unificationTooComplex;
size_t failedOptionCount = 0;
std::optional<TypeError> failedOption;
bool foundHeuristic = false;
size_t startIndex = 0; size_t startIndex = 0;
if (FFlag::LuauUnionHeuristic) if (FFlag::LuauUnionHeuristic)
{ {
bool found = false; if (const std::string* subName = getName(subTy))
const std::string* subName = getName(subTy);
if (subName)
{ {
for (size_t i = 0; i < uv->options.size(); ++i) for (size_t i = 0; i < uv->options.size(); ++i)
{ {
const std::string* optionName = getName(uv->options[i]); const std::string* optionName = getName(uv->options[i]);
if (optionName && *optionName == *subName) if (optionName && *optionName == *subName)
{ {
found = true; foundHeuristic = true;
startIndex = i; startIndex = i;
break; 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) 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; unificationTooComplex = e;
} }
else if (FFlag::LuauExtendedUnionMismatchError && !isNil(type))
{
failedOptionCount++;
if (!failedOption)
failedOption = {innerState.errors.front()};
}
innerState.log.rollback(); innerState.log.rollback();
} }
if (unificationTooComplex) if (unificationTooComplex)
{
errors.push_back(*unificationTooComplex); errors.push_back(*unificationTooComplex);
}
else if (!found) 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"}}); errors.push_back(TypeError{location, TypeMismatch{superTy, subTy, "none of the union options are compatible"}});
else else
errors.push_back(TypeError{location, TypeMismatch{superTy, subTy}}); errors.push_back(TypeError{location, TypeMismatch{superTy, subTy}});
@ -431,7 +569,7 @@ void Unifier::tryUnify_(TypeId superTy, TypeId subTy, bool isFunctionCall, bool
if (unificationTooComplex) if (unificationTooComplex)
errors.push_back(*unificationTooComplex); errors.push_back(*unificationTooComplex);
else if (firstFailedOption) 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 else
{ {
@ -771,6 +909,10 @@ void Unifier::tryUnify_(TypePackId superTp, TypePackId subTp, bool isFunctionCal
if (superIter.good() && subIter.good()) if (superIter.good() && subIter.good())
{ {
tryUnify_(*superIter, *subIter); tryUnify_(*superIter, *subIter);
if (FFlag::LuauExtendedFunctionMismatchError && !errors.empty() && !firstPackErrorPos)
firstPackErrorPos = loopCount;
superIter.advance(); superIter.advance();
subIter.advance(); subIter.advance();
continue; continue;
@ -853,13 +995,13 @@ void Unifier::tryUnify_(TypePackId superTp, TypePackId subTp, bool isFunctionCal
while (superIter.good()) while (superIter.good())
{ {
tryUnify_(singletonTypes.errorRecoveryType(), *superIter); tryUnify_(getSingletonTypes().errorRecoveryType(), *superIter);
superIter.advance(); superIter.advance();
} }
while (subIter.good()) while (subIter.good())
{ {
tryUnify_(singletonTypes.errorRecoveryType(), *subIter); tryUnify_(getSingletonTypes().errorRecoveryType(), *subIter);
subIter.advance(); subIter.advance();
} }
@ -917,14 +1059,22 @@ void Unifier::tryUnifyFunctions(TypeId superTy, TypeId subTy, bool isFunctionCal
if (numGenerics != rf->generics.size()) if (numGenerics != rf->generics.size())
{ {
numGenerics = std::min(lf->generics.size(), 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(); size_t numGenericPacks = lf->genericPacks.size();
if (numGenericPacks != rf->genericPacks.size()) if (numGenericPacks != rf->genericPacks.size())
{ {
numGenericPacks = std::min(lf->genericPacks.size(), 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++) for (size_t i = 0; i < numGenerics; i++)
@ -936,13 +1086,49 @@ void Unifier::tryUnifyFunctions(TypeId superTy, TypeId subTy, bool isFunctionCal
{ {
Unifier innerState = makeChildUnifier(); Unifier innerState = makeChildUnifier();
ctx = CountMismatch::Arg; if (FFlag::LuauExtendedFunctionMismatchError)
innerState.tryUnify_(rf->argTypes, lf->argTypes, isFunctionCall); {
innerState.ctx = CountMismatch::Arg;
innerState.tryUnify_(rf->argTypes, lf->argTypes, isFunctionCall);
ctx = CountMismatch::Result; bool reported = !innerState.errors.empty();
innerState.tryUnify_(lf->retType, rf->retType);
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)); log.concat(std::move(innerState.log));
} }
@ -994,7 +1180,7 @@ struct Resetter
void Unifier::tryUnifyTables(TypeId left, TypeId right, bool isIntersection) void Unifier::tryUnifyTables(TypeId left, TypeId right, bool isIntersection)
{ {
if (!FFlag::LuauTableSubtypingVariance) if (!FFlag::LuauTableSubtypingVariance2)
return DEPRECATED_tryUnifyTables(left, right, isIntersection); return DEPRECATED_tryUnifyTables(left, right, isIntersection);
TableTypeVar* lt = getMutable<TableTypeVar>(left); 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. // TODO: hopefully readonly/writeonly properties will fix this.
Property clone = prop; Property clone = prop;
clone.type = deeplyOptional(clone.type); clone.type = deeplyOptional(clone.type);
log(lt); log(left);
lt->props[name] = clone; lt->props[name] = clone;
} }
else if (variance == Covariant) else if (variance == Covariant)
@ -1146,7 +1332,7 @@ void Unifier::tryUnifyTables(TypeId left, TypeId right, bool isIntersection)
} }
else if (lt->state == TableState::Free) else if (lt->state == TableState::Free)
{ {
log(lt); log(left);
lt->props[name] = prop; lt->props[name] = prop;
} }
else 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. // 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 // TODO: we only need to do this if the supertype's indexer is read/write
// since that can add indexed elements. // since that can add indexed elements.
log(rt); log(right);
rt->indexer = lt->indexer; rt->indexer = lt->indexer;
} }
} }
@ -1185,7 +1371,7 @@ void Unifier::tryUnifyTables(TypeId left, TypeId right, bool isIntersection)
// Symmetric if we are invariant // Symmetric if we are invariant
if (lt->state == TableState::Unsealed || lt->state == TableState::Free) if (lt->state == TableState::Unsealed || lt->state == TableState::Free)
{ {
log(lt); log(left);
lt->indexer = rt->indexer; lt->indexer = rt->indexer;
} }
} }
@ -1241,15 +1427,15 @@ TypeId Unifier::deeplyOptional(TypeId ty, std::unordered_map<TypeId, TypeId> see
TableTypeVar* resultTtv = getMutable<TableTypeVar>(result); TableTypeVar* resultTtv = getMutable<TableTypeVar>(result);
for (auto& [name, prop] : resultTtv->props) for (auto& [name, prop] : resultTtv->props)
prop.type = deeplyOptional(prop.type, seen); prop.type = deeplyOptional(prop.type, seen);
return types->addType(UnionTypeVar{{singletonTypes.nilType, result}}); return types->addType(UnionTypeVar{{getSingletonTypes().nilType, result}});
} }
else 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) void Unifier::DEPRECATED_tryUnifyTables(TypeId left, TypeId right, bool isIntersection)
{ {
LUAU_ASSERT(!FFlag::LuauTableSubtypingVariance); LUAU_ASSERT(!FFlag::LuauTableSubtypingVariance2);
Resetter resetter{&variance}; Resetter resetter{&variance};
variance = Invariant; variance = Invariant;
@ -1467,7 +1653,7 @@ void Unifier::tryUnifySealedTables(TypeId left, TypeId right, bool isIntersectio
} }
else if (lt->indexer) 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. // 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. // Skip those and just look for the ones remaining and see if they fit into the indexer.
for (const auto& [name, type] : rt->props) for (const auto& [name, type] : rt->props)
@ -1636,7 +1822,7 @@ void Unifier::tryUnifyWithClass(TypeId superTy, TypeId subTy, bool reversed)
ok = false; ok = false;
errors.push_back(TypeError{location, UnknownProperty{superTy, propName}}); errors.push_back(TypeError{location, UnknownProperty{superTy, propName}});
if (!FFlag::LuauExtendedClassMismatchError) if (!FFlag::LuauExtendedClassMismatchError)
tryUnify_(prop.type, singletonTypes.errorRecoveryType()); tryUnify_(prop.type, getSingletonTypes().errorRecoveryType());
} }
else else
{ {
@ -1825,7 +2011,7 @@ void Unifier::tryUnifyWithAny(TypeId any, TypeId ty)
if (get<PrimitiveTypeVar>(ty) || get<AnyTypeVar>(ty) || get<ClassTypeVar>(ty)) if (get<PrimitiveTypeVar>(ty) || get<AnyTypeVar>(ty) || get<ClassTypeVar>(ty))
return; 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{}}); 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.tempSeenTy.clear();
sharedState.tempSeenTp.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) void Unifier::tryUnifyWithAny(TypePackId any, TypePackId ty)
{ {
LUAU_ASSERT(get<Unifiable::Error>(any)); LUAU_ASSERT(get<Unifiable::Error>(any));
const TypeId anyTy = singletonTypes.errorRecoveryType(); const TypeId anyTy = getSingletonTypes().errorRecoveryType();
std::vector<TypeId> queue; std::vector<TypeId> queue;
@ -1887,7 +2073,7 @@ void Unifier::occursCheck(DenseHashSet<TypeId>& seen, TypeId needle, TypeId hays
{ {
errors.push_back(TypeError{location, OccursCheckFailed{}}); errors.push_back(TypeError{location, OccursCheckFailed{}});
log(needle); log(needle);
*asMutable(needle) = *singletonTypes.errorRecoveryType(); *asMutable(needle) = *getSingletonTypes().errorRecoveryType();
return; return;
} }
@ -1951,7 +2137,7 @@ void Unifier::occursCheck(DenseHashSet<TypePackId>& seen, TypePackId needle, Typ
{ {
errors.push_back(TypeError{location, OccursCheckFailed{}}); errors.push_back(TypeError{location, OccursCheckFailed{}});
log(needle); log(needle);
*asMutable(needle) = *singletonTypes.errorRecoveryTypePack(); *asMutable(needle) = *getSingletonTypes().errorRecoveryTypePack();
return; return;
} }
@ -2005,7 +2191,7 @@ void Unifier::checkChildUnifierTypeMismatch(const ErrorVec& innerErrors, const s
errors.push_back(*e); errors.push_back(*e);
else if (!innerErrors.empty()) else if (!innerErrors.empty())
errors.push_back( 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) void Unifier::ice(const std::string& message, const Location& location)

View file

@ -10,7 +10,6 @@
// See docs/SyntaxChanges.md for an explanation. // See docs/SyntaxChanges.md for an explanation.
LUAU_FASTINTVARIABLE(LuauRecursionLimit, 1000) LUAU_FASTINTVARIABLE(LuauRecursionLimit, 1000)
LUAU_FASTINTVARIABLE(LuauParseErrorLimit, 100) LUAU_FASTINTVARIABLE(LuauParseErrorLimit, 100)
LUAU_FASTFLAGVARIABLE(LuauCaptureBrokenCommentSpans, false)
LUAU_FASTFLAGVARIABLE(LuauIfElseExpressionBaseSupport, false) LUAU_FASTFLAGVARIABLE(LuauIfElseExpressionBaseSupport, false)
LUAU_FASTFLAGVARIABLE(LuauIfStatementRecursionGuard, false) LUAU_FASTFLAGVARIABLE(LuauIfStatementRecursionGuard, false)
LUAU_FASTFLAGVARIABLE(LuauFixAmbiguousErrorRecoveryInAssign, 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; 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; const char* text = p.lexer.current().data;
unsigned int length = p.lexer.current().length; unsigned int length = p.lexer.current().length;
@ -2780,7 +2779,7 @@ const Lexeme& Parser::nextLexeme()
const Lexeme& lexeme = lexer.next(/*skipComments*/ false); 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. // 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. // 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}); commentLocations.push_back(Comment{lexeme.type, lexeme.location});
if (isComment(lexeme)) if (isComment(lexeme))
commentLocations.push_back(Comment{lexeme.type, lexeme.location}); commentLocations.push_back(Comment{lexeme.type, lexeme.location});

View file

@ -11,26 +11,33 @@
enum class ReportFormat enum class ReportFormat
{ {
Default, 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) switch (format)
{ {
case ReportFormat::Default: 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; break;
case ReportFormat::Luacheck: case ReportFormat::Luacheck:
{ {
// Note: luacheck's end column is inclusive but our end column is exclusive // 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 // 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; 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("\n");
printf("Available options:\n"); printf("Available options:\n");
printf(" --formatter=plain: report analysis errors in Luacheck-compatible format\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) 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) if (strcmp(argv[i], "--formatter=plain") == 0)
format = ReportFormat::Luacheck; format = ReportFormat::Luacheck;
else if (strcmp(argv[i], "--formatter=gnu") == 0)
format = ReportFormat::Gnu;
else if (strcmp(argv[i], "--annotate") == 0) else if (strcmp(argv[i], "--annotate") == 0)
annotate = true; 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(); 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) if (!f)
{ {
fprintf(stderr, "Error opening profile %s\n", name); fprintf(stderr, "Error opening profile %s\n", path);
return; return;
} }
@ -129,7 +129,7 @@ void profilerDump(const char* name)
fclose(f); 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())); static_cast<long long>(gProfiler.samples.load()), static_cast<long long>(gProfiler.data.size()));
uint64_t totalgc = 0; uint64_t totalgc = 0;

View file

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

View file

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

View file

@ -10,7 +10,6 @@
#include <bitset> #include <bitset>
#include <math.h> #include <math.h>
LUAU_FASTFLAGVARIABLE(LuauPreloadClosures, false)
LUAU_FASTFLAG(LuauIfElseExpressionBaseSupport) LUAU_FASTFLAG(LuauIfElseExpressionBaseSupport)
LUAU_FASTFLAGVARIABLE(LuauBit32CountBuiltin, false) LUAU_FASTFLAGVARIABLE(LuauBit32CountBuiltin, false)
@ -462,20 +461,17 @@ struct Compiler
bool shared = false; 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 int32_t cid = bytecode.addConstantClosure(f->id);
// 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);
if (cid >= 0 && cid < 32768) if (cid >= 0 && cid < 32768)
{ {
bytecode.emitAD(LOP_DUPCLOSURE, target, cid); bytecode.emitAD(LOP_DUPCLOSURE, target, cid);
shared = true; shared = true;
}
} }
} }

View file

@ -27,7 +27,7 @@ TESTS_SOURCES=$(wildcard tests/*.cpp)
TESTS_OBJECTS=$(TESTS_SOURCES:%=$(BUILD)/%.o) TESTS_OBJECTS=$(TESTS_SOURCES:%=$(BUILD)/%.o)
TESTS_TARGET=$(BUILD)/luau-tests 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_OBJECTS=$(REPL_CLI_SOURCES:%=$(BUILD)/%.o)
REPL_CLI_TARGET=$(BUILD)/luau REPL_CLI_TARGET=$(BUILD)/luau
@ -128,10 +128,10 @@ luau-size: luau
# executable target aliases # executable target aliases
luau: $(REPL_CLI_TARGET) luau: $(REPL_CLI_TARGET)
cp $^ $@ ln -fs $^ $@
luau-analyze: $(ANALYZE_CLI_TARGET) luau-analyze: $(ANALYZE_CLI_TARGET)
cp $^ $@ ln -fs $^ $@
# executable targets # executable targets
$(TESTS_TARGET): $(TESTS_OBJECTS) $(ANALYSIS_TARGET) $(COMPILER_TARGET) $(AST_TARGET) $(VM_TARGET) $(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/ltable.cpp
VM/src/ltablib.cpp VM/src/ltablib.cpp
VM/src/ltm.cpp VM/src/ltm.cpp
VM/src/ludata.cpp
VM/src/lutf8lib.cpp VM/src/lutf8lib.cpp
VM/src/lvmexecute.cpp VM/src/lvmexecute.cpp
VM/src/lvmload.cpp VM/src/lvmload.cpp
@ -152,12 +153,15 @@ target_sources(Luau.VM PRIVATE
VM/src/lstring.h VM/src/lstring.h
VM/src/ltable.h VM/src/ltable.h
VM/src/ltm.h VM/src/ltm.h
VM/src/ludata.h
VM/src/lvm.h VM/src/lvm.h
) )
if(TARGET Luau.Repl.CLI) if(TARGET Luau.Repl.CLI)
# Luau.Repl.CLI Sources # Luau.Repl.CLI Sources
target_sources(Luau.Repl.CLI PRIVATE target_sources(Luau.Repl.CLI PRIVATE
CLI/Coverage.h
CLI/Coverage.cpp
CLI/FileUtils.h CLI/FileUtils.h
CLI/FileUtils.cpp CLI/FileUtils.cpp
CLI/Profiler.h 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_singlestep(lua_State* L, int enabled);
LUA_API void lua_breakpoint(lua_State* L, int funcindex, int line, 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. */ /* 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); LUA_API const char* lua_debugtrace(lua_State* L);

View file

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

View file

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

View file

@ -7,6 +7,7 @@
#include "ltable.h" #include "ltable.h"
#include "lfunc.h" #include "lfunc.h"
#include "lstring.h" #include "lstring.h"
#include "ludata.h"
#include <string.h> #include <string.h>
#include <stdio.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 thvalue(o) check_exp(ttisthread(o), &(o)->value.gc->th)
#define upvalue(o) check_exp(ttisupval(o), &(o)->value.gc->uv) #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 #define l_isfalse(o) (ttisnil(o) || (ttisboolean(o) && bvalue(o) == 0))
// 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)))
/* /*
** for internal debug only ** for internal debug only

View file

@ -206,32 +206,3 @@ void luaS_free(lua_State* L, TString* ts)
L->global->strt.nuse--; L->global->strt.nuse--;
luaM_free(L, ts, sizestring(ts->len), ts->memcat); 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 */ /* string size limit */
#define MAXSSIZE (1 << 30) #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 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_new(L, s) (luaS_newlstr(L, s, strlen(s)))
#define luaS_newliteral(L, s) (luaS_newlstr(L, "" s, (sizeof(s) / sizeof(char)) - 1)) #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 TString* luaS_newlstr(lua_State* L, const char* str, size_t l);
LUAI_FUNC void luaS_free(lua_State* L, TString* ts); 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_bufstart(lua_State* L, size_t size);
LUAI_FUNC TString* luaS_buffinish(lua_State* L, TString* ts); 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_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_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 // NOTE: If debugging the Luau code, disable this macro to prevent timeouts from
// occurring when tracing code in Visual Studio / XCode // occurring when tracing code in Visual Studio / XCode
@ -120,7 +121,7 @@
*/ */
#if VM_USE_CGOTO #if VM_USE_CGOTO
#define VM_CASE(op) CASE_##op: #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)] #define VM_CONTINUE(op) goto* kDispatchTable[uint8_t(op)]
#else #else
#define VM_CASE(op) case op: #define VM_CASE(op) case op:
@ -325,7 +326,7 @@ static void luau_execute(lua_State* L)
// ... and singlestep logic :) // ... and singlestep logic :)
if (SingleStep) 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)); 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 #if VM_USE_CGOTO
VM_CONTINUE(*(uint8_t*)pc); VM_CONTINUE(LUAU_INSN_OP(*pc));
#endif #endif
} }
#if !VM_USE_CGOTO #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 = LUAU_INSN_OP(*pc);
size_t dispatchOp = *(uint8_t*)pc;
dispatchContinue: dispatchContinue:
switch (dispatchOp) switch (dispatchOp)
@ -2577,7 +2577,7 @@ static void luau_execute(lua_State* L)
// update hits with saturated add and patch the instruction in place // update hits with saturated add and patch the instruction in place
hits = (hits < (1 << 23) - 1) ? hits + 1 : hits; 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(); VM_NEXT();
} }

View file

@ -451,15 +451,16 @@ function raytraceScene()
end end
function arrayToCanvasCommands(pixels) 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 for y = 0,size-1 do
s = s .. "["; table.insert(s, "[");
for x = 0,size-1 do 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 end
s = s .. "],"; table.insert(s, "],");
end 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\
\n\ \n\
var size = ' .. size .. ';\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.setFillColor(l[0], l[1], l[2], 1);\n\
canvas.fillRect(x, y, 1, 1);\n\ canvas.fillRect(x, y, 1, 1);\n\
}\n\ }\n\
}</scr' .. 'ipt></body></html>'; }</script></body></html>');
return s; return table.concat(s);
end end
testOutput = arrayToCanvasCommands(raytraceScene()); 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") TEST_CASE_FIXTURE(ACFixture, "dont_offer_any_suggestions_from_within_a_broken_comment")
{ {
ScopedFastFlag sff{"LuauCaptureBrokenCommentSpans", true};
check(R"( check(R"(
--[[ @1 --[[ @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") 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"); check("--[[@1");
auto ac = autocomplete('1'); auto ac = autocomplete('1');
@ -2625,4 +2621,55 @@ local a: A<(number, s@1>
CHECK(ac.entryMap.count("string")); 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(); TEST_SUITE_END();

View file

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

View file

@ -123,8 +123,8 @@ int lua_silence(lua_State* L)
using StateRef = std::unique_ptr<lua_State, void (*)(lua_State*)>; using StateRef = std::unique_ptr<lua_State, void (*)(lua_State*)>;
static StateRef runConformance( static StateRef runConformance(const char* name, void (*setup)(lua_State* L) = nullptr, void (*yield)(lua_State* L) = nullptr,
const char* name, void (*setup)(lua_State* L) = nullptr, void (*yield)(lua_State* L) = nullptr, lua_State* initialLuaState = nullptr) lua_State* initialLuaState = nullptr, lua_CompileOptions* copts = nullptr)
{ {
std::string path = __FILE__; std::string path = __FILE__;
path.erase(path.find_last_of("\\/")); path.erase(path.find_last_of("\\/"));
@ -180,13 +180,8 @@ static StateRef runConformance(
std::string chunkname = "=" + std::string(name); 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; 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); int result = luau_load(L, chunkname.c_str(), bytecode, bytecodeSize, 0);
free(bytecode); free(bytecode);
@ -373,29 +368,37 @@ TEST_CASE("Vector")
{ {
ScopedFastFlag sff{"LuauIfElseExpressionBaseSupport", true}; ScopedFastFlag sff{"LuauIfElseExpressionBaseSupport", true};
runConformance("vector.lua", [](lua_State* L) { lua_CompileOptions copts = {};
lua_pushcfunction(L, lua_vector, "vector"); copts.optimizationLevel = 1;
lua_setglobal(L, "vector"); 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 #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 #else
lua_pushvector(L, 0.0f, 0.0f, 0.0f); lua_pushvector(L, 0.0f, 0.0f, 0.0f);
#endif #endif
luaL_newmetatable(L, "vector"); luaL_newmetatable(L, "vector");
lua_pushstring(L, "__index"); lua_pushstring(L, "__index");
lua_pushcfunction(L, lua_vector_index, nullptr); lua_pushcfunction(L, lua_vector_index, nullptr);
lua_settable(L, -3); lua_settable(L, -3);
lua_pushstring(L, "__namecall"); lua_pushstring(L, "__namecall");
lua_pushcfunction(L, lua_vector_namecall, nullptr); lua_pushcfunction(L, lua_vector_namecall, nullptr);
lua_settable(L, -3); lua_settable(L, -3);
lua_setreadonly(L, -1, true); lua_setreadonly(L, -1, true);
lua_setmetatable(L, -2); lua_setmetatable(L, -2);
lua_pop(L, 1); lua_pop(L, 1);
}); },
nullptr, nullptr, &copts);
} }
static void populateRTTI(lua_State* L, Luau::TypeId type) static void populateRTTI(lua_State* L, Luau::TypeId type)
@ -499,6 +502,10 @@ TEST_CASE("Debugger")
breakhits = 0; breakhits = 0;
interruptedthread = nullptr; interruptedthread = nullptr;
lua_CompileOptions copts = {};
copts.optimizationLevel = 1;
copts.debugLevel = 2;
runConformance( runConformance(
"debugger.lua", "debugger.lua",
[](lua_State* L) { [](lua_State* L) {
@ -614,7 +621,8 @@ TEST_CASE("Debugger")
lua_resume(interruptedthread, nullptr, 0); lua_resume(interruptedthread, nullptr, 0);
interruptedthread = nullptr; interruptedthread = nullptr;
} }
}); },
nullptr, &copts);
CHECK(breakhits == 10); // 2 hits per breakpoint 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(); 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") TEST_CASE_FIXTURE(Fixture, "capture_broken_comment_at_the_start_of_the_file")
{ {
ScopedFastFlag sff{"LuauCaptureBrokenCommentSpans", true};
ParseOptions options; ParseOptions options;
options.captureComments = true; 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") TEST_CASE_FIXTURE(Fixture, "capture_broken_comment")
{ {
ScopedFastFlag sff{"LuauCaptureBrokenCommentSpans", true};
ParseOptions options; ParseOptions options;
options.captureComments = true; 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"))); 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") TEST_CASE_FIXTURE(Fixture, "type_annotations_inside_function_bodies")
{ {
CheckResult result = check(R"( CheckResult result = check(R"(

View file

@ -7,6 +7,8 @@
#include "doctest.h" #include "doctest.h"
LUAU_FASTFLAG(LuauFixTonumberReturnType)
using namespace Luau; using namespace Luau;
TEST_SUITE_BEGIN("BuiltinTests"); 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); 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") TEST_CASE_FIXTURE(Fixture, "dont_add_definitions_to_persistent_types")
{ {
CheckResult result = check(R"( CheckResult result = check(R"(

View file

@ -9,6 +9,8 @@
using namespace Luau; using namespace Luau;
LUAU_FASTFLAG(LuauExtendedFunctionMismatchError)
TEST_SUITE_BEGIN("GenericsTests"); TEST_SUITE_BEGIN("GenericsTests");
TEST_CASE_FIXTURE(Fixture, "check_generic_function") TEST_CASE_FIXTURE(Fixture, "check_generic_function")
@ -644,4 +646,42 @@ f(1, 2, 3)
CHECK_EQ(toString(*ty, opts), "(a: number, number, number) -> ()"); 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(); 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") 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}; ScopedFastFlag luauExtendedTypeMismatchError{"LuauExtendedTypeMismatchError", true};
CheckResult result = check(R"( CheckResult result = check(R"(
@ -1085,4 +1085,19 @@ TEST_CASE_FIXTURE(Fixture, "type_comparison_ifelse_expression")
CHECK_EQ("any", toString(requireTypeAtPosition({6, 66}))); 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(); TEST_SUITE_END();

View file

@ -374,4 +374,54 @@ TEST_CASE_FIXTURE(Fixture, "table_properties_type_error_escapes")
toString(result.errors[0])); 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(); TEST_SUITE_END();

View file

@ -12,6 +12,8 @@
using namespace Luau; using namespace Luau;
LUAU_FASTFLAG(LuauExtendedFunctionMismatchError)
TEST_SUITE_BEGIN("TableTests"); TEST_SUITE_BEGIN("TableTests");
TEST_CASE_FIXTURE(Fixture, "basic") TEST_CASE_FIXTURE(Fixture, "basic")
@ -275,7 +277,7 @@ TEST_CASE_FIXTURE(Fixture, "open_table_unification")
TEST_CASE_FIXTURE(Fixture, "open_table_unification_2") TEST_CASE_FIXTURE(Fixture, "open_table_unification_2")
{ {
ScopedFastFlag sff{"LuauTableSubtypingVariance", true}; ScopedFastFlag sff{"LuauTableSubtypingVariance2", true};
CheckResult result = check(R"( CheckResult result = check(R"(
local a = {} local a = {}
@ -346,7 +348,7 @@ TEST_CASE_FIXTURE(Fixture, "table_param_row_polymorphism_1")
TEST_CASE_FIXTURE(Fixture, "table_param_row_polymorphism_2") TEST_CASE_FIXTURE(Fixture, "table_param_row_polymorphism_2")
{ {
ScopedFastFlag sff{"LuauTableSubtypingVariance", true}; ScopedFastFlag sff{"LuauTableSubtypingVariance2", true};
CheckResult result = check(R"( CheckResult result = check(R"(
--!strict --!strict
@ -369,7 +371,7 @@ TEST_CASE_FIXTURE(Fixture, "table_param_row_polymorphism_2")
TEST_CASE_FIXTURE(Fixture, "table_param_row_polymorphism_3") TEST_CASE_FIXTURE(Fixture, "table_param_row_polymorphism_3")
{ {
ScopedFastFlag sff{"LuauTableSubtypingVariance", true}; ScopedFastFlag sff{"LuauTableSubtypingVariance2", true};
CheckResult result = check(R"( CheckResult result = check(R"(
local T = {} 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") 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"( CheckResult result = check(R"(
--!strict --!strict
@ -511,7 +513,7 @@ TEST_CASE_FIXTURE(Fixture, "okay_to_add_property_to_unsealed_tables_by_function_
TEST_CASE_FIXTURE(Fixture, "width_subtyping") TEST_CASE_FIXTURE(Fixture, "width_subtyping")
{ {
ScopedFastFlag sff{"LuauTableSubtypingVariance", true}; ScopedFastFlag sff{"LuauTableSubtypingVariance2", true};
CheckResult result = check(R"( CheckResult result = check(R"(
--!strict --!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") TEST_CASE_FIXTURE(Fixture, "sealed_table_value_can_infer_an_indexer")
{ {
ScopedFastFlag sff{"LuauTableSubtypingVariance", true}; ScopedFastFlag sff{"LuauTableSubtypingVariance2", true};
CheckResult result = check(R"( CheckResult result = check(R"(
local t: { a: string, [number]: string } = { a = "foo" } 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") TEST_CASE_FIXTURE(Fixture, "array_factory_function")
{ {
ScopedFastFlag sff{"LuauTableSubtypingVariance", true}; ScopedFastFlag sff{"LuauTableSubtypingVariance2", true};
CheckResult result = check(R"( CheckResult result = check(R"(
function empty() return {} end 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") TEST_CASE_FIXTURE(Fixture, "casting_tables_with_props_into_table_with_indexer3")
{ {
ScopedFastFlag sff{"LuauTableSubtypingVariance", true}; ScopedFastFlag sff{"LuauTableSubtypingVariance2", true};
CheckResult result = check(R"( CheckResult result = check(R"(
local function foo(a: {[string]: number, a: string}) end 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") TEST_CASE_FIXTURE(Fixture, "table_subtyping_with_extra_props_is_ok")
{ {
ScopedFastFlag sff{"LuauTableSubtypingVariance", true}; ScopedFastFlag sff{"LuauTableSubtypingVariance2", true};
CheckResult result = check(R"( CheckResult result = check(R"(
local vec3 = {x = 1, y = 2, z = 3} 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") 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"( CheckResult result = check(R"(
--!strict --!strict
@ -1952,7 +1954,7 @@ TEST_CASE_FIXTURE(Fixture, "table_insert_should_cope_with_optional_properties_in
TEST_CASE_FIXTURE(Fixture, "error_detailed_prop") 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}; ScopedFastFlag luauExtendedTypeMismatchError{"LuauExtendedTypeMismatchError", true};
CheckResult result = check(R"( CheckResult result = check(R"(
@ -1971,7 +1973,7 @@ caused by:
TEST_CASE_FIXTURE(Fixture, "error_detailed_prop_nested") 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}; ScopedFastFlag luauExtendedTypeMismatchError{"LuauExtendedTypeMismatchError", true};
CheckResult result = check(R"( CheckResult result = check(R"(
@ -1995,7 +1997,7 @@ caused by:
TEST_CASE_FIXTURE(Fixture, "error_detailed_metatable_prop") 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}; ScopedFastFlag luauExtendedTypeMismatchError{"LuauExtendedTypeMismatchError", true};
CheckResult result = check(R"( CheckResult result = check(R"(
@ -2015,11 +2017,22 @@ caused by:
caused by: caused by:
Property 'y' is not compatible. Type 'string' could not be converted into 'number')"); 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: caused by:
Type '{| __call: (a, b) -> () |}' could not be converted into '{| __call: <a>(a) -> () |}' Type '{| __call: (a, b) -> () |}' could not be converted into '{| __call: <a>(a) -> () |}'
caused by: caused by:
Property '__call' is not compatible. Type '(a, b) -> ()' could not be converted into '<a>(a) -> ()')"); Property '__call' is not compatible. Type '(a, b) -> ()' could not be converted into '<a>(a) -> ()')");
}
} }
TEST_CASE_FIXTURE(Fixture, "explicitly_typed_table") TEST_CASE_FIXTURE(Fixture, "explicitly_typed_table")
@ -2027,7 +2040,7 @@ TEST_CASE_FIXTURE(Fixture, "explicitly_typed_table")
ScopedFastFlag sffs[] { ScopedFastFlag sffs[] {
{"LuauPropertiesGetExpectedType", true}, {"LuauPropertiesGetExpectedType", true},
{"LuauExpectedTypesOfProperties", true}, {"LuauExpectedTypesOfProperties", true},
{"LuauTableSubtypingVariance", true}, {"LuauTableSubtypingVariance2", true},
}; };
CheckResult result = check(R"( CheckResult result = check(R"(
@ -2048,7 +2061,7 @@ TEST_CASE_FIXTURE(Fixture, "explicitly_typed_table_error")
ScopedFastFlag sffs[] { ScopedFastFlag sffs[] {
{"LuauPropertiesGetExpectedType", true}, {"LuauPropertiesGetExpectedType", true},
{"LuauExpectedTypesOfProperties", true}, {"LuauExpectedTypesOfProperties", true},
{"LuauTableSubtypingVariance", true}, {"LuauTableSubtypingVariance2", true},
{"LuauExtendedTypeMismatchError", true}, {"LuauExtendedTypeMismatchError", true},
}; };
@ -2076,7 +2089,7 @@ TEST_CASE_FIXTURE(Fixture, "explicitly_typed_table_with_indexer")
ScopedFastFlag sffs[] { ScopedFastFlag sffs[] {
{"LuauPropertiesGetExpectedType", true}, {"LuauPropertiesGetExpectedType", true},
{"LuauExpectedTypesOfProperties", true}, {"LuauExpectedTypesOfProperties", true},
{"LuauTableSubtypingVariance", true}, {"LuauTableSubtypingVariance2", true},
}; };
CheckResult result = check(R"( CheckResult result = check(R"(
@ -2092,4 +2105,18 @@ a.p = { x = 9 }
LUAU_REQUIRE_NO_ERRORS(result); 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(); TEST_SUITE_END();

View file

@ -16,6 +16,7 @@
LUAU_FASTFLAG(LuauFixLocationSpanTableIndexExpr) LUAU_FASTFLAG(LuauFixLocationSpanTableIndexExpr)
LUAU_FASTFLAG(LuauEqConstraint) LUAU_FASTFLAG(LuauEqConstraint)
LUAU_FASTFLAG(LuauExtendedFunctionMismatchError)
using namespace Luau; using namespace Luau;
@ -2084,7 +2085,7 @@ TEST_CASE_FIXTURE(Fixture, "primitive_arith_no_metatable")
{ {
CheckResult result = check(R"( CheckResult result = check(R"(
function add(a: number, b: string) function add(a: number, b: string)
return a + tonumber(b), a .. b return a + (tonumber(b) :: number), a .. b
end end
local n, s = add(2,"3") 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"( CheckResult result = check(R"(
--!strict --!strict
function f(U) 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 end
)"); )");
@ -3329,7 +3330,7 @@ TEST_CASE_FIXTURE(Fixture, "relation_op_on_any_lhs_where_rhs_maybe_has_metatable
{ {
CheckResult result = check(R"( CheckResult result = check(R"(
local x local x
print((x == true and (x .. "y")) .. 1) print((x == true and (x .. "y")) .. 1)
)"); )");
LUAU_REQUIRE_ERROR_COUNT(1, result); LUAU_REQUIRE_ERROR_COUNT(1, result);
@ -4473,7 +4474,18 @@ f(function(a, b, c, ...) return a + b end)
)"); )");
LUAU_REQUIRE_ERRORS(result); 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 // Infer from variadic packs into elements
result = check(R"( 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); 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") TEST_CASE_FIXTURE(Fixture, "infer_return_value_type")
@ -4799,4 +4821,211 @@ local ModuleA = require(game.A)
CHECK_EQ("*unknown*", toString(*oty)); 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(); 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) -> ()"); 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(); 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)"); 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(); 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'