Sync to upstream/release/590 (#1008)

* Better indentation in multi-line type mismatch error messages
* Error message clone can no longer cause a stack overflow (when
typechecking with retainFullTypeGraphs set to false); fixes
https://github.com/Roblox/luau/issues/975
* `string.format` with %s is now ~2x faster on strings smaller than 100
characters

Native code generation:
* All VM side exits will block return to the native execution of the
current function to preserve correctness
* Fixed executable page allocation on Apple platforms when using
hardened runtime
* Added statistics for code generation (no. of functions compiler,
memory used for different areas)
* Fixed issue with function entry type checks performed more that once
in some functions
This commit is contained in:
vegorov-rbx 2023-08-11 07:42:37 -07:00 committed by GitHub
parent bd229816c0
commit d98256bb80
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
77 changed files with 2345 additions and 1277 deletions

View file

@ -16,6 +16,8 @@ using SeenTypePacks = std::unordered_map<TypePackId, TypePackId>;
struct CloneState struct CloneState
{ {
NotNull<BuiltinTypes> builtinTypes;
SeenTypes seenTypes; SeenTypes seenTypes;
SeenTypePacks seenTypePacks; SeenTypePacks seenTypePacks;

View file

@ -1,7 +1,7 @@
// 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
#pragma once #pragma once
#include "Luau/Linter.h" #include "Luau/LinterConfig.h"
#include "Luau/ParseOptions.h" #include "Luau/ParseOptions.h"
#include <string> #include <string>

View file

@ -128,10 +128,10 @@ struct DiffError
checkValidInitialization(left, right); checkValidInitialization(left, right);
} }
std::string toString() const; std::string toString(bool multiLine = false) const;
private: private:
std::string toStringALeaf(std::string rootName, const DiffPathNodeLeaf& leaf, const DiffPathNodeLeaf& otherLeaf) const; std::string toStringALeaf(std::string rootName, const DiffPathNodeLeaf& leaf, const DiffPathNodeLeaf& otherLeaf, bool multiLine) const;
void checkValidInitialization(const DiffPathNodeLeaf& left, const DiffPathNodeLeaf& right); void checkValidInitialization(const DiffPathNodeLeaf& left, const DiffPathNodeLeaf& right);
void checkNonMissingPropertyLeavesHaveNulloptTableProperty() const; void checkNonMissingPropertyLeavesHaveNulloptTableProperty() const;
}; };
@ -152,12 +152,17 @@ struct DifferEnvironment
{ {
TypeId rootLeft; TypeId rootLeft;
TypeId rootRight; TypeId rootRight;
std::optional<std::string> externalSymbolLeft;
std::optional<std::string> externalSymbolRight;
DenseHashMap<TypeId, TypeId> genericMatchedPairs; DenseHashMap<TypeId, TypeId> genericMatchedPairs;
DenseHashMap<TypePackId, TypePackId> genericTpMatchedPairs; DenseHashMap<TypePackId, TypePackId> genericTpMatchedPairs;
DifferEnvironment(TypeId rootLeft, TypeId rootRight) DifferEnvironment(
TypeId rootLeft, TypeId rootRight, std::optional<std::string> externalSymbolLeft, std::optional<std::string> externalSymbolRight)
: rootLeft(rootLeft) : rootLeft(rootLeft)
, rootRight(rootRight) , rootRight(rootRight)
, externalSymbolLeft(externalSymbolLeft)
, externalSymbolRight(externalSymbolRight)
, genericMatchedPairs(nullptr) , genericMatchedPairs(nullptr)
, genericTpMatchedPairs(nullptr) , genericTpMatchedPairs(nullptr)
{ {
@ -170,6 +175,8 @@ struct DifferEnvironment
void popVisiting(); void popVisiting();
std::vector<std::pair<TypeId, TypeId>>::const_reverse_iterator visitingBegin() const; std::vector<std::pair<TypeId, TypeId>>::const_reverse_iterator visitingBegin() const;
std::vector<std::pair<TypeId, TypeId>>::const_reverse_iterator visitingEnd() const; std::vector<std::pair<TypeId, TypeId>>::const_reverse_iterator visitingEnd() const;
std::string getDevFixFriendlyNameLeft() const;
std::string getDevFixFriendlyNameRight() const;
private: private:
// TODO: consider using DenseHashSet // TODO: consider using DenseHashSet
@ -179,6 +186,7 @@ private:
std::vector<std::pair<TypeId, TypeId>> visitingStack; std::vector<std::pair<TypeId, TypeId>> visitingStack;
}; };
DifferResult diff(TypeId ty1, TypeId ty2); DifferResult diff(TypeId ty1, TypeId ty2);
DifferResult diffWithSymbols(TypeId ty1, TypeId ty2, std::optional<std::string> symbol1, std::optional<std::string> symbol2);
/** /**
* True if ty is a "simple" type, i.e. cannot contain types. * True if ty is a "simple" type, i.e. cannot contain types.

View file

@ -2,6 +2,7 @@
#pragma once #pragma once
#include "Luau/Location.h" #include "Luau/Location.h"
#include "Luau/NotNull.h"
#include "Luau/Type.h" #include "Luau/Type.h"
#include "Luau/Variant.h" #include "Luau/Variant.h"
@ -432,7 +433,7 @@ std::string toString(const TypeError& error, TypeErrorToStringOptions options);
bool containsParseErrorName(const TypeError& error); bool containsParseErrorName(const TypeError& error);
// Copy any types named in the error into destArena. // Copy any types named in the error into destArena.
void copyErrors(ErrorVec& errors, struct TypeArena& destArena); void copyErrors(ErrorVec& errors, struct TypeArena& destArena, NotNull<BuiltinTypes> builtinTypes);
// Internal Compiler Error // Internal Compiler Error
struct InternalErrorReporter struct InternalErrorReporter

View file

@ -1,6 +1,7 @@
// 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
#pragma once #pragma once
#include "Luau/LinterConfig.h"
#include "Luau/Location.h" #include "Luau/Location.h"
#include <memory> #include <memory>
@ -15,86 +16,15 @@ class AstStat;
class AstNameTable; class AstNameTable;
struct TypeChecker; struct TypeChecker;
struct Module; struct Module;
struct HotComment;
using ScopePtr = std::shared_ptr<struct Scope>; using ScopePtr = std::shared_ptr<struct Scope>;
struct LintWarning
{
// Make sure any new lint codes are documented here: https://luau-lang.org/lint
// Note that in Studio, the active set of lint warnings is determined by FStringStudioLuauLints
enum Code
{
Code_Unknown = 0,
Code_UnknownGlobal = 1, // superseded by type checker
Code_DeprecatedGlobal = 2,
Code_GlobalUsedAsLocal = 3,
Code_LocalShadow = 4, // disabled in Studio
Code_SameLineStatement = 5, // disabled in Studio
Code_MultiLineStatement = 6,
Code_LocalUnused = 7, // disabled in Studio
Code_FunctionUnused = 8, // disabled in Studio
Code_ImportUnused = 9, // disabled in Studio
Code_BuiltinGlobalWrite = 10,
Code_PlaceholderRead = 11,
Code_UnreachableCode = 12,
Code_UnknownType = 13,
Code_ForRange = 14,
Code_UnbalancedAssignment = 15,
Code_ImplicitReturn = 16, // disabled in Studio, superseded by type checker in strict mode
Code_DuplicateLocal = 17,
Code_FormatString = 18,
Code_TableLiteral = 19,
Code_UninitializedLocal = 20,
Code_DuplicateFunction = 21,
Code_DeprecatedApi = 22,
Code_TableOperations = 23,
Code_DuplicateCondition = 24,
Code_MisleadingAndOr = 25,
Code_CommentDirective = 26,
Code_IntegerParsing = 27,
Code_ComparisonPrecedence = 28,
Code__Count
};
Code code;
Location location;
std::string text;
static const char* getName(Code code);
static Code parseName(const char* name);
static uint64_t parseMask(const std::vector<HotComment>& hotcomments);
};
struct LintResult struct LintResult
{ {
std::vector<LintWarning> errors; std::vector<LintWarning> errors;
std::vector<LintWarning> warnings; std::vector<LintWarning> warnings;
}; };
struct LintOptions
{
uint64_t warningMask = 0;
void enableWarning(LintWarning::Code code)
{
warningMask |= 1ull << code;
}
void disableWarning(LintWarning::Code code)
{
warningMask &= ~(1ull << code);
}
bool isEnabled(LintWarning::Code code) const
{
return 0 != (warningMask & (1ull << code));
}
void setDefaults();
};
std::vector<LintWarning> lint(AstStat* root, const AstNameTable& names, const ScopePtr& env, const Module* module, std::vector<LintWarning> lint(AstStat* root, const AstNameTable& names, const ScopePtr& env, const Module* module,
const std::vector<HotComment>& hotcomments, const LintOptions& options); const std::vector<HotComment>& hotcomments, const LintOptions& options);

View file

@ -0,0 +1,121 @@
// This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details
#pragma once
#include "Luau/Location.h"
#include <string>
#include <vector>
namespace Luau
{
struct HotComment;
struct LintWarning
{
// Make sure any new lint codes are documented here: https://luau-lang.org/lint
// Note that in Studio, the active set of lint warnings is determined by FStringStudioLuauLints
enum Code
{
Code_Unknown = 0,
Code_UnknownGlobal = 1, // superseded by type checker
Code_DeprecatedGlobal = 2,
Code_GlobalUsedAsLocal = 3,
Code_LocalShadow = 4, // disabled in Studio
Code_SameLineStatement = 5, // disabled in Studio
Code_MultiLineStatement = 6,
Code_LocalUnused = 7, // disabled in Studio
Code_FunctionUnused = 8, // disabled in Studio
Code_ImportUnused = 9, // disabled in Studio
Code_BuiltinGlobalWrite = 10,
Code_PlaceholderRead = 11,
Code_UnreachableCode = 12,
Code_UnknownType = 13,
Code_ForRange = 14,
Code_UnbalancedAssignment = 15,
Code_ImplicitReturn = 16, // disabled in Studio, superseded by type checker in strict mode
Code_DuplicateLocal = 17,
Code_FormatString = 18,
Code_TableLiteral = 19,
Code_UninitializedLocal = 20,
Code_DuplicateFunction = 21,
Code_DeprecatedApi = 22,
Code_TableOperations = 23,
Code_DuplicateCondition = 24,
Code_MisleadingAndOr = 25,
Code_CommentDirective = 26,
Code_IntegerParsing = 27,
Code_ComparisonPrecedence = 28,
Code__Count
};
Code code;
Location location;
std::string text;
static const char* getName(Code code);
static Code parseName(const char* name);
static uint64_t parseMask(const std::vector<HotComment>& hotcomments);
};
struct LintOptions
{
uint64_t warningMask = 0;
void enableWarning(LintWarning::Code code)
{
warningMask |= 1ull << code;
}
void disableWarning(LintWarning::Code code)
{
warningMask &= ~(1ull << code);
}
bool isEnabled(LintWarning::Code code) const
{
return 0 != (warningMask & (1ull << code));
}
void setDefaults();
};
// clang-format off
static const char* kWarningNames[] = {
"Unknown",
"UnknownGlobal",
"DeprecatedGlobal",
"GlobalUsedAsLocal",
"LocalShadow",
"SameLineStatement",
"MultiLineStatement",
"LocalUnused",
"FunctionUnused",
"ImportUnused",
"BuiltinGlobalWrite",
"PlaceholderRead",
"UnreachableCode",
"UnknownType",
"ForRange",
"UnbalancedAssignment",
"ImplicitReturn",
"DuplicateLocal",
"FormatString",
"TableLiteral",
"UninitializedLocal",
"DuplicateFunction",
"DeprecatedApi",
"TableOperations",
"DuplicateCondition",
"MisleadingAndOr",
"CommentDirective",
"IntegerParsing",
"ComparisonPrecedence",
};
// clang-format on
static_assert(std::size(kWarningNames) == unsigned(LintWarning::Code__Count), "did you forget to add warning to the list?");
} // namespace Luau

View file

@ -147,9 +147,6 @@ struct Tarjan
void visitEdge(int index, int parentIndex); void visitEdge(int index, int parentIndex);
void visitSCC(int index); void visitSCC(int index);
TarjanResult loop_DEPRECATED();
void visitSCC_DEPRECATED(int index);
// Each subclass can decide to ignore some nodes. // Each subclass can decide to ignore some nodes.
virtual bool ignoreChildren(TypeId ty) virtual bool ignoreChildren(TypeId ty)
{ {
@ -178,13 +175,6 @@ struct Tarjan
virtual bool isDirty(TypePackId tp) = 0; virtual bool isDirty(TypePackId tp) = 0;
virtual void foundDirty(TypeId ty) = 0; virtual void foundDirty(TypeId ty) = 0;
virtual void foundDirty(TypePackId tp) = 0; virtual void foundDirty(TypePackId tp) = 0;
// TODO: remove with FFlagLuauTarjanSingleArr
std::vector<TypeId> indexToType;
std::vector<TypePackId> indexToPack;
std::vector<bool> onStack;
std::vector<int> lowlink;
std::vector<bool> dirty;
}; };
// And finally substitution, which finds all the reachable dirty vertices // And finally substitution, which finds all the reachable dirty vertices

View file

@ -1,6 +1,7 @@
// 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/Clone.h" #include "Luau/Clone.h"
#include "Luau/NotNull.h"
#include "Luau/RecursionCounter.h" #include "Luau/RecursionCounter.h"
#include "Luau/TxnLog.h" #include "Luau/TxnLog.h"
#include "Luau/TypePack.h" #include "Luau/TypePack.h"
@ -13,12 +14,424 @@ LUAU_FASTFLAG(DebugLuauDeferredConstraintResolution)
LUAU_FASTINTVARIABLE(LuauTypeCloneRecursionLimit, 300) LUAU_FASTINTVARIABLE(LuauTypeCloneRecursionLimit, 300)
LUAU_FASTFLAGVARIABLE(LuauCloneCyclicUnions, false) LUAU_FASTFLAGVARIABLE(LuauCloneCyclicUnions, false)
LUAU_FASTFLAGVARIABLE(LuauStacklessTypeClone, false)
LUAU_FASTINTVARIABLE(LuauTypeCloneIterationLimit, 100'000)
namespace Luau namespace Luau
{ {
namespace namespace
{ {
using Kind = Variant<TypeId, TypePackId>;
template<typename T>
const T* get(const Kind& kind)
{
return get_if<T>(&kind);
}
class TypeCloner2
{
NotNull<TypeArena> arena;
NotNull<BuiltinTypes> builtinTypes;
// A queue of kinds where we cloned it, but whose interior types hasn't
// been updated to point to new clones. Once all of its interior types
// has been updated, it gets removed from the queue.
std::vector<Kind> queue;
NotNull<SeenTypes> types;
NotNull<SeenTypePacks> packs;
int steps = 0;
public:
TypeCloner2(NotNull<TypeArena> arena, NotNull<BuiltinTypes> builtinTypes, NotNull<SeenTypes> types, NotNull<SeenTypePacks> packs)
: arena(arena)
, builtinTypes(builtinTypes)
, types(types)
, packs(packs)
{
}
TypeId clone(TypeId ty)
{
shallowClone(ty);
run();
if (hasExceededIterationLimit())
{
TypeId error = builtinTypes->errorRecoveryType();
(*types)[ty] = error;
return error;
}
return find(ty).value_or(builtinTypes->errorRecoveryType());
}
TypePackId clone(TypePackId tp)
{
shallowClone(tp);
run();
if (hasExceededIterationLimit())
{
TypePackId error = builtinTypes->errorRecoveryTypePack();
(*packs)[tp] = error;
return error;
}
return find(tp).value_or(builtinTypes->errorRecoveryTypePack());
}
private:
bool hasExceededIterationLimit() const
{
if (FInt::LuauTypeCloneIterationLimit == 0)
return false;
return steps + queue.size() >= size_t(FInt::LuauTypeCloneIterationLimit);
}
void run()
{
while (!queue.empty())
{
++steps;
if (hasExceededIterationLimit())
break;
Kind kind = queue.back();
queue.pop_back();
if (find(kind))
continue;
cloneChildren(kind);
}
}
std::optional<TypeId> find(TypeId ty) const
{
if (auto it = types->find(ty); it != types->end())
return it->second;
return std::nullopt;
}
std::optional<TypePackId> find(TypePackId tp) const
{
if (auto it = packs->find(tp); it != packs->end())
return it->second;
return std::nullopt;
}
std::optional<Kind> find(Kind kind) const
{
if (auto ty = get<TypeId>(kind))
return find(*ty);
else if (auto tp = get<TypePackId>(kind))
return find(*tp);
else
{
LUAU_ASSERT(!"Unknown kind?");
return std::nullopt;
}
}
private:
TypeId shallowClone(TypeId ty)
{
if (auto clone = find(ty))
return *clone;
else if (ty->persistent)
return ty;
// We want to [`Luau::follow`] but without forcing the expansion of [`LazyType`]s.
TypeId target = nullptr;
if (auto bt = get<BoundType>(ty))
target = bt->boundTo;
else if (auto tt = get<TableType>(ty); tt && tt->boundTo)
target = *tt->boundTo;
else
{
target = arena->addType(ty->ty);
asMutable(target)->documentationSymbol = ty->documentationSymbol;
}
LUAU_ASSERT(target);
(*types)[ty] = target;
queue.push_back(target);
return target;
}
TypePackId shallowClone(TypePackId tp)
{
if (auto clone = find(tp))
return *clone;
else if (tp->persistent)
return tp;
TypePackId target;
if (auto btp = get<BoundTypePack>(tp))
target = btp->boundTo;
else
target = arena->addTypePack(tp->ty);
LUAU_ASSERT(target);
(*packs)[tp] = target;
queue.push_back(target);
return target;
}
Property shallowClone(const Property& p)
{
if (FFlag::DebugLuauReadWriteProperties)
{
std::optional<TypeId> cloneReadTy;
if (auto ty = p.readType())
cloneReadTy = shallowClone(*ty);
std::optional<TypeId> cloneWriteTy;
if (auto ty = p.writeType())
cloneWriteTy = shallowClone(*ty);
std::optional<Property> cloned = Property::create(cloneReadTy, cloneWriteTy);
LUAU_ASSERT(cloned);
cloned->deprecated = p.deprecated;
cloned->deprecatedSuggestion = p.deprecatedSuggestion;
cloned->location = p.location;
cloned->tags = p.tags;
cloned->documentationSymbol = p.documentationSymbol;
return *cloned;
}
else
{
return Property{
shallowClone(p.type()),
p.deprecated,
p.deprecatedSuggestion,
p.location,
p.tags,
p.documentationSymbol,
};
}
}
void cloneChildren(TypeId ty)
{
return visit(
[&](auto&& t) {
return cloneChildren(&t);
},
asMutable(ty)->ty);
}
void cloneChildren(TypePackId tp)
{
return visit(
[&](auto&& t) {
return cloneChildren(&t);
},
asMutable(tp)->ty);
}
void cloneChildren(Kind kind)
{
if (auto ty = get<TypeId>(kind))
return cloneChildren(*ty);
else if (auto tp = get<TypePackId>(kind))
return cloneChildren(*tp);
else
LUAU_ASSERT(!"Item holds neither TypeId nor TypePackId when enqueuing its children?");
}
// ErrorType and ErrorTypePack is an alias to this type.
void cloneChildren(Unifiable::Error* t)
{
// noop.
}
void cloneChildren(BoundType* t)
{
t->boundTo = shallowClone(t->boundTo);
}
void cloneChildren(FreeType* t)
{
// TODO: clone lower and upper bounds.
// TODO: In the new solver, we should ice.
}
void cloneChildren(GenericType* t)
{
// TOOD: clone upper bounds.
}
void cloneChildren(PrimitiveType* t)
{
// noop.
}
void cloneChildren(BlockedType* t)
{
// TODO: In the new solver, we should ice.
}
void cloneChildren(PendingExpansionType* t)
{
// TODO: In the new solver, we should ice.
}
void cloneChildren(SingletonType* t)
{
// noop.
}
void cloneChildren(FunctionType* t)
{
for (TypeId& g : t->generics)
g = shallowClone(g);
for (TypePackId& gp : t->genericPacks)
gp = shallowClone(gp);
t->argTypes = shallowClone(t->argTypes);
t->retTypes = shallowClone(t->retTypes);
}
void cloneChildren(TableType* t)
{
if (t->indexer)
{
t->indexer->indexType = shallowClone(t->indexer->indexType);
t->indexer->indexResultType = shallowClone(t->indexer->indexResultType);
}
for (auto& [_, p] : t->props)
p = shallowClone(p);
for (TypeId& ty : t->instantiatedTypeParams)
ty = shallowClone(ty);
for (TypePackId& tp : t->instantiatedTypePackParams)
tp = shallowClone(tp);
}
void cloneChildren(MetatableType* t)
{
t->table = shallowClone(t->table);
t->metatable = shallowClone(t->metatable);
}
void cloneChildren(ClassType* t)
{
for (auto& [_, p] : t->props)
p = shallowClone(p);
if (t->parent)
t->parent = shallowClone(*t->parent);
if (t->metatable)
t->metatable = shallowClone(*t->metatable);
if (t->indexer)
{
t->indexer->indexType = shallowClone(t->indexer->indexType);
t->indexer->indexResultType = shallowClone(t->indexer->indexResultType);
}
}
void cloneChildren(AnyType* t)
{
// noop.
}
void cloneChildren(UnionType* t)
{
for (TypeId& ty : t->options)
ty = shallowClone(ty);
}
void cloneChildren(IntersectionType* t)
{
for (TypeId& ty : t->parts)
ty = shallowClone(ty);
}
void cloneChildren(LazyType* t)
{
if (auto unwrapped = t->unwrapped.load())
t->unwrapped.store(shallowClone(unwrapped));
}
void cloneChildren(UnknownType* t)
{
// noop.
}
void cloneChildren(NeverType* t)
{
// noop.
}
void cloneChildren(NegationType* t)
{
t->ty = shallowClone(t->ty);
}
void cloneChildren(TypeFamilyInstanceType* t)
{
// TODO: In the new solver, we should ice.
}
void cloneChildren(FreeTypePack* t)
{
// TODO: clone lower and upper bounds.
// TODO: In the new solver, we should ice.
}
void cloneChildren(GenericTypePack* t)
{
// TOOD: clone upper bounds.
}
void cloneChildren(BlockedTypePack* t)
{
// TODO: In the new solver, we should ice.
}
void cloneChildren(BoundTypePack* t)
{
t->boundTo = shallowClone(t->boundTo);
}
void cloneChildren(VariadicTypePack* t)
{
t->ty = shallowClone(t->ty);
}
void cloneChildren(TypePack* t)
{
for (TypeId& ty : t->head)
ty = shallowClone(ty);
if (t->tail)
t->tail = shallowClone(*t->tail);
}
void cloneChildren(TypeFamilyInstanceTypePack* t)
{
// TODO: In the new solver, we should ice.
}
};
} // namespace
namespace
{
Property clone(const Property& prop, TypeArena& dest, CloneState& cloneState) Property clone(const Property& prop, TypeArena& dest, CloneState& cloneState)
{ {
if (FFlag::DebugLuauReadWriteProperties) if (FFlag::DebugLuauReadWriteProperties)
@ -470,6 +883,13 @@ TypePackId clone(TypePackId tp, TypeArena& dest, CloneState& cloneState)
if (tp->persistent) if (tp->persistent)
return tp; return tp;
if (FFlag::LuauStacklessTypeClone)
{
TypeCloner2 cloner{NotNull{&dest}, cloneState.builtinTypes, NotNull{&cloneState.seenTypes}, NotNull{&cloneState.seenTypePacks}};
return cloner.clone(tp);
}
else
{
RecursionLimiter _ra(&cloneState.recursionCount, FInt::LuauTypeCloneRecursionLimit); RecursionLimiter _ra(&cloneState.recursionCount, FInt::LuauTypeCloneRecursionLimit);
TypePackId& res = cloneState.seenTypePacks[tp]; TypePackId& res = cloneState.seenTypePacks[tp];
@ -482,12 +902,20 @@ TypePackId clone(TypePackId tp, TypeArena& dest, CloneState& cloneState)
return res; return res;
} }
}
TypeId clone(TypeId typeId, TypeArena& dest, CloneState& cloneState) TypeId clone(TypeId typeId, TypeArena& dest, CloneState& cloneState)
{ {
if (typeId->persistent) if (typeId->persistent)
return typeId; return typeId;
if (FFlag::LuauStacklessTypeClone)
{
TypeCloner2 cloner{NotNull{&dest}, cloneState.builtinTypes, NotNull{&cloneState.seenTypes}, NotNull{&cloneState.seenTypePacks}};
return cloner.clone(typeId);
}
else
{
RecursionLimiter _ra(&cloneState.recursionCount, FInt::LuauTypeCloneRecursionLimit); RecursionLimiter _ra(&cloneState.recursionCount, FInt::LuauTypeCloneRecursionLimit);
TypeId& res = cloneState.seenTypes[typeId]; TypeId& res = cloneState.seenTypes[typeId];
@ -506,8 +934,37 @@ TypeId clone(TypeId typeId, TypeArena& dest, CloneState& cloneState)
return res; return res;
} }
}
TypeFun clone(const TypeFun& typeFun, TypeArena& dest, CloneState& cloneState) TypeFun clone(const TypeFun& typeFun, TypeArena& dest, CloneState& cloneState)
{
if (FFlag::LuauStacklessTypeClone)
{
TypeCloner2 cloner{NotNull{&dest}, cloneState.builtinTypes, NotNull{&cloneState.seenTypes}, NotNull{&cloneState.seenTypePacks}};
TypeFun copy = typeFun;
for (auto& param : copy.typeParams)
{
param.ty = cloner.clone(param.ty);
if (param.defaultValue)
param.defaultValue = cloner.clone(*param.defaultValue);
}
for (auto& param : copy.typePackParams)
{
param.tp = cloner.clone(param.tp);
if (param.defaultValue)
param.defaultValue = cloner.clone(*param.defaultValue);
}
copy.type = cloner.clone(copy.type);
return copy;
}
else
{ {
TypeFun result; TypeFun result;
@ -537,5 +994,6 @@ TypeFun clone(const TypeFun& typeFun, TypeArena& dest, CloneState& cloneState)
return result; return result;
} }
}
} // namespace Luau } // namespace Luau

View file

@ -107,15 +107,19 @@ std::string DiffPath::toString(bool prependDot) const
} }
return pathStr; return pathStr;
} }
std::string DiffError::toStringALeaf(std::string rootName, const DiffPathNodeLeaf& leaf, const DiffPathNodeLeaf& otherLeaf) const std::string DiffError::toStringALeaf(std::string rootName, const DiffPathNodeLeaf& leaf, const DiffPathNodeLeaf& otherLeaf, bool multiLine) const
{ {
std::string conditionalNewline = multiLine ? "\n" : " ";
std::string conditionalIndent = multiLine ? " " : "";
std::string pathStr{rootName + diffPath.toString(true)}; std::string pathStr{rootName + diffPath.toString(true)};
switch (kind) switch (kind)
{ {
case DiffError::Kind::Normal: case DiffError::Kind::Normal:
{ {
checkNonMissingPropertyLeavesHaveNulloptTableProperty(); checkNonMissingPropertyLeavesHaveNulloptTableProperty();
return pathStr + " has type " + Luau::toString(*leaf.ty); return pathStr + conditionalNewline
+ "has type" + conditionalNewline
+ conditionalIndent + Luau::toString(*leaf.ty);
} }
case DiffError::Kind::MissingTableProperty: case DiffError::Kind::MissingTableProperty:
{ {
@ -123,13 +127,17 @@ std::string DiffError::toStringALeaf(std::string rootName, const DiffPathNodeLea
{ {
if (!leaf.tableProperty.has_value()) if (!leaf.tableProperty.has_value())
throw InternalCompilerError{"leaf.tableProperty is nullopt"}; throw InternalCompilerError{"leaf.tableProperty is nullopt"};
return pathStr + "." + *leaf.tableProperty + " has type " + Luau::toString(*leaf.ty); return pathStr + "." + *leaf.tableProperty + conditionalNewline
+ "has type" + conditionalNewline
+ conditionalIndent + Luau::toString(*leaf.ty);
} }
else if (otherLeaf.ty.has_value()) else if (otherLeaf.ty.has_value())
{ {
if (!otherLeaf.tableProperty.has_value()) if (!otherLeaf.tableProperty.has_value())
throw InternalCompilerError{"otherLeaf.tableProperty is nullopt"}; throw InternalCompilerError{"otherLeaf.tableProperty is nullopt"};
return pathStr + " is missing the property " + *otherLeaf.tableProperty; return pathStr + conditionalNewline
+ "is missing the property" + conditionalNewline
+ conditionalIndent + *otherLeaf.tableProperty;
} }
throw InternalCompilerError{"Both leaf.ty and otherLeaf.ty is nullopt"}; throw InternalCompilerError{"Both leaf.ty and otherLeaf.ty is nullopt"};
} }
@ -140,11 +148,15 @@ std::string DiffError::toStringALeaf(std::string rootName, const DiffPathNodeLea
{ {
if (!leaf.unionIndex.has_value()) if (!leaf.unionIndex.has_value())
throw InternalCompilerError{"leaf.unionIndex is nullopt"}; throw InternalCompilerError{"leaf.unionIndex is nullopt"};
return pathStr + " is a union containing type " + Luau::toString(*leaf.ty); return pathStr + conditionalNewline
+ "is a union containing type" + conditionalNewline
+ conditionalIndent + Luau::toString(*leaf.ty);
} }
else if (otherLeaf.ty.has_value()) else if (otherLeaf.ty.has_value())
{ {
return pathStr + " is a union missing type " + Luau::toString(*otherLeaf.ty); return pathStr + conditionalNewline
+ "is a union missing type" + conditionalNewline
+ conditionalIndent + Luau::toString(*otherLeaf.ty);
} }
throw InternalCompilerError{"Both leaf.ty and otherLeaf.ty is nullopt"}; throw InternalCompilerError{"Both leaf.ty and otherLeaf.ty is nullopt"};
} }
@ -157,11 +169,15 @@ std::string DiffError::toStringALeaf(std::string rootName, const DiffPathNodeLea
{ {
if (!leaf.unionIndex.has_value()) if (!leaf.unionIndex.has_value())
throw InternalCompilerError{"leaf.unionIndex is nullopt"}; throw InternalCompilerError{"leaf.unionIndex is nullopt"};
return pathStr + " is an intersection containing type " + Luau::toString(*leaf.ty); return pathStr + conditionalNewline
+ "is an intersection containing type" + conditionalNewline
+ conditionalIndent + Luau::toString(*leaf.ty);
} }
else if (otherLeaf.ty.has_value()) else if (otherLeaf.ty.has_value())
{ {
return pathStr + " is an intersection missing type " + Luau::toString(*otherLeaf.ty); return pathStr + conditionalNewline
+ "is an intersection missing type" + conditionalNewline
+ conditionalIndent + Luau::toString(*otherLeaf.ty);
} }
throw InternalCompilerError{"Both leaf.ty and otherLeaf.ty is nullopt"}; throw InternalCompilerError{"Both leaf.ty and otherLeaf.ty is nullopt"};
} }
@ -169,13 +185,15 @@ std::string DiffError::toStringALeaf(std::string rootName, const DiffPathNodeLea
{ {
if (!leaf.minLength.has_value()) if (!leaf.minLength.has_value())
throw InternalCompilerError{"leaf.minLength is nullopt"}; throw InternalCompilerError{"leaf.minLength is nullopt"};
return pathStr + " takes " + std::to_string(*leaf.minLength) + (leaf.isVariadic ? " or more" : "") + " arguments"; return pathStr + conditionalNewline
+ "takes " + std::to_string(*leaf.minLength) + (leaf.isVariadic ? " or more" : "") + " arguments";
} }
case DiffError::Kind::LengthMismatchInFnRets: case DiffError::Kind::LengthMismatchInFnRets:
{ {
if (!leaf.minLength.has_value()) if (!leaf.minLength.has_value())
throw InternalCompilerError{"leaf.minLength is nullopt"}; throw InternalCompilerError{"leaf.minLength is nullopt"};
return pathStr + " returns " + std::to_string(*leaf.minLength) + (leaf.isVariadic ? " or more" : "") + " values"; return pathStr + conditionalNewline
+ "returns " + std::to_string(*leaf.minLength) + (leaf.isVariadic ? " or more" : "") + " values";
} }
default: default:
{ {
@ -190,8 +208,11 @@ void DiffError::checkNonMissingPropertyLeavesHaveNulloptTableProperty() const
throw InternalCompilerError{"Non-MissingProperty DiffError should have nullopt tableProperty in both leaves"}; throw InternalCompilerError{"Non-MissingProperty DiffError should have nullopt tableProperty in both leaves"};
} }
std::string getDevFixFriendlyName(TypeId ty) std::string getDevFixFriendlyName(const std::optional<std::string>& maybeSymbol, TypeId ty)
{ {
if (maybeSymbol.has_value())
return *maybeSymbol;
if (auto table = get<TableType>(ty)) if (auto table = get<TableType>(ty))
{ {
if (table->name.has_value()) if (table->name.has_value())
@ -206,27 +227,39 @@ std::string getDevFixFriendlyName(TypeId ty)
return *metatable->syntheticName; return *metatable->syntheticName;
} }
} }
// else if (auto primitive = get<PrimitiveType>(ty))
//{
// return "<unlabeled-symbol>";
//}
return "<unlabeled-symbol>"; return "<unlabeled-symbol>";
} }
std::string DiffError::toString() const std::string DifferEnvironment::getDevFixFriendlyNameLeft() const
{ {
return getDevFixFriendlyName(externalSymbolLeft, rootLeft);
}
std::string DifferEnvironment::getDevFixFriendlyNameRight() const
{
return getDevFixFriendlyName(externalSymbolRight, rootRight);
}
std::string DiffError::toString(bool multiLine) const
{
std::string conditionalNewline = multiLine ? "\n" : " ";
std::string conditionalIndent = multiLine ? " " : "";
switch (kind) switch (kind)
{ {
case DiffError::Kind::IncompatibleGeneric: case DiffError::Kind::IncompatibleGeneric:
{ {
std::string diffPathStr{diffPath.toString(true)}; std::string diffPathStr{diffPath.toString(true)};
return "DiffError: these two types are not equal because the left generic at " + leftRootName + diffPathStr + return "DiffError: these two types are not equal because the left generic at" + conditionalNewline
" cannot be the same type parameter as the right generic at " + rightRootName + diffPathStr; + conditionalIndent + leftRootName + diffPathStr + conditionalNewline
+ "cannot be the same type parameter as the right generic at" + conditionalNewline
+ conditionalIndent + rightRootName + diffPathStr;
} }
default: default:
{ {
return "DiffError: these two types are not equal because the left type at " + toStringALeaf(leftRootName, left, right) + return "DiffError: these two types are not equal because the left type at" + conditionalNewline
", while the right type at " + toStringALeaf(rightRootName, right, left); + conditionalIndent + toStringALeaf(leftRootName, left, right, multiLine) + "," + conditionalNewline +
"while the right type at" + conditionalNewline
+ conditionalIndent + toStringALeaf(rightRootName, right, left, multiLine);
} }
} }
} }
@ -296,8 +329,8 @@ static DifferResult diffTable(DifferEnvironment& env, TypeId left, TypeId right)
DiffError::Kind::MissingTableProperty, DiffError::Kind::MissingTableProperty,
DiffPathNodeLeaf::detailsTableProperty(value.type(), field), DiffPathNodeLeaf::detailsTableProperty(value.type(), field),
DiffPathNodeLeaf::nullopts(), DiffPathNodeLeaf::nullopts(),
getDevFixFriendlyName(env.rootLeft), env.getDevFixFriendlyNameLeft(),
getDevFixFriendlyName(env.rootRight), env.getDevFixFriendlyNameRight(),
}}; }};
} }
} }
@ -307,8 +340,7 @@ static DifferResult diffTable(DifferEnvironment& env, TypeId left, TypeId right)
{ {
// right has a field the left doesn't // right has a field the left doesn't
return DifferResult{DiffError{DiffError::Kind::MissingTableProperty, DiffPathNodeLeaf::nullopts(), return DifferResult{DiffError{DiffError::Kind::MissingTableProperty, DiffPathNodeLeaf::nullopts(),
DiffPathNodeLeaf::detailsTableProperty(value.type(), field), getDevFixFriendlyName(env.rootLeft), DiffPathNodeLeaf::detailsTableProperty(value.type(), field), env.getDevFixFriendlyNameLeft(), env.getDevFixFriendlyNameRight()}};
getDevFixFriendlyName(env.rootRight)}};
} }
} }
// left and right have the same set of keys // left and right have the same set of keys
@ -360,8 +392,8 @@ static DifferResult diffPrimitive(DifferEnvironment& env, TypeId left, TypeId ri
DiffError::Kind::Normal, DiffError::Kind::Normal,
DiffPathNodeLeaf::detailsNormal(left), DiffPathNodeLeaf::detailsNormal(left),
DiffPathNodeLeaf::detailsNormal(right), DiffPathNodeLeaf::detailsNormal(right),
getDevFixFriendlyName(env.rootLeft), env.getDevFixFriendlyNameLeft(),
getDevFixFriendlyName(env.rootRight), env.getDevFixFriendlyNameRight(),
}}; }};
} }
return DifferResult{}; return DifferResult{};
@ -380,8 +412,8 @@ static DifferResult diffSingleton(DifferEnvironment& env, TypeId left, TypeId ri
DiffError::Kind::Normal, DiffError::Kind::Normal,
DiffPathNodeLeaf::detailsNormal(left), DiffPathNodeLeaf::detailsNormal(left),
DiffPathNodeLeaf::detailsNormal(right), DiffPathNodeLeaf::detailsNormal(right),
getDevFixFriendlyName(env.rootLeft), env.getDevFixFriendlyNameLeft(),
getDevFixFriendlyName(env.rootRight), env.getDevFixFriendlyNameRight(),
}}; }};
} }
return DifferResult{}; return DifferResult{};
@ -419,8 +451,8 @@ static DifferResult diffGeneric(DifferEnvironment& env, TypeId left, TypeId righ
DiffError::Kind::IncompatibleGeneric, DiffError::Kind::IncompatibleGeneric,
DiffPathNodeLeaf::nullopts(), DiffPathNodeLeaf::nullopts(),
DiffPathNodeLeaf::nullopts(), DiffPathNodeLeaf::nullopts(),
getDevFixFriendlyName(env.rootLeft), env.getDevFixFriendlyNameLeft(),
getDevFixFriendlyName(env.rootRight), env.getDevFixFriendlyNameRight(),
}}; }};
} }
@ -432,8 +464,8 @@ static DifferResult diffGeneric(DifferEnvironment& env, TypeId left, TypeId righ
DiffError::Kind::IncompatibleGeneric, DiffError::Kind::IncompatibleGeneric,
DiffPathNodeLeaf::nullopts(), DiffPathNodeLeaf::nullopts(),
DiffPathNodeLeaf::nullopts(), DiffPathNodeLeaf::nullopts(),
getDevFixFriendlyName(env.rootLeft), env.getDevFixFriendlyNameLeft(),
getDevFixFriendlyName(env.rootRight), env.getDevFixFriendlyNameRight(),
}}; }};
} }
@ -468,8 +500,8 @@ static DifferResult diffClass(DifferEnvironment& env, TypeId left, TypeId right)
DiffError::Kind::Normal, DiffError::Kind::Normal,
DiffPathNodeLeaf::detailsNormal(left), DiffPathNodeLeaf::detailsNormal(left),
DiffPathNodeLeaf::detailsNormal(right), DiffPathNodeLeaf::detailsNormal(right),
getDevFixFriendlyName(env.rootLeft), env.getDevFixFriendlyNameLeft(),
getDevFixFriendlyName(env.rootRight), env.getDevFixFriendlyNameRight(),
}}; }};
} }
@ -521,16 +553,16 @@ static DifferResult diffUnion(DifferEnvironment& env, TypeId left, TypeId right)
DiffError::Kind::MissingUnionMember, DiffError::Kind::MissingUnionMember,
DiffPathNodeLeaf::detailsUnionIndex(leftUnion->options[*findSeteqCexResult.mismatchIdx], *findSeteqCexResult.mismatchIdx), DiffPathNodeLeaf::detailsUnionIndex(leftUnion->options[*findSeteqCexResult.mismatchIdx], *findSeteqCexResult.mismatchIdx),
DiffPathNodeLeaf::nullopts(), DiffPathNodeLeaf::nullopts(),
getDevFixFriendlyName(env.rootLeft), env.getDevFixFriendlyNameLeft(),
getDevFixFriendlyName(env.rootRight), env.getDevFixFriendlyNameRight(),
}}; }};
else else
return DifferResult{DiffError{ return DifferResult{DiffError{
DiffError::Kind::MissingUnionMember, DiffError::Kind::MissingUnionMember,
DiffPathNodeLeaf::nullopts(), DiffPathNodeLeaf::nullopts(),
DiffPathNodeLeaf::detailsUnionIndex(rightUnion->options[*findSeteqCexResult.mismatchIdx], *findSeteqCexResult.mismatchIdx), DiffPathNodeLeaf::detailsUnionIndex(rightUnion->options[*findSeteqCexResult.mismatchIdx], *findSeteqCexResult.mismatchIdx),
getDevFixFriendlyName(env.rootLeft), env.getDevFixFriendlyNameLeft(),
getDevFixFriendlyName(env.rootRight), env.getDevFixFriendlyNameRight(),
}}; }};
} }
@ -554,16 +586,16 @@ static DifferResult diffIntersection(DifferEnvironment& env, TypeId left, TypeId
DiffError::Kind::MissingIntersectionMember, DiffError::Kind::MissingIntersectionMember,
DiffPathNodeLeaf::detailsUnionIndex(leftIntersection->parts[*findSeteqCexResult.mismatchIdx], *findSeteqCexResult.mismatchIdx), DiffPathNodeLeaf::detailsUnionIndex(leftIntersection->parts[*findSeteqCexResult.mismatchIdx], *findSeteqCexResult.mismatchIdx),
DiffPathNodeLeaf::nullopts(), DiffPathNodeLeaf::nullopts(),
getDevFixFriendlyName(env.rootLeft), env.getDevFixFriendlyNameLeft(),
getDevFixFriendlyName(env.rootRight), env.getDevFixFriendlyNameRight(),
}}; }};
else else
return DifferResult{DiffError{ return DifferResult{DiffError{
DiffError::Kind::MissingIntersectionMember, DiffError::Kind::MissingIntersectionMember,
DiffPathNodeLeaf::nullopts(), DiffPathNodeLeaf::nullopts(),
DiffPathNodeLeaf::detailsUnionIndex(rightIntersection->parts[*findSeteqCexResult.mismatchIdx], *findSeteqCexResult.mismatchIdx), DiffPathNodeLeaf::detailsUnionIndex(rightIntersection->parts[*findSeteqCexResult.mismatchIdx], *findSeteqCexResult.mismatchIdx),
getDevFixFriendlyName(env.rootLeft), env.getDevFixFriendlyNameLeft(),
getDevFixFriendlyName(env.rootRight), env.getDevFixFriendlyNameRight(),
}}; }};
} }
@ -583,8 +615,8 @@ static DifferResult diffUsingEnv(DifferEnvironment& env, TypeId left, TypeId rig
DiffError::Kind::Normal, DiffError::Kind::Normal,
DiffPathNodeLeaf::detailsNormal(left), DiffPathNodeLeaf::detailsNormal(left),
DiffPathNodeLeaf::detailsNormal(right), DiffPathNodeLeaf::detailsNormal(right),
getDevFixFriendlyName(env.rootLeft), env.getDevFixFriendlyNameLeft(),
getDevFixFriendlyName(env.rootRight), env.getDevFixFriendlyNameRight(),
}}; }};
} }
@ -753,8 +785,8 @@ static DifferResult diffCanonicalTpShape(DifferEnvironment& env, DiffError::Kind
possibleNonNormalErrorKind, possibleNonNormalErrorKind,
DiffPathNodeLeaf::detailsLength(int(left.first.size()), left.second.has_value()), DiffPathNodeLeaf::detailsLength(int(left.first.size()), left.second.has_value()),
DiffPathNodeLeaf::detailsLength(int(right.first.size()), right.second.has_value()), DiffPathNodeLeaf::detailsLength(int(right.first.size()), right.second.has_value()),
getDevFixFriendlyName(env.rootLeft), env.getDevFixFriendlyNameLeft(),
getDevFixFriendlyName(env.rootRight), env.getDevFixFriendlyNameRight(),
}}; }};
} }
@ -769,8 +801,8 @@ static DifferResult diffHandleFlattenedTail(DifferEnvironment& env, DiffError::K
DiffError::Kind::Normal, DiffError::Kind::Normal,
DiffPathNodeLeaf::detailsNormal(env.visitingBegin()->first), DiffPathNodeLeaf::detailsNormal(env.visitingBegin()->first),
DiffPathNodeLeaf::detailsNormal(env.visitingBegin()->second), DiffPathNodeLeaf::detailsNormal(env.visitingBegin()->second),
getDevFixFriendlyName(env.rootLeft), env.getDevFixFriendlyNameLeft(),
getDevFixFriendlyName(env.rootRight), env.getDevFixFriendlyNameRight(),
}}; }};
} }
@ -847,8 +879,8 @@ static DifferResult diffGenericTp(DifferEnvironment& env, TypePackId left, TypeP
DiffError::Kind::IncompatibleGeneric, DiffError::Kind::IncompatibleGeneric,
DiffPathNodeLeaf::nullopts(), DiffPathNodeLeaf::nullopts(),
DiffPathNodeLeaf::nullopts(), DiffPathNodeLeaf::nullopts(),
getDevFixFriendlyName(env.rootLeft), env.getDevFixFriendlyNameLeft(),
getDevFixFriendlyName(env.rootRight), env.getDevFixFriendlyNameRight(),
}}; }};
} }
@ -860,8 +892,8 @@ static DifferResult diffGenericTp(DifferEnvironment& env, TypePackId left, TypeP
DiffError::Kind::IncompatibleGeneric, DiffError::Kind::IncompatibleGeneric,
DiffPathNodeLeaf::nullopts(), DiffPathNodeLeaf::nullopts(),
DiffPathNodeLeaf::nullopts(), DiffPathNodeLeaf::nullopts(),
getDevFixFriendlyName(env.rootLeft), env.getDevFixFriendlyNameLeft(),
getDevFixFriendlyName(env.rootRight), env.getDevFixFriendlyNameRight(),
}}; }};
} }
@ -910,7 +942,13 @@ std::vector<std::pair<TypeId, TypeId>>::const_reverse_iterator DifferEnvironment
DifferResult diff(TypeId ty1, TypeId ty2) DifferResult diff(TypeId ty1, TypeId ty2)
{ {
DifferEnvironment differEnv{ty1, ty2}; DifferEnvironment differEnv{ty1, ty2, std::nullopt, std::nullopt};
return diffUsingEnv(differEnv, ty1, ty2);
}
DifferResult diffWithSymbols(TypeId ty1, TypeId ty2, std::optional<std::string> symbol1, std::optional<std::string> symbol2)
{
DifferEnvironment differEnv{ty1, ty2, symbol1, symbol2};
return diffUsingEnv(differEnv, ty1, ty2); return diffUsingEnv(differEnv, ty1, ty2);
} }

View file

@ -4,12 +4,16 @@
#include "Luau/Clone.h" #include "Luau/Clone.h"
#include "Luau/Common.h" #include "Luau/Common.h"
#include "Luau/FileResolver.h" #include "Luau/FileResolver.h"
#include "Luau/NotNull.h"
#include "Luau/StringUtils.h" #include "Luau/StringUtils.h"
#include "Luau/ToString.h" #include "Luau/ToString.h"
#include <optional>
#include <stdexcept> #include <stdexcept>
#include <type_traits> #include <type_traits>
LUAU_FASTFLAGVARIABLE(LuauIndentTypeMismatch, false)
LUAU_FASTINTVARIABLE(LuauIndentTypeMismatchMaxTypeLength, 10)
static std::string wrongNumberOfArgsString( static std::string wrongNumberOfArgsString(
size_t expectedCount, std::optional<size_t> maximumCount, size_t actualCount, const char* argPrefix = nullptr, bool isVariadic = false) size_t expectedCount, std::optional<size_t> maximumCount, size_t actualCount, const char* argPrefix = nullptr, bool isVariadic = false)
@ -66,6 +70,20 @@ struct ErrorConverter
std::string result; std::string result;
auto quote = [&](std::string s) {
return "'" + s + "'";
};
auto constructErrorMessage = [&](std::string givenType, std::string wantedType, std::optional<std::string> givenModule,
std::optional<std::string> wantedModule) -> std::string {
std::string given = givenModule ? quote(givenType) + " from " + quote(*givenModule) : quote(givenType);
std::string wanted = wantedModule ? quote(wantedType) + " from " + quote(*wantedModule) : quote(wantedType);
size_t luauIndentTypeMismatchMaxTypeLength = size_t(FInt::LuauIndentTypeMismatchMaxTypeLength);
if (givenType.length() <= luauIndentTypeMismatchMaxTypeLength || wantedType.length() <= luauIndentTypeMismatchMaxTypeLength)
return "Type " + given + " could not be converted into " + wanted;
return "Type\n " + given + "\ncould not be converted into\n " + wanted;
};
if (givenTypeName == wantedTypeName) if (givenTypeName == wantedTypeName)
{ {
if (auto givenDefinitionModule = getDefinitionModuleName(tm.givenType)) if (auto givenDefinitionModule = getDefinitionModuleName(tm.givenType))
@ -76,20 +94,31 @@ struct ErrorConverter
{ {
std::string givenModuleName = fileResolver->getHumanReadableModuleName(*givenDefinitionModule); std::string givenModuleName = fileResolver->getHumanReadableModuleName(*givenDefinitionModule);
std::string wantedModuleName = fileResolver->getHumanReadableModuleName(*wantedDefinitionModule); std::string wantedModuleName = fileResolver->getHumanReadableModuleName(*wantedDefinitionModule);
if (FFlag::LuauIndentTypeMismatch)
result = constructErrorMessage(givenTypeName, wantedTypeName, givenModuleName, wantedModuleName);
else
result = "Type '" + givenTypeName + "' from '" + givenModuleName + "' could not be converted into '" + wantedTypeName + result = "Type '" + givenTypeName + "' from '" + givenModuleName + "' could not be converted into '" + wantedTypeName +
"' from '" + wantedModuleName + "'"; "' from '" + wantedModuleName + "'";
} }
else else
{ {
result = "Type '" + givenTypeName + "' from '" + *givenDefinitionModule + "' could not be converted into '" + wantedTypeName + if (FFlag::LuauIndentTypeMismatch)
"' from '" + *wantedDefinitionModule + "'"; result = constructErrorMessage(givenTypeName, wantedTypeName, *givenDefinitionModule, *wantedDefinitionModule);
else
result = "Type '" + givenTypeName + "' from '" + *givenDefinitionModule + "' could not be converted into '" +
wantedTypeName + "' from '" + *wantedDefinitionModule + "'";
} }
} }
} }
} }
if (result.empty()) if (result.empty())
{
if (FFlag::LuauIndentTypeMismatch)
result = constructErrorMessage(givenTypeName, wantedTypeName, std::nullopt, std::nullopt);
else
result = "Type '" + givenTypeName + "' could not be converted into '" + wantedTypeName + "'"; result = "Type '" + givenTypeName + "' could not be converted into '" + wantedTypeName + "'";
}
if (tm.error) if (tm.error)
@ -97,7 +126,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 + (FFlag::LuauIndentTypeMismatch ? " \n" : " ");
result += Luau::toString(*tm.error, TypeErrorToStringOptions{fileResolver}); result += Luau::toString(*tm.error, TypeErrorToStringOptions{fileResolver});
} }
@ -845,7 +874,7 @@ bool containsParseErrorName(const TypeError& error)
} }
template<typename T> template<typename T>
void copyError(T& e, TypeArena& destArena, CloneState cloneState) void copyError(T& e, TypeArena& destArena, CloneState& cloneState)
{ {
auto clone = [&](auto&& ty) { auto clone = [&](auto&& ty) {
return ::Luau::clone(ty, destArena, cloneState); return ::Luau::clone(ty, destArena, cloneState);
@ -998,9 +1027,9 @@ void copyError(T& e, TypeArena& destArena, CloneState cloneState)
static_assert(always_false_v<T>, "Non-exhaustive type switch"); static_assert(always_false_v<T>, "Non-exhaustive type switch");
} }
void copyErrors(ErrorVec& errors, TypeArena& destArena) void copyErrors(ErrorVec& errors, TypeArena& destArena, NotNull<BuiltinTypes> builtinTypes)
{ {
CloneState cloneState; CloneState cloneState{builtinTypes};
auto visitErrorData = [&](auto&& e) { auto visitErrorData = [&](auto&& e) {
copyError(e, destArena, cloneState); copyError(e, destArena, cloneState);

View file

@ -35,7 +35,6 @@ LUAU_FASTINTVARIABLE(LuauAutocompleteCheckTimeoutMs, 100)
LUAU_FASTFLAGVARIABLE(DebugLuauDeferredConstraintResolution, false) LUAU_FASTFLAGVARIABLE(DebugLuauDeferredConstraintResolution, false)
LUAU_FASTFLAGVARIABLE(DebugLuauLogSolverToJson, false) LUAU_FASTFLAGVARIABLE(DebugLuauLogSolverToJson, false)
LUAU_FASTFLAGVARIABLE(DebugLuauReadWriteProperties, false) LUAU_FASTFLAGVARIABLE(DebugLuauReadWriteProperties, false)
LUAU_FASTFLAGVARIABLE(LuauTypecheckCancellation, false)
namespace Luau namespace Luau
{ {
@ -126,7 +125,7 @@ static ParseResult parseSourceForModule(std::string_view source, Luau::SourceMod
static void persistCheckedTypes(ModulePtr checkedModule, GlobalTypes& globals, ScopePtr targetScope, const std::string& packageName) static void persistCheckedTypes(ModulePtr checkedModule, GlobalTypes& globals, ScopePtr targetScope, const std::string& packageName)
{ {
CloneState cloneState; CloneState cloneState{globals.builtinTypes};
std::vector<TypeId> typesToPersist; std::vector<TypeId> typesToPersist;
typesToPersist.reserve(checkedModule->declaredGlobals.size() + checkedModule->exportedTypeBindings.size()); typesToPersist.reserve(checkedModule->declaredGlobals.size() + checkedModule->exportedTypeBindings.size());
@ -462,7 +461,7 @@ CheckResult Frontend::check(const ModuleName& name, std::optional<FrontendOption
checkResult.timeoutHits.push_back(item.name); checkResult.timeoutHits.push_back(item.name);
// If check was manually cancelled, do not return partial results // If check was manually cancelled, do not return partial results
if (FFlag::LuauTypecheckCancellation && item.module->cancelled) if (item.module->cancelled)
return {}; return {};
checkResult.errors.insert(checkResult.errors.end(), item.module->errors.begin(), item.module->errors.end()); checkResult.errors.insert(checkResult.errors.end(), item.module->errors.begin(), item.module->errors.end());
@ -635,7 +634,7 @@ std::vector<ModuleName> Frontend::checkQueuedModules(std::optional<FrontendOptio
if (item.exception) if (item.exception)
itemWithException = i; itemWithException = i;
if (FFlag::LuauTypecheckCancellation && item.module && item.module->cancelled) if (item.module && item.module->cancelled)
cancelled = true; cancelled = true;
if (itemWithException || cancelled) if (itemWithException || cancelled)
@ -677,7 +676,7 @@ std::vector<ModuleName> Frontend::checkQueuedModules(std::optional<FrontendOptio
if (remaining != 0 && processing == 0) if (remaining != 0 && processing == 0)
{ {
// Typechecking might have been cancelled by user, don't return partial results // Typechecking might have been cancelled by user, don't return partial results
if (FFlag::LuauTypecheckCancellation && cancelled) if (cancelled)
return {}; return {};
// We might have stopped because of a pending exception // We might have stopped because of a pending exception
@ -910,7 +909,6 @@ void Frontend::checkBuildQueueItem(BuildQueueItem& item)
else else
typeCheckLimits.unifierIterationLimit = std::nullopt; typeCheckLimits.unifierIterationLimit = std::nullopt;
if (FFlag::LuauTypecheckCancellation)
typeCheckLimits.cancellationToken = item.options.cancellationToken; typeCheckLimits.cancellationToken = item.options.cancellationToken;
ModulePtr moduleForAutocomplete = check(sourceModule, Mode::Strict, requireCycles, environmentScope, /*forAutocomplete*/ true, ModulePtr moduleForAutocomplete = check(sourceModule, Mode::Strict, requireCycles, environmentScope, /*forAutocomplete*/ true,
@ -932,7 +930,6 @@ void Frontend::checkBuildQueueItem(BuildQueueItem& item)
TypeCheckLimits typeCheckLimits; TypeCheckLimits typeCheckLimits;
if (FFlag::LuauTypecheckCancellation)
typeCheckLimits.cancellationToken = item.options.cancellationToken; typeCheckLimits.cancellationToken = item.options.cancellationToken;
ModulePtr module = check(sourceModule, mode, requireCycles, environmentScope, /*forAutocomplete*/ false, item.recordJsonLog, typeCheckLimits); ModulePtr module = check(sourceModule, mode, requireCycles, environmentScope, /*forAutocomplete*/ false, item.recordJsonLog, typeCheckLimits);
@ -969,7 +966,7 @@ void Frontend::checkBuildQueueItem(BuildQueueItem& item)
// copyErrors needs to allocate into interfaceTypes as it copies // copyErrors needs to allocate into interfaceTypes as it copies
// types out of internalTypes, so we unfreeze it here. // types out of internalTypes, so we unfreeze it here.
unfreeze(module->interfaceTypes); unfreeze(module->interfaceTypes);
copyErrors(module->errors, module->interfaceTypes); copyErrors(module->errors, module->interfaceTypes, builtinTypes);
freeze(module->interfaceTypes); freeze(module->interfaceTypes);
module->internalTypes.clear(); module->internalTypes.clear();
@ -1014,7 +1011,7 @@ void Frontend::checkBuildQueueItems(std::vector<BuildQueueItem>& items)
{ {
checkBuildQueueItem(item); checkBuildQueueItem(item);
if (FFlag::LuauTypecheckCancellation && item.module && item.module->cancelled) if (item.module && item.module->cancelled)
break; break;
recordItemResult(item); recordItemResult(item);
@ -1295,8 +1292,6 @@ ModulePtr Frontend::check(const SourceModule& sourceModule, Mode mode, std::vect
typeChecker.finishTime = typeCheckLimits.finishTime; typeChecker.finishTime = typeCheckLimits.finishTime;
typeChecker.instantiationChildLimit = typeCheckLimits.instantiationChildLimit; typeChecker.instantiationChildLimit = typeCheckLimits.instantiationChildLimit;
typeChecker.unifierIterationLimit = typeCheckLimits.unifierIterationLimit; typeChecker.unifierIterationLimit = typeCheckLimits.unifierIterationLimit;
if (FFlag::LuauTypecheckCancellation)
typeChecker.cancellationToken = typeCheckLimits.cancellationToken; typeChecker.cancellationToken = typeCheckLimits.cancellationToken;
return typeChecker.check(sourceModule, mode, environmentScope); return typeChecker.check(sourceModule, mode, environmentScope);

View file

@ -14,48 +14,9 @@
LUAU_FASTINTVARIABLE(LuauSuggestionDistance, 4) LUAU_FASTINTVARIABLE(LuauSuggestionDistance, 4)
LUAU_FASTFLAGVARIABLE(LuauLintNativeComment, false)
namespace Luau namespace Luau
{ {
// clang-format off
static const char* kWarningNames[] = {
"Unknown",
"UnknownGlobal",
"DeprecatedGlobal",
"GlobalUsedAsLocal",
"LocalShadow",
"SameLineStatement",
"MultiLineStatement",
"LocalUnused",
"FunctionUnused",
"ImportUnused",
"BuiltinGlobalWrite",
"PlaceholderRead",
"UnreachableCode",
"UnknownType",
"ForRange",
"UnbalancedAssignment",
"ImplicitReturn",
"DuplicateLocal",
"FormatString",
"TableLiteral",
"UninitializedLocal",
"DuplicateFunction",
"DeprecatedApi",
"TableOperations",
"DuplicateCondition",
"MisleadingAndOr",
"CommentDirective",
"IntegerParsing",
"ComparisonPrecedence",
};
// clang-format on
static_assert(std::size(kWarningNames) == unsigned(LintWarning::Code__Count), "did you forget to add warning to the list?");
struct LintContext struct LintContext
{ {
struct Global struct Global
@ -2827,7 +2788,7 @@ static void lintComments(LintContext& context, const std::vector<HotComment>& ho
"optimize directive uses unknown optimization level '%s', 0..2 expected", level); "optimize directive uses unknown optimization level '%s', 0..2 expected", level);
} }
} }
else if (FFlag::LuauLintNativeComment && first == "native") else if (first == "native")
{ {
if (space != std::string::npos) if (space != std::string::npos)
emitWarning(context, LintWarning::Code_CommentDirective, hc.location, emitWarning(context, LintWarning::Code_CommentDirective, hc.location,
@ -2855,12 +2816,6 @@ static void lintComments(LintContext& context, const std::vector<HotComment>& ho
} }
} }
void LintOptions::setDefaults()
{
// By default, we enable all warnings
warningMask = ~0ull;
}
std::vector<LintWarning> lint(AstStat* root, const AstNameTable& names, const ScopePtr& env, const Module* module, std::vector<LintWarning> lint(AstStat* root, const AstNameTable& names, const ScopePtr& env, const Module* module,
const std::vector<HotComment>& hotcomments, const LintOptions& options) const std::vector<HotComment>& hotcomments, const LintOptions& options)
{ {
@ -2952,54 +2907,6 @@ std::vector<LintWarning> lint(AstStat* root, const AstNameTable& names, const Sc
return context.result; return context.result;
} }
const char* LintWarning::getName(Code code)
{
LUAU_ASSERT(unsigned(code) < Code__Count);
return kWarningNames[code];
}
LintWarning::Code LintWarning::parseName(const char* name)
{
for (int code = Code_Unknown; code < Code__Count; ++code)
if (strcmp(name, getName(Code(code))) == 0)
return Code(code);
return Code_Unknown;
}
uint64_t LintWarning::parseMask(const std::vector<HotComment>& hotcomments)
{
uint64_t result = 0;
for (const HotComment& hc : hotcomments)
{
if (!hc.header)
continue;
if (hc.content.compare(0, 6, "nolint") != 0)
continue;
size_t name = hc.content.find_first_not_of(" \t", 6);
// --!nolint disables everything
if (name == std::string::npos)
return ~0ull;
// --!nolint needs to be followed by a whitespace character
if (name == 6)
continue;
// --!nolint name disables the specific lint
LintWarning::Code code = LintWarning::parseName(hc.content.c_str() + name);
if (code != LintWarning::Code_Unknown)
result |= 1ull << int(code);
}
return result;
}
std::vector<AstName> getDeprecatedGlobals(const AstNameTable& names) std::vector<AstName> getDeprecatedGlobals(const AstNameTable& names)
{ {
LintContext context; LintContext context;

View file

@ -0,0 +1,63 @@
// This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details
#include "Luau/LinterConfig.h"
#include "Luau/ParseResult.h"
namespace Luau
{
void LintOptions::setDefaults()
{
// By default, we enable all warnings
warningMask = ~0ull;
}
const char* LintWarning::getName(Code code)
{
LUAU_ASSERT(unsigned(code) < Code__Count);
return kWarningNames[code];
}
LintWarning::Code LintWarning::parseName(const char* name)
{
for (int code = Code_Unknown; code < Code__Count; ++code)
if (strcmp(name, getName(Code(code))) == 0)
return Code(code);
return Code_Unknown;
}
uint64_t LintWarning::parseMask(const std::vector<HotComment>& hotcomments)
{
uint64_t result = 0;
for (const HotComment& hc : hotcomments)
{
if (!hc.header)
continue;
if (hc.content.compare(0, 6, "nolint") != 0)
continue;
size_t name = hc.content.find_first_not_of(" \t", 6);
// --!nolint disables everything
if (name == std::string::npos)
return ~0ull;
// --!nolint needs to be followed by a whitespace character
if (name == 6)
continue;
// --!nolint name disables the specific lint
LintWarning::Code code = LintWarning::parseName(hc.content.c_str() + name);
if (code != LintWarning::Code_Unknown)
result |= 1ull << int(code);
}
return result;
}
} // namespace Luau

View file

@ -199,7 +199,7 @@ void Module::clonePublicInterface(NotNull<BuiltinTypes> builtinTypes, InternalEr
LUAU_ASSERT(interfaceTypes.types.empty()); LUAU_ASSERT(interfaceTypes.types.empty());
LUAU_ASSERT(interfaceTypes.typePacks.empty()); LUAU_ASSERT(interfaceTypes.typePacks.empty());
CloneState cloneState; CloneState cloneState{builtinTypes};
ScopePtr moduleScope = getModuleScope(); ScopePtr moduleScope = getModuleScope();

View file

@ -10,7 +10,6 @@
LUAU_FASTINTVARIABLE(LuauTarjanChildLimit, 10000) LUAU_FASTINTVARIABLE(LuauTarjanChildLimit, 10000)
LUAU_FASTFLAG(DebugLuauReadWriteProperties) LUAU_FASTFLAG(DebugLuauReadWriteProperties)
LUAU_FASTFLAGVARIABLE(LuauTarjanSingleArr, false)
namespace Luau namespace Luau
{ {
@ -269,8 +268,6 @@ std::pair<int, bool> Tarjan::indexify(TypeId ty)
{ {
ty = log->follow(ty); ty = log->follow(ty);
if (FFlag::LuauTarjanSingleArr)
{
auto [index, fresh] = typeToIndex.try_insert(ty, false); auto [index, fresh] = typeToIndex.try_insert(ty, false);
if (fresh) if (fresh)
@ -281,29 +278,11 @@ std::pair<int, bool> Tarjan::indexify(TypeId ty)
return {index, fresh}; return {index, fresh};
} }
else
{
bool fresh = !typeToIndex.contains(ty);
int& index = typeToIndex[ty];
if (fresh)
{
index = int(indexToType.size());
indexToType.push_back(ty);
indexToPack.push_back(nullptr);
onStack.push_back(false);
lowlink.push_back(index);
}
return {index, fresh};
}
}
std::pair<int, bool> Tarjan::indexify(TypePackId tp) std::pair<int, bool> Tarjan::indexify(TypePackId tp)
{ {
tp = log->follow(tp); tp = log->follow(tp);
if (FFlag::LuauTarjanSingleArr)
{
auto [index, fresh] = packToIndex.try_insert(tp, false); auto [index, fresh] = packToIndex.try_insert(tp, false);
if (fresh) if (fresh)
@ -314,23 +293,6 @@ std::pair<int, bool> Tarjan::indexify(TypePackId tp)
return {index, fresh}; return {index, fresh};
} }
else
{
bool fresh = !packToIndex.contains(tp);
int& index = packToIndex[tp];
if (fresh)
{
index = int(indexToPack.size());
indexToType.push_back(nullptr);
indexToPack.push_back(tp);
onStack.push_back(false);
lowlink.push_back(index);
}
return {index, fresh};
}
}
void Tarjan::visitChild(TypeId ty) void Tarjan::visitChild(TypeId ty)
{ {
@ -350,9 +312,6 @@ void Tarjan::visitChild(TypePackId tp)
TarjanResult Tarjan::loop() TarjanResult Tarjan::loop()
{ {
if (!FFlag::LuauTarjanSingleArr)
return loop_DEPRECATED();
// Normally Tarjan is presented recursively, but this is a hot loop, so worth optimizing // Normally Tarjan is presented recursively, but this is a hot loop, so worth optimizing
while (!worklist.empty()) while (!worklist.empty())
{ {
@ -475,28 +434,12 @@ TarjanResult Tarjan::visitRoot(TypePackId tp)
} }
void Tarjan::clearTarjan() void Tarjan::clearTarjan()
{
if (FFlag::LuauTarjanSingleArr)
{ {
typeToIndex.clear(); typeToIndex.clear();
packToIndex.clear(); packToIndex.clear();
nodes.clear(); nodes.clear();
stack.clear(); stack.clear();
}
else
{
dirty.clear();
typeToIndex.clear();
packToIndex.clear();
indexToType.clear();
indexToPack.clear();
stack.clear();
onStack.clear();
lowlink.clear();
}
edgesTy.clear(); edgesTy.clear();
edgesTp.clear(); edgesTp.clear();
@ -504,34 +447,16 @@ void Tarjan::clearTarjan()
} }
bool Tarjan::getDirty(int index) bool Tarjan::getDirty(int index)
{
if (FFlag::LuauTarjanSingleArr)
{ {
LUAU_ASSERT(size_t(index) < nodes.size()); LUAU_ASSERT(size_t(index) < nodes.size());
return nodes[index].dirty; return nodes[index].dirty;
} }
else
{
if (dirty.size() <= size_t(index))
dirty.resize(index + 1, false);
return dirty[index];
}
}
void Tarjan::setDirty(int index, bool d) void Tarjan::setDirty(int index, bool d)
{
if (FFlag::LuauTarjanSingleArr)
{ {
LUAU_ASSERT(size_t(index) < nodes.size()); LUAU_ASSERT(size_t(index) < nodes.size());
nodes[index].dirty = d; nodes[index].dirty = d;
} }
else
{
if (dirty.size() <= size_t(index))
dirty.resize(index + 1, false);
dirty[index] = d;
}
}
void Tarjan::visitEdge(int index, int parentIndex) void Tarjan::visitEdge(int index, int parentIndex)
{ {
@ -541,9 +466,6 @@ void Tarjan::visitEdge(int index, int parentIndex)
void Tarjan::visitSCC(int index) void Tarjan::visitSCC(int index)
{ {
if (!FFlag::LuauTarjanSingleArr)
return visitSCC_DEPRECATED(index);
bool d = getDirty(index); bool d = getDirty(index);
for (auto it = stack.rbegin(); !d && it != stack.rend(); it++) for (auto it = stack.rbegin(); !d && it != stack.rend(); it++)
@ -588,132 +510,6 @@ TarjanResult Tarjan::findDirty(TypePackId tp)
return visitRoot(tp); return visitRoot(tp);
} }
TarjanResult Tarjan::loop_DEPRECATED()
{
// Normally Tarjan is presented recursively, but this is a hot loop, so worth optimizing
while (!worklist.empty())
{
auto [index, currEdge, lastEdge] = worklist.back();
// First visit
if (currEdge == -1)
{
++childCount;
if (childLimit > 0 && childLimit <= childCount)
return TarjanResult::TooManyChildren;
stack.push_back(index);
onStack[index] = true;
currEdge = int(edgesTy.size());
// Fill in edge list of this vertex
if (TypeId ty = indexToType[index])
visitChildren(ty, index);
else if (TypePackId tp = indexToPack[index])
visitChildren(tp, index);
lastEdge = int(edgesTy.size());
}
// Visit children
bool foundFresh = false;
for (; currEdge < lastEdge; currEdge++)
{
int childIndex = -1;
bool fresh = false;
if (auto ty = edgesTy[currEdge])
std::tie(childIndex, fresh) = indexify(ty);
else if (auto tp = edgesTp[currEdge])
std::tie(childIndex, fresh) = indexify(tp);
else
LUAU_ASSERT(false);
if (fresh)
{
// Original recursion point, update the parent continuation point and start the new element
worklist.back() = {index, currEdge + 1, lastEdge};
worklist.push_back({childIndex, -1, -1});
// We need to continue the top-level loop from the start with the new worklist element
foundFresh = true;
break;
}
else if (onStack[childIndex])
{
lowlink[index] = std::min(lowlink[index], childIndex);
}
visitEdge(childIndex, index);
}
if (foundFresh)
continue;
if (lowlink[index] == index)
{
visitSCC(index);
while (!stack.empty())
{
int popped = stack.back();
stack.pop_back();
onStack[popped] = false;
if (popped == index)
break;
}
}
worklist.pop_back();
// Original return from recursion into a child
if (!worklist.empty())
{
auto [parentIndex, _, parentEndEdge] = worklist.back();
// No need to keep child edges around
edgesTy.resize(parentEndEdge);
edgesTp.resize(parentEndEdge);
lowlink[parentIndex] = std::min(lowlink[parentIndex], lowlink[index]);
visitEdge(index, parentIndex);
}
}
return TarjanResult::Ok;
}
void Tarjan::visitSCC_DEPRECATED(int index)
{
bool d = getDirty(index);
for (auto it = stack.rbegin(); !d && it != stack.rend(); it++)
{
if (TypeId ty = indexToType[*it])
d = isDirty(ty);
else if (TypePackId tp = indexToPack[*it])
d = isDirty(tp);
if (*it == index)
break;
}
if (!d)
return;
for (auto it = stack.rbegin(); it != stack.rend(); it++)
{
setDirty(*it, true);
if (TypeId ty = indexToType[*it])
foundDirty(ty);
else if (TypePackId tp = indexToPack[*it])
foundDirty(tp);
if (*it == index)
return;
}
}
std::optional<TypeId> Substitution::substitute(TypeId ty) std::optional<TypeId> Substitution::substitute(TypeId ty)
{ {
ty = log->follow(ty); ty = log->follow(ty);

View file

@ -2688,7 +2688,7 @@ void check(
typeChecker.visit(sourceModule.root); typeChecker.visit(sourceModule.root);
unfreeze(module->interfaceTypes); unfreeze(module->interfaceTypes);
copyErrors(module->errors, module->interfaceTypes); copyErrors(module->errors, module->interfaceTypes, builtinTypes);
freeze(module->interfaceTypes); freeze(module->interfaceTypes);
} }

View file

@ -38,11 +38,9 @@ LUAU_FASTFLAG(LuauInstantiateInSubtyping)
LUAU_FASTFLAGVARIABLE(LuauAllowIndexClassParameters, false) LUAU_FASTFLAGVARIABLE(LuauAllowIndexClassParameters, false)
LUAU_FASTFLAGVARIABLE(LuauFixCyclicModuleExports, false) LUAU_FASTFLAGVARIABLE(LuauFixCyclicModuleExports, false)
LUAU_FASTFLAG(LuauOccursIsntAlwaysFailure) LUAU_FASTFLAG(LuauOccursIsntAlwaysFailure)
LUAU_FASTFLAGVARIABLE(LuauTypecheckTypeguards, false)
LUAU_FASTFLAGVARIABLE(LuauTinyControlFlowAnalysis, false) LUAU_FASTFLAGVARIABLE(LuauTinyControlFlowAnalysis, false)
LUAU_FASTFLAGVARIABLE(LuauAlwaysCommitInferencesOfFunctionCalls, false) LUAU_FASTFLAGVARIABLE(LuauAlwaysCommitInferencesOfFunctionCalls, false)
LUAU_FASTFLAG(LuauParseDeclareClassIndexer) LUAU_FASTFLAG(LuauParseDeclareClassIndexer)
LUAU_FASTFLAGVARIABLE(LuauIndexTableIntersectionStringExpr, false)
namespace Luau namespace Luau
{ {
@ -3110,22 +3108,13 @@ WithPredicate<TypeId> TypeChecker::checkExpr(const ScopePtr& scope, const AstExp
} }
else if (expr.op == AstExprBinary::CompareEq || expr.op == AstExprBinary::CompareNe) else if (expr.op == AstExprBinary::CompareEq || expr.op == AstExprBinary::CompareNe)
{ {
if (!FFlag::LuauTypecheckTypeguards)
{
if (auto predicate = tryGetTypeGuardPredicate(expr))
return {booleanType, {std::move(*predicate)}};
}
// For these, passing expectedType is worse than simply forcing them, because their implementation // For these, passing expectedType is worse than simply forcing them, because their implementation
// may inadvertently check if expectedTypes exist first and use it, instead of forceSingleton first. // may inadvertently check if expectedTypes exist first and use it, instead of forceSingleton first.
WithPredicate<TypeId> lhs = checkExpr(scope, *expr.left, std::nullopt, /*forceSingleton=*/true); WithPredicate<TypeId> lhs = checkExpr(scope, *expr.left, std::nullopt, /*forceSingleton=*/true);
WithPredicate<TypeId> rhs = checkExpr(scope, *expr.right, std::nullopt, /*forceSingleton=*/true); WithPredicate<TypeId> rhs = checkExpr(scope, *expr.right, std::nullopt, /*forceSingleton=*/true);
if (FFlag::LuauTypecheckTypeguards)
{
if (auto predicate = tryGetTypeGuardPredicate(expr)) if (auto predicate = tryGetTypeGuardPredicate(expr))
return {booleanType, {std::move(*predicate)}}; return {booleanType, {std::move(*predicate)}};
}
PredicateVec predicates; PredicateVec predicates;
@ -3405,7 +3394,7 @@ TypeId TypeChecker::checkLValueBinding(const ScopePtr& scope, const AstExprIndex
reportError(TypeError{expr.location, UnknownProperty{exprType, value->value.data}}); reportError(TypeError{expr.location, UnknownProperty{exprType, value->value.data}});
return errorRecoveryType(scope); return errorRecoveryType(scope);
} }
else if (FFlag::LuauIndexTableIntersectionStringExpr && get<IntersectionType>(exprType)) else if (get<IntersectionType>(exprType))
{ {
Name name = std::string(value->value.data, value->value.size); Name name = std::string(value->value.data, value->value.size);

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
#pragma once #pragma once
#include "Luau/CodeGen.h"
#include <vector> #include <vector>
#include <stddef.h> #include <stddef.h>
@ -16,6 +18,7 @@ constexpr uint32_t kCodeAlignment = 32;
struct CodeAllocator struct CodeAllocator
{ {
CodeAllocator(size_t blockSize, size_t maxTotalSize); CodeAllocator(size_t blockSize, size_t maxTotalSize);
CodeAllocator(size_t blockSize, size_t maxTotalSize, AllocationCallback* allocationCallback, void* allocationCallbackContext);
~CodeAllocator(); ~CodeAllocator();
// Places data and code into the executable page area // Places data and code into the executable page area
@ -24,7 +27,7 @@ struct CodeAllocator
bool allocate( bool allocate(
const uint8_t* data, size_t dataSize, const uint8_t* code, size_t codeSize, uint8_t*& result, size_t& resultSize, uint8_t*& resultCodeStart); const uint8_t* data, size_t dataSize, const uint8_t* code, size_t codeSize, uint8_t*& result, size_t& resultSize, uint8_t*& resultCodeStart);
// Provided to callbacks // Provided to unwind info callbacks
void* context = nullptr; void* context = nullptr;
// Called when new block is created to create and setup the unwinding information for all the code in the block // Called when new block is created to create and setup the unwinding information for all the code in the block
@ -34,12 +37,16 @@ struct CodeAllocator
// Called to destroy unwinding information returned by 'createBlockUnwindInfo' // Called to destroy unwinding information returned by 'createBlockUnwindInfo'
void (*destroyBlockUnwindInfo)(void* context, void* unwindData) = nullptr; void (*destroyBlockUnwindInfo)(void* context, void* unwindData) = nullptr;
private:
// Unwind information can be placed inside the block with some implementation-specific reservations at the beginning // Unwind information can be placed inside the block with some implementation-specific reservations at the beginning
// But to simplify block space checks, we limit the max size of all that data // But to simplify block space checks, we limit the max size of all that data
static const size_t kMaxReservedDataSize = 256; static const size_t kMaxReservedDataSize = 256;
bool allocateNewBlock(size_t& unwindInfoSize); bool allocateNewBlock(size_t& unwindInfoSize);
uint8_t* allocatePages(size_t size) const;
void freePages(uint8_t* mem, size_t size) const;
// Current block we use for allocations // Current block we use for allocations
uint8_t* blockPos = nullptr; uint8_t* blockPos = nullptr;
uint8_t* blockEnd = nullptr; uint8_t* blockEnd = nullptr;
@ -50,6 +57,9 @@ struct CodeAllocator
size_t blockSize = 0; size_t blockSize = 0;
size_t maxTotalSize = 0; size_t maxTotalSize = 0;
AllocationCallback* allocationCallback = nullptr;
void* allocationCallbackContext = nullptr;
}; };
} // namespace CodeGen } // namespace CodeGen

View file

@ -18,12 +18,25 @@ enum CodeGenFlags
CodeGen_OnlyNativeModules = 1 << 0, CodeGen_OnlyNativeModules = 1 << 0,
}; };
struct CompilationStats
{
size_t bytecodeSizeBytes = 0;
size_t nativeCodeSizeBytes = 0;
size_t nativeDataSizeBytes = 0;
size_t nativeMetadataSizeBytes = 0;
uint32_t functionsCompiled = 0;
};
using AllocationCallback = void(void* context, void* oldPointer, size_t oldSize, void* newPointer, size_t newSize);
bool isSupported(); bool isSupported();
void create(lua_State* L, AllocationCallback* allocationCallback, void* allocationCallbackContext);
void create(lua_State* L); void create(lua_State* L);
// Builds target function and all inner functions // Builds target function and all inner functions
void compile(lua_State* L, int idx, unsigned int flags = 0); void compile(lua_State* L, int idx, unsigned int flags = 0, CompilationStats* stats = nullptr);
using AnnotatorFn = void (*)(void* context, std::string& result, int fid, int instpos); using AnnotatorFn = void (*)(void* context, std::string& result, int fid, int instpos);

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
#pragma once #pragma once
#include "Luau/Common.h"
namespace Luau namespace Luau
{ {
namespace CodeGen namespace CodeGen
@ -43,5 +45,68 @@ enum class ConditionX64 : uint8_t
Count Count
}; };
inline ConditionX64 getReverseCondition(ConditionX64 cond)
{
switch (cond)
{
case ConditionX64::Overflow:
return ConditionX64::NoOverflow;
case ConditionX64::NoOverflow:
return ConditionX64::Overflow;
case ConditionX64::Carry:
return ConditionX64::NoCarry;
case ConditionX64::NoCarry:
return ConditionX64::Carry;
case ConditionX64::Below:
return ConditionX64::NotBelow;
case ConditionX64::BelowEqual:
return ConditionX64::NotBelowEqual;
case ConditionX64::Above:
return ConditionX64::NotAbove;
case ConditionX64::AboveEqual:
return ConditionX64::NotAboveEqual;
case ConditionX64::Equal:
return ConditionX64::NotEqual;
case ConditionX64::Less:
return ConditionX64::NotLess;
case ConditionX64::LessEqual:
return ConditionX64::NotLessEqual;
case ConditionX64::Greater:
return ConditionX64::NotGreater;
case ConditionX64::GreaterEqual:
return ConditionX64::NotGreaterEqual;
case ConditionX64::NotBelow:
return ConditionX64::Below;
case ConditionX64::NotBelowEqual:
return ConditionX64::BelowEqual;
case ConditionX64::NotAbove:
return ConditionX64::Above;
case ConditionX64::NotAboveEqual:
return ConditionX64::AboveEqual;
case ConditionX64::NotEqual:
return ConditionX64::Equal;
case ConditionX64::NotLess:
return ConditionX64::Less;
case ConditionX64::NotLessEqual:
return ConditionX64::LessEqual;
case ConditionX64::NotGreater:
return ConditionX64::Greater;
case ConditionX64::NotGreaterEqual:
return ConditionX64::GreaterEqual;
case ConditionX64::Zero:
return ConditionX64::NotZero;
case ConditionX64::NotZero:
return ConditionX64::Zero;
case ConditionX64::Parity:
return ConditionX64::NotParity;
case ConditionX64::NotParity:
return ConditionX64::Parity;
case ConditionX64::Count:
LUAU_ASSERT(!"invalid ConditionX64 value");
}
return ConditionX64::Count;
}
} // namespace CodeGen } // namespace CodeGen
} // namespace Luau } // namespace Luau

View file

@ -176,7 +176,7 @@ enum class IrCmd : uint8_t
CMP_ANY, CMP_ANY,
// Unconditional jump // Unconditional jump
// A: block/vmexit // A: block/vmexit/undef
JUMP, JUMP,
// Jump if TValue is truthy // Jump if TValue is truthy
@ -369,10 +369,8 @@ enum class IrCmd : uint8_t
// Guard against tag mismatch // Guard against tag mismatch
// A, B: tag // A, B: tag
// C: block/vmexit/undef // C: block/vmexit/undef
// D: bool (finish execution in VM on failure)
// In final x64 lowering, A can also be Rn // In final x64 lowering, A can also be Rn
// When undef is specified instead of a block, execution is aborted on check failure; if D is true, execution is continued in VM interpreter // When undef is specified instead of a block, execution is aborted on check failure
// instead.
CHECK_TAG, CHECK_TAG,
// Guard against a falsy tag+value // Guard against a falsy tag+value
@ -689,6 +687,10 @@ enum class IrOpKind : uint32_t
VmExit, VmExit,
}; };
// VmExit uses a special value to indicate that pcpos update should be skipped
// This is only used during type checking at function entry
constexpr uint32_t kVmExitEntryGuardPc = (1u << 28) - 1;
struct IrOp struct IrOp
{ {
IrOpKind kind : 4; IrOpKind kind : 4;
@ -851,6 +853,8 @@ struct IrFunction
std::vector<IrConst> constants; std::vector<IrConst> constants;
std::vector<BytecodeMapping> bcMapping; std::vector<BytecodeMapping> bcMapping;
uint32_t entryBlock = 0;
uint32_t entryLocation = 0;
// For each instruction, an operand that can be used to recompute the value // For each instruction, an operand that can be used to recompute the value
std::vector<IrOp> valueRestoreOps; std::vector<IrOp> valueRestoreOps;
@ -1037,5 +1041,11 @@ inline int vmUpvalueOp(IrOp op)
return op.index; return op.index;
} }
inline uint32_t vmExitOp(IrOp op)
{
LUAU_ASSERT(op.kind == IrOpKind::VmExit);
return op.index;
}
} // namespace CodeGen } // namespace CodeGen
} // namespace Luau } // namespace Luau

View file

@ -5,6 +5,7 @@
#include "ByteUtils.h" #include "ByteUtils.h"
#include <stdarg.h> #include <stdarg.h>
#include <stdio.h>
namespace Luau namespace Luau
{ {

View file

@ -5,7 +5,6 @@
#include <stdarg.h> #include <stdarg.h>
#include <stdio.h> #include <stdio.h>
#include <string.h>
namespace Luau namespace Luau
{ {

View file

@ -33,13 +33,17 @@ static size_t alignToPageSize(size_t size)
} }
#if defined(_WIN32) #if defined(_WIN32)
static uint8_t* allocatePages(size_t size) static uint8_t* allocatePagesImpl(size_t size)
{ {
return (uint8_t*)VirtualAlloc(nullptr, alignToPageSize(size), MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE); LUAU_ASSERT(size == alignToPageSize(size));
return (uint8_t*)VirtualAlloc(nullptr, size, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE);
} }
static void freePages(uint8_t* mem, size_t size) static void freePagesImpl(uint8_t* mem, size_t size)
{ {
LUAU_ASSERT(size == alignToPageSize(size));
if (VirtualFree(mem, 0, MEM_RELEASE) == 0) if (VirtualFree(mem, 0, MEM_RELEASE) == 0)
LUAU_ASSERT(!"failed to deallocate block memory"); LUAU_ASSERT(!"failed to deallocate block memory");
} }
@ -62,14 +66,22 @@ static void flushInstructionCache(uint8_t* mem, size_t size)
#endif #endif
} }
#else #else
static uint8_t* allocatePages(size_t size) static uint8_t* allocatePagesImpl(size_t size)
{ {
return (uint8_t*)mmap(nullptr, alignToPageSize(size), PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANON, -1, 0); LUAU_ASSERT(size == alignToPageSize(size));
#ifdef __APPLE__
return (uint8_t*)mmap(nullptr, size, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANON | MAP_JIT, -1, 0);
#else
return (uint8_t*)mmap(nullptr, size, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANON, -1, 0);
#endif
} }
static void freePages(uint8_t* mem, size_t size) static void freePagesImpl(uint8_t* mem, size_t size)
{ {
if (munmap(mem, alignToPageSize(size)) != 0) LUAU_ASSERT(size == alignToPageSize(size));
if (munmap(mem, size) != 0)
LUAU_ASSERT(!"Failed to deallocate block memory"); LUAU_ASSERT(!"Failed to deallocate block memory");
} }
@ -94,8 +106,15 @@ namespace CodeGen
{ {
CodeAllocator::CodeAllocator(size_t blockSize, size_t maxTotalSize) CodeAllocator::CodeAllocator(size_t blockSize, size_t maxTotalSize)
: blockSize(blockSize) : CodeAllocator(blockSize, maxTotalSize, nullptr, nullptr)
, maxTotalSize(maxTotalSize) {
}
CodeAllocator::CodeAllocator(size_t blockSize, size_t maxTotalSize, AllocationCallback* allocationCallback, void* allocationCallbackContext)
: blockSize{blockSize}
, maxTotalSize{maxTotalSize}
, allocationCallback{allocationCallback}
, allocationCallbackContext{allocationCallbackContext}
{ {
LUAU_ASSERT(blockSize > kMaxReservedDataSize); LUAU_ASSERT(blockSize > kMaxReservedDataSize);
LUAU_ASSERT(maxTotalSize >= blockSize); LUAU_ASSERT(maxTotalSize >= blockSize);
@ -207,5 +226,29 @@ bool CodeAllocator::allocateNewBlock(size_t& unwindInfoSize)
return true; return true;
} }
uint8_t* CodeAllocator::allocatePages(size_t size) const
{
const size_t pageAlignedSize = alignToPageSize(size);
uint8_t* const mem = allocatePagesImpl(pageAlignedSize);
if (mem == nullptr)
return nullptr;
if (allocationCallback)
allocationCallback(allocationCallbackContext, nullptr, 0, mem, pageAlignedSize);
return mem;
}
void CodeAllocator::freePages(uint8_t* mem, size_t size) const
{
const size_t pageAlignedSize = alignToPageSize(size);
if (allocationCallback)
allocationCallback(allocationCallbackContext, mem, pageAlignedSize, nullptr, 0);
freePagesImpl(mem, pageAlignedSize);
}
} // namespace CodeGen } // namespace CodeGen
} // namespace Luau } // namespace Luau

View file

@ -65,7 +65,7 @@ static NativeProto createNativeProto(Proto* proto, const IrBuilder& ir)
int sizecode = proto->sizecode; int sizecode = proto->sizecode;
uint32_t* instOffsets = new uint32_t[sizecode]; uint32_t* instOffsets = new uint32_t[sizecode];
uint32_t instTarget = ir.function.bcMapping[0].asmLocation; uint32_t instTarget = ir.function.entryLocation;
for (int i = 0; i < sizecode; i++) for (int i = 0; i < sizecode; i++)
{ {
@ -74,6 +74,9 @@ static NativeProto createNativeProto(Proto* proto, const IrBuilder& ir)
instOffsets[i] = ir.function.bcMapping[i].asmLocation - instTarget; instOffsets[i] = ir.function.bcMapping[i].asmLocation - instTarget;
} }
// Set first instruction offset to 0 so that entering this function still executes any generated entry code.
instOffsets[0] = 0;
// entry target will be relocated when assembly is finalized // entry target will be relocated when assembly is finalized
return {proto, instOffsets, instTarget}; return {proto, instOffsets, instTarget};
} }
@ -202,11 +205,11 @@ bool isSupported()
#endif #endif
} }
void create(lua_State* L) void create(lua_State* L, AllocationCallback* allocationCallback, void* allocationCallbackContext)
{ {
LUAU_ASSERT(isSupported()); LUAU_ASSERT(isSupported());
std::unique_ptr<NativeState> data = std::make_unique<NativeState>(); std::unique_ptr<NativeState> data = std::make_unique<NativeState>(allocationCallback, allocationCallbackContext);
#if defined(_WIN32) #if defined(_WIN32)
data->unwindBuilder = std::make_unique<UnwindBuilderWin>(); data->unwindBuilder = std::make_unique<UnwindBuilderWin>();
@ -239,7 +242,12 @@ void create(lua_State* L)
ecb->enter = onEnter; ecb->enter = onEnter;
} }
void compile(lua_State* L, int idx, unsigned int flags) void create(lua_State* L)
{
create(L, nullptr, nullptr);
}
void compile(lua_State* L, int idx, unsigned int flags, CompilationStats* stats)
{ {
LUAU_ASSERT(lua_isLfunction(L, idx)); LUAU_ASSERT(lua_isLfunction(L, idx));
const TValue* func = luaA_toobject(L, idx); const TValue* func = luaA_toobject(L, idx);
@ -318,13 +326,28 @@ void compile(lua_State* L, int idx, unsigned int flags)
} }
} }
for (NativeProto result : results) for (const NativeProto& result : results)
{ {
// the memory is now managed by VM and will be freed via onDestroyFunction // the memory is now managed by VM and will be freed via onDestroyFunction
result.p->execdata = result.execdata; result.p->execdata = result.execdata;
result.p->exectarget = uintptr_t(codeStart) + result.exectarget; result.p->exectarget = uintptr_t(codeStart) + result.exectarget;
result.p->codeentry = &kCodeEntryInsn; result.p->codeentry = &kCodeEntryInsn;
} }
if (stats != nullptr)
{
for (const NativeProto& result : results)
{
stats->bytecodeSizeBytes += result.p->sizecode * sizeof(Instruction);
// Account for the native -> bytecode instruction offsets mapping:
stats->nativeMetadataSizeBytes += result.p->sizecode * sizeof(uint32_t);
}
stats->functionsCompiled += uint32_t(results.size());
stats->nativeCodeSizeBytes += build.code.size();
stats->nativeDataSizeBytes += build.data.size();
}
} }
void setPerfLog(void* context, PerfLogFn logFn) void setPerfLog(void* context, PerfLogFn logFn)

View file

@ -24,15 +24,6 @@ struct EntryLocations
Label epilogueStart; Label epilogueStart;
}; };
static void emitClearNativeFlag(AssemblyBuilderA64& build)
{
build.ldr(x0, mem(rState, offsetof(lua_State, ci)));
build.ldr(w1, mem(x0, offsetof(CallInfo, flags)));
build.mov(w2, ~LUA_CALLINFO_NATIVE);
build.and_(w1, w1, w2);
build.str(w1, mem(x0, offsetof(CallInfo, flags)));
}
static void emitExit(AssemblyBuilderA64& build, bool continueInVm) static void emitExit(AssemblyBuilderA64& build, bool continueInVm)
{ {
build.mov(x0, continueInVm); build.mov(x0, continueInVm);
@ -40,14 +31,21 @@ static void emitExit(AssemblyBuilderA64& build, bool continueInVm)
build.br(x1); build.br(x1);
} }
static void emitUpdatePcAndContinueInVm(AssemblyBuilderA64& build) static void emitUpdatePcForExit(AssemblyBuilderA64& build)
{ {
// x0 = pcpos * sizeof(Instruction) // x0 = pcpos * sizeof(Instruction)
build.add(x0, rCode, x0); build.add(x0, rCode, x0);
build.ldr(x1, mem(rState, offsetof(lua_State, ci))); build.ldr(x1, mem(rState, offsetof(lua_State, ci)));
build.str(x0, mem(x1, offsetof(CallInfo, savedpc))); build.str(x0, mem(x1, offsetof(CallInfo, savedpc)));
}
emitExit(build, /* continueInVm */ true); static void emitClearNativeFlag(AssemblyBuilderA64& build)
{
build.ldr(x0, mem(rState, offsetof(lua_State, ci)));
build.ldr(w1, mem(x0, offsetof(CallInfo, flags)));
build.mov(w2, ~LUA_CALLINFO_NATIVE);
build.and_(w1, w1, w2);
build.str(w1, mem(x0, offsetof(CallInfo, flags)));
} }
static void emitInterrupt(AssemblyBuilderA64& build) static void emitInterrupt(AssemblyBuilderA64& build)
@ -305,6 +303,11 @@ bool initHeaderFunctions(NativeState& data)
void assembleHelpers(AssemblyBuilderA64& build, ModuleHelpers& helpers) void assembleHelpers(AssemblyBuilderA64& build, ModuleHelpers& helpers)
{ {
if (build.logText)
build.logAppend("; updatePcAndContinueInVm\n");
build.setLabel(helpers.updatePcAndContinueInVm);
emitUpdatePcForExit(build);
if (build.logText) if (build.logText)
build.logAppend("; exitContinueVmClearNativeFlag\n"); build.logAppend("; exitContinueVmClearNativeFlag\n");
build.setLabel(helpers.exitContinueVmClearNativeFlag); build.setLabel(helpers.exitContinueVmClearNativeFlag);
@ -320,11 +323,6 @@ void assembleHelpers(AssemblyBuilderA64& build, ModuleHelpers& helpers)
build.setLabel(helpers.exitNoContinueVm); build.setLabel(helpers.exitNoContinueVm);
emitExit(build, /* continueInVm */ false); emitExit(build, /* continueInVm */ false);
if (build.logText)
build.logAppend("; updatePcAndContinueInVm\n");
build.setLabel(helpers.updatePcAndContinueInVm);
emitUpdatePcAndContinueInVm(build);
if (build.logText) if (build.logText)
build.logAppend("; reentry\n"); build.logAppend("; reentry\n");
build.setLabel(helpers.reentry); build.setLabel(helpers.reentry);

View file

@ -130,6 +130,11 @@ inline bool lowerImpl(AssemblyBuilder& build, IrLowering& lowering, IrFunction&
build.setLabel(block.label); build.setLabel(block.label);
if (blockIndex == function.entryBlock)
{
function.entryLocation = build.getLabelOffset(block.label);
}
IrBlock& nextBlock = getNextBlock(function, sortedBlocks, dummy, i); IrBlock& nextBlock = getNextBlock(function, sortedBlocks, dummy, i);
for (uint32_t index = block.start; index <= block.finish; index++) for (uint32_t index = block.start; index <= block.finish; index++)

View file

@ -180,6 +180,11 @@ bool initHeaderFunctions(NativeState& data)
void assembleHelpers(X64::AssemblyBuilderX64& build, ModuleHelpers& helpers) void assembleHelpers(X64::AssemblyBuilderX64& build, ModuleHelpers& helpers)
{ {
if (build.logText)
build.logAppend("; updatePcAndContinueInVm\n");
build.setLabel(helpers.updatePcAndContinueInVm);
emitUpdatePcForExit(build);
if (build.logText) if (build.logText)
build.logAppend("; exitContinueVmClearNativeFlag\n"); build.logAppend("; exitContinueVmClearNativeFlag\n");
build.setLabel(helpers.exitContinueVmClearNativeFlag); build.setLabel(helpers.exitContinueVmClearNativeFlag);
@ -195,11 +200,6 @@ void assembleHelpers(X64::AssemblyBuilderX64& build, ModuleHelpers& helpers)
build.setLabel(helpers.exitNoContinueVm); build.setLabel(helpers.exitNoContinueVm);
emitExit(build, /* continueInVm */ false); emitExit(build, /* continueInVm */ false);
if (build.logText)
build.logAppend("; updatePcAndContinueInVm\n");
build.setLabel(helpers.updatePcAndContinueInVm);
emitUpdatePcAndContinueInVm(build);
if (build.logText) if (build.logText)
build.logAppend("; continueCallInVm\n"); build.logAppend("; continueCallInVm\n");
build.setLabel(helpers.continueCallInVm); build.setLabel(helpers.continueCallInVm);

View file

@ -1,8 +1,9 @@
// 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 "EmitBuiltinsX64.h" #include "EmitBuiltinsX64.h"
#include "Luau/AssemblyBuilderX64.h"
#include "Luau/Bytecode.h" #include "Luau/Bytecode.h"
#include "Luau/AssemblyBuilderX64.h"
#include "Luau/IrCallWrapperX64.h" #include "Luau/IrCallWrapperX64.h"
#include "Luau/IrRegAllocX64.h" #include "Luau/IrRegAllocX64.h"

View file

@ -25,7 +25,7 @@ struct ModuleHelpers
Label exitContinueVm; Label exitContinueVm;
Label exitNoContinueVm; Label exitNoContinueVm;
Label exitContinueVmClearNativeFlag; Label exitContinueVmClearNativeFlag;
Label updatePcAndContinueInVm; Label updatePcAndContinueInVm; // no reentry
Label return_; Label return_;
Label interrupt; Label interrupt;

View file

@ -328,14 +328,12 @@ void emitFallback(IrRegAllocX64& regs, AssemblyBuilderX64& build, int offset, in
emitUpdateBase(build); emitUpdateBase(build);
} }
void emitUpdatePcAndContinueInVm(AssemblyBuilderX64& build) void emitUpdatePcForExit(AssemblyBuilderX64& build)
{ {
// edx = pcpos * sizeof(Instruction) // edx = pcpos * sizeof(Instruction)
build.add(rdx, sCode); build.add(rdx, sCode);
build.mov(rax, qword[rState + offsetof(lua_State, ci)]); build.mov(rax, qword[rState + offsetof(lua_State, ci)]);
build.mov(qword[rax + offsetof(CallInfo, savedpc)], rdx); build.mov(qword[rax + offsetof(CallInfo, savedpc)], rdx);
emitExit(build, /* continueInVm */ true);
} }
void emitContinueCallInVm(AssemblyBuilderX64& build) void emitContinueCallInVm(AssemblyBuilderX64& build)

View file

@ -180,7 +180,7 @@ void emitUpdateBase(AssemblyBuilderX64& build);
void emitInterrupt(AssemblyBuilderX64& build); void emitInterrupt(AssemblyBuilderX64& build);
void emitFallback(IrRegAllocX64& regs, AssemblyBuilderX64& build, int offset, int pcpos); void emitFallback(IrRegAllocX64& regs, AssemblyBuilderX64& build, int offset, int pcpos);
void emitUpdatePcAndContinueInVm(AssemblyBuilderX64& build); void emitUpdatePcForExit(AssemblyBuilderX64& build);
void emitContinueCallInVm(AssemblyBuilderX64& build); void emitContinueCallInVm(AssemblyBuilderX64& build);
void emitReturn(AssemblyBuilderX64& build, ModuleHelpers& helpers); void emitReturn(AssemblyBuilderX64& build, ModuleHelpers& helpers);

View file

@ -186,75 +186,12 @@ void requireVariadicSequence(RegisterSet& sourceRs, const RegisterSet& defRs, ui
} }
} }
static RegisterSet computeBlockLiveInRegSet(IrFunction& function, const IrBlock& block, RegisterSet& defRs, std::bitset<256>& capturedRegs) template<typename T>
static void visitVmRegDefsUses(T& visitor, IrFunction& function, const IrBlock& block)
{ {
RegisterSet inRs;
auto def = [&](IrOp op, int offset = 0) {
defRs.regs.set(vmRegOp(op) + offset, true);
};
auto use = [&](IrOp op, int offset = 0) {
if (!defRs.regs.test(vmRegOp(op) + offset))
inRs.regs.set(vmRegOp(op) + offset, true);
};
auto maybeDef = [&](IrOp op) {
if (op.kind == IrOpKind::VmReg)
defRs.regs.set(vmRegOp(op), true);
};
auto maybeUse = [&](IrOp op) {
if (op.kind == IrOpKind::VmReg)
{
if (!defRs.regs.test(vmRegOp(op)))
inRs.regs.set(vmRegOp(op), true);
}
};
auto defVarargs = [&](uint8_t varargStart) {
defRs.varargSeq = true;
defRs.varargStart = varargStart;
};
auto useVarargs = [&](uint8_t varargStart) {
requireVariadicSequence(inRs, defRs, varargStart);
// Variadic sequence has been consumed
defRs.varargSeq = false;
defRs.varargStart = 0;
};
auto defRange = [&](int start, int count) {
if (count == -1)
{
defVarargs(start);
}
else
{
for (int i = start; i < start + count; i++)
defRs.regs.set(i, true);
}
};
auto useRange = [&](int start, int count) {
if (count == -1)
{
useVarargs(start);
}
else
{
for (int i = start; i < start + count; i++)
{
if (!defRs.regs.test(i))
inRs.regs.set(i, true);
}
}
};
for (uint32_t instIdx = block.start; instIdx <= block.finish; instIdx++) for (uint32_t instIdx = block.start; instIdx <= block.finish; instIdx++)
{ {
const IrInst& inst = function.instructions[instIdx]; IrInst& inst = function.instructions[instIdx];
// For correct analysis, all instruction uses must be handled before handling the definitions // For correct analysis, all instruction uses must be handled before handling the definitions
switch (inst.cmd) switch (inst.cmd)
@ -264,7 +201,7 @@ static RegisterSet computeBlockLiveInRegSet(IrFunction& function, const IrBlock&
case IrCmd::LOAD_DOUBLE: case IrCmd::LOAD_DOUBLE:
case IrCmd::LOAD_INT: case IrCmd::LOAD_INT:
case IrCmd::LOAD_TVALUE: case IrCmd::LOAD_TVALUE:
maybeUse(inst.a); // Argument can also be a VmConst visitor.maybeUse(inst.a); // Argument can also be a VmConst
break; break;
case IrCmd::STORE_TAG: case IrCmd::STORE_TAG:
case IrCmd::STORE_POINTER: case IrCmd::STORE_POINTER:
@ -272,63 +209,63 @@ static RegisterSet computeBlockLiveInRegSet(IrFunction& function, const IrBlock&
case IrCmd::STORE_INT: case IrCmd::STORE_INT:
case IrCmd::STORE_VECTOR: case IrCmd::STORE_VECTOR:
case IrCmd::STORE_TVALUE: case IrCmd::STORE_TVALUE:
maybeDef(inst.a); // Argument can also be a pointer value visitor.maybeDef(inst.a); // Argument can also be a pointer value
break; break;
case IrCmd::CMP_ANY: case IrCmd::CMP_ANY:
use(inst.a); visitor.use(inst.a);
use(inst.b); visitor.use(inst.b);
break; break;
case IrCmd::JUMP_IF_TRUTHY: case IrCmd::JUMP_IF_TRUTHY:
case IrCmd::JUMP_IF_FALSY: case IrCmd::JUMP_IF_FALSY:
use(inst.a); visitor.use(inst.a);
break; break;
// A <- B, C // A <- B, C
case IrCmd::DO_ARITH: case IrCmd::DO_ARITH:
case IrCmd::GET_TABLE: case IrCmd::GET_TABLE:
use(inst.b); visitor.use(inst.b);
maybeUse(inst.c); // Argument can also be a VmConst visitor.maybeUse(inst.c); // Argument can also be a VmConst
def(inst.a); visitor.def(inst.a);
break; break;
case IrCmd::SET_TABLE: case IrCmd::SET_TABLE:
use(inst.a); visitor.use(inst.a);
use(inst.b); visitor.use(inst.b);
maybeUse(inst.c); // Argument can also be a VmConst visitor.maybeUse(inst.c); // Argument can also be a VmConst
break; break;
// A <- B // A <- B
case IrCmd::DO_LEN: case IrCmd::DO_LEN:
use(inst.b); visitor.use(inst.b);
def(inst.a); visitor.def(inst.a);
break; break;
case IrCmd::GET_IMPORT: case IrCmd::GET_IMPORT:
def(inst.a); visitor.def(inst.a);
break; break;
case IrCmd::CONCAT: case IrCmd::CONCAT:
useRange(vmRegOp(inst.a), function.uintOp(inst.b)); visitor.useRange(vmRegOp(inst.a), function.uintOp(inst.b));
defRange(vmRegOp(inst.a), function.uintOp(inst.b)); visitor.defRange(vmRegOp(inst.a), function.uintOp(inst.b));
break; break;
case IrCmd::GET_UPVALUE: case IrCmd::GET_UPVALUE:
def(inst.a); visitor.def(inst.a);
break; break;
case IrCmd::SET_UPVALUE: case IrCmd::SET_UPVALUE:
use(inst.b); visitor.use(inst.b);
break; break;
case IrCmd::PREPARE_FORN: case IrCmd::PREPARE_FORN:
use(inst.a); visitor.use(inst.a);
use(inst.b); visitor.use(inst.b);
use(inst.c); visitor.use(inst.c);
def(inst.a); visitor.def(inst.a);
def(inst.b); visitor.def(inst.b);
def(inst.c); visitor.def(inst.c);
break; break;
case IrCmd::INTERRUPT: case IrCmd::INTERRUPT:
break; break;
case IrCmd::BARRIER_OBJ: case IrCmd::BARRIER_OBJ:
case IrCmd::BARRIER_TABLE_FORWARD: case IrCmd::BARRIER_TABLE_FORWARD:
use(inst.b); visitor.use(inst.b);
break; break;
case IrCmd::CLOSE_UPVALS: case IrCmd::CLOSE_UPVALS:
// Closing an upvalue should be counted as a register use (it copies the fresh register value) // Closing an upvalue should be counted as a register use (it copies the fresh register value)
@ -336,23 +273,23 @@ static RegisterSet computeBlockLiveInRegSet(IrFunction& function, const IrBlock&
// Because we don't plan to optimize captured registers atm, we skip full dataflow analysis for them right now // Because we don't plan to optimize captured registers atm, we skip full dataflow analysis for them right now
break; break;
case IrCmd::CAPTURE: case IrCmd::CAPTURE:
maybeUse(inst.a); visitor.maybeUse(inst.a);
if (function.uintOp(inst.b) == 1) if (function.uintOp(inst.b) == 1)
capturedRegs.set(vmRegOp(inst.a), true); visitor.capture(vmRegOp(inst.a));
break; break;
case IrCmd::SETLIST: case IrCmd::SETLIST:
use(inst.b); visitor.use(inst.b);
useRange(vmRegOp(inst.c), function.intOp(inst.d)); visitor.useRange(vmRegOp(inst.c), function.intOp(inst.d));
break; break;
case IrCmd::CALL: case IrCmd::CALL:
use(inst.a); visitor.use(inst.a);
useRange(vmRegOp(inst.a) + 1, function.intOp(inst.b)); visitor.useRange(vmRegOp(inst.a) + 1, function.intOp(inst.b));
defRange(vmRegOp(inst.a), function.intOp(inst.c)); visitor.defRange(vmRegOp(inst.a), function.intOp(inst.c));
break; break;
case IrCmd::RETURN: case IrCmd::RETURN:
useRange(vmRegOp(inst.a), function.intOp(inst.b)); visitor.useRange(vmRegOp(inst.a), function.intOp(inst.b));
break; break;
// TODO: FASTCALL is more restrictive than INVOKE_FASTCALL; we should either determine the exact semantics, or rework it // TODO: FASTCALL is more restrictive than INVOKE_FASTCALL; we should either determine the exact semantics, or rework it
@ -364,89 +301,89 @@ static RegisterSet computeBlockLiveInRegSet(IrFunction& function, const IrBlock&
{ {
LUAU_ASSERT(inst.d.kind == IrOpKind::VmReg && vmRegOp(inst.d) == vmRegOp(inst.c) + 1); LUAU_ASSERT(inst.d.kind == IrOpKind::VmReg && vmRegOp(inst.d) == vmRegOp(inst.c) + 1);
useRange(vmRegOp(inst.c), count); visitor.useRange(vmRegOp(inst.c), count);
} }
else else
{ {
if (count >= 1) if (count >= 1)
use(inst.c); visitor.use(inst.c);
if (count >= 2) if (count >= 2)
maybeUse(inst.d); // Argument can also be a VmConst visitor.maybeUse(inst.d); // Argument can also be a VmConst
} }
} }
else else
{ {
useVarargs(vmRegOp(inst.c)); visitor.useVarargs(vmRegOp(inst.c));
} }
// Multiple return sequences (count == -1) are defined by ADJUST_STACK_TO_REG // Multiple return sequences (count == -1) are defined by ADJUST_STACK_TO_REG
if (int count = function.intOp(inst.f); count != -1) if (int count = function.intOp(inst.f); count != -1)
defRange(vmRegOp(inst.b), count); visitor.defRange(vmRegOp(inst.b), count);
break; break;
case IrCmd::FORGLOOP: case IrCmd::FORGLOOP:
// First register is not used by instruction, we check that it's still 'nil' with CHECK_TAG // First register is not used by instruction, we check that it's still 'nil' with CHECK_TAG
use(inst.a, 1); visitor.use(inst.a, 1);
use(inst.a, 2); visitor.use(inst.a, 2);
def(inst.a, 2); visitor.def(inst.a, 2);
defRange(vmRegOp(inst.a) + 3, function.intOp(inst.b)); visitor.defRange(vmRegOp(inst.a) + 3, function.intOp(inst.b));
break; break;
case IrCmd::FORGLOOP_FALLBACK: case IrCmd::FORGLOOP_FALLBACK:
useRange(vmRegOp(inst.a), 3); visitor.useRange(vmRegOp(inst.a), 3);
def(inst.a, 2); visitor.def(inst.a, 2);
defRange(vmRegOp(inst.a) + 3, uint8_t(function.intOp(inst.b))); // ignore most significant bit visitor.defRange(vmRegOp(inst.a) + 3, uint8_t(function.intOp(inst.b))); // ignore most significant bit
break; break;
case IrCmd::FORGPREP_XNEXT_FALLBACK: case IrCmd::FORGPREP_XNEXT_FALLBACK:
use(inst.b); visitor.use(inst.b);
break; break;
case IrCmd::FALLBACK_GETGLOBAL: case IrCmd::FALLBACK_GETGLOBAL:
def(inst.b); visitor.def(inst.b);
break; break;
case IrCmd::FALLBACK_SETGLOBAL: case IrCmd::FALLBACK_SETGLOBAL:
use(inst.b); visitor.use(inst.b);
break; break;
case IrCmd::FALLBACK_GETTABLEKS: case IrCmd::FALLBACK_GETTABLEKS:
use(inst.c); visitor.use(inst.c);
def(inst.b); visitor.def(inst.b);
break; break;
case IrCmd::FALLBACK_SETTABLEKS: case IrCmd::FALLBACK_SETTABLEKS:
use(inst.b); visitor.use(inst.b);
use(inst.c); visitor.use(inst.c);
break; break;
case IrCmd::FALLBACK_NAMECALL: case IrCmd::FALLBACK_NAMECALL:
use(inst.c); visitor.use(inst.c);
defRange(vmRegOp(inst.b), 2); visitor.defRange(vmRegOp(inst.b), 2);
break; break;
case IrCmd::FALLBACK_PREPVARARGS: case IrCmd::FALLBACK_PREPVARARGS:
// No effect on explicitly referenced registers // No effect on explicitly referenced registers
break; break;
case IrCmd::FALLBACK_GETVARARGS: case IrCmd::FALLBACK_GETVARARGS:
defRange(vmRegOp(inst.b), function.intOp(inst.c)); visitor.defRange(vmRegOp(inst.b), function.intOp(inst.c));
break; break;
case IrCmd::FALLBACK_DUPCLOSURE: case IrCmd::FALLBACK_DUPCLOSURE:
def(inst.b); visitor.def(inst.b);
break; break;
case IrCmd::FALLBACK_FORGPREP: case IrCmd::FALLBACK_FORGPREP:
use(inst.b); visitor.use(inst.b);
defRange(vmRegOp(inst.b), 3); visitor.defRange(vmRegOp(inst.b), 3);
break; break;
case IrCmd::ADJUST_STACK_TO_REG: case IrCmd::ADJUST_STACK_TO_REG:
defRange(vmRegOp(inst.a), -1); visitor.defRange(vmRegOp(inst.a), -1);
break; break;
case IrCmd::ADJUST_STACK_TO_TOP: case IrCmd::ADJUST_STACK_TO_TOP:
// While this can be considered to be a vararg consumer, it is already handled in fastcall instructions // While this can be considered to be a vararg consumer, it is already handled in fastcall instructions
break; break;
case IrCmd::GET_TYPEOF: case IrCmd::GET_TYPEOF:
use(inst.a); visitor.use(inst.a);
break; break;
case IrCmd::FINDUPVAL: case IrCmd::FINDUPVAL:
use(inst.a); visitor.use(inst.a);
break; break;
default: default:
@ -460,8 +397,102 @@ static RegisterSet computeBlockLiveInRegSet(IrFunction& function, const IrBlock&
break; break;
} }
} }
}
return inRs; struct BlockVmRegLiveInComputation
{
BlockVmRegLiveInComputation(RegisterSet& defRs, std::bitset<256>& capturedRegs)
: defRs(defRs)
, capturedRegs(capturedRegs)
{
}
RegisterSet& defRs;
std::bitset<256>& capturedRegs;
RegisterSet inRs;
void def(IrOp op, int offset = 0)
{
defRs.regs.set(vmRegOp(op) + offset, true);
}
void use(IrOp op, int offset = 0)
{
if (!defRs.regs.test(vmRegOp(op) + offset))
inRs.regs.set(vmRegOp(op) + offset, true);
}
void maybeDef(IrOp op)
{
if (op.kind == IrOpKind::VmReg)
defRs.regs.set(vmRegOp(op), true);
}
void maybeUse(IrOp op)
{
if (op.kind == IrOpKind::VmReg)
{
if (!defRs.regs.test(vmRegOp(op)))
inRs.regs.set(vmRegOp(op), true);
}
}
void defVarargs(uint8_t varargStart)
{
defRs.varargSeq = true;
defRs.varargStart = varargStart;
}
void useVarargs(uint8_t varargStart)
{
requireVariadicSequence(inRs, defRs, varargStart);
// Variadic sequence has been consumed
defRs.varargSeq = false;
defRs.varargStart = 0;
}
void defRange(int start, int count)
{
if (count == -1)
{
defVarargs(start);
}
else
{
for (int i = start; i < start + count; i++)
defRs.regs.set(i, true);
}
}
void useRange(int start, int count)
{
if (count == -1)
{
useVarargs(start);
}
else
{
for (int i = start; i < start + count; i++)
{
if (!defRs.regs.test(i))
inRs.regs.set(i, true);
}
}
}
void capture(int reg)
{
capturedRegs.set(reg, true);
}
};
static RegisterSet computeBlockLiveInRegSet(IrFunction& function, const IrBlock& block, RegisterSet& defRs, std::bitset<256>& capturedRegs)
{
BlockVmRegLiveInComputation visitor(defRs, capturedRegs);
visitVmRegDefsUses(visitor, function, block);
return visitor.inRs;
} }
// The algorithm used here is commonly known as backwards data-flow analysis. // The algorithm used here is commonly known as backwards data-flow analysis.

View file

@ -1,7 +1,7 @@
// 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/IrBuilder.h" #include "Luau/IrBuilder.h"
#include "Luau/IrAnalysis.h" #include "Luau/IrData.h"
#include "Luau/IrUtils.h" #include "Luau/IrUtils.h"
#include "IrTranslation.h" #include "IrTranslation.h"
@ -22,10 +22,14 @@ IrBuilder::IrBuilder()
{ {
} }
static bool hasTypedParameters(Proto* proto)
{
return proto->typeinfo && proto->numparams != 0;
}
static void buildArgumentTypeChecks(IrBuilder& build, Proto* proto) static void buildArgumentTypeChecks(IrBuilder& build, Proto* proto)
{ {
if (!proto->typeinfo || proto->numparams == 0) LUAU_ASSERT(hasTypedParameters(proto));
return;
for (int i = 0; i < proto->numparams; ++i) for (int i = 0; i < proto->numparams; ++i)
{ {
@ -53,31 +57,31 @@ static void buildArgumentTypeChecks(IrBuilder& build, Proto* proto)
switch (tag) switch (tag)
{ {
case LBC_TYPE_NIL: case LBC_TYPE_NIL:
build.inst(IrCmd::CHECK_TAG, load, build.constTag(LUA_TNIL), build.undef(), build.constInt(1)); build.inst(IrCmd::CHECK_TAG, load, build.constTag(LUA_TNIL), build.vmExit(kVmExitEntryGuardPc));
break; break;
case LBC_TYPE_BOOLEAN: case LBC_TYPE_BOOLEAN:
build.inst(IrCmd::CHECK_TAG, load, build.constTag(LUA_TBOOLEAN), build.undef(), build.constInt(1)); build.inst(IrCmd::CHECK_TAG, load, build.constTag(LUA_TBOOLEAN), build.vmExit(kVmExitEntryGuardPc));
break; break;
case LBC_TYPE_NUMBER: case LBC_TYPE_NUMBER:
build.inst(IrCmd::CHECK_TAG, load, build.constTag(LUA_TNUMBER), build.undef(), build.constInt(1)); build.inst(IrCmd::CHECK_TAG, load, build.constTag(LUA_TNUMBER), build.vmExit(kVmExitEntryGuardPc));
break; break;
case LBC_TYPE_STRING: case LBC_TYPE_STRING:
build.inst(IrCmd::CHECK_TAG, load, build.constTag(LUA_TSTRING), build.undef(), build.constInt(1)); build.inst(IrCmd::CHECK_TAG, load, build.constTag(LUA_TSTRING), build.vmExit(kVmExitEntryGuardPc));
break; break;
case LBC_TYPE_TABLE: case LBC_TYPE_TABLE:
build.inst(IrCmd::CHECK_TAG, load, build.constTag(LUA_TTABLE), build.undef(), build.constInt(1)); build.inst(IrCmd::CHECK_TAG, load, build.constTag(LUA_TTABLE), build.vmExit(kVmExitEntryGuardPc));
break; break;
case LBC_TYPE_FUNCTION: case LBC_TYPE_FUNCTION:
build.inst(IrCmd::CHECK_TAG, load, build.constTag(LUA_TFUNCTION), build.undef(), build.constInt(1)); build.inst(IrCmd::CHECK_TAG, load, build.constTag(LUA_TFUNCTION), build.vmExit(kVmExitEntryGuardPc));
break; break;
case LBC_TYPE_THREAD: case LBC_TYPE_THREAD:
build.inst(IrCmd::CHECK_TAG, load, build.constTag(LUA_TTHREAD), build.undef(), build.constInt(1)); build.inst(IrCmd::CHECK_TAG, load, build.constTag(LUA_TTHREAD), build.vmExit(kVmExitEntryGuardPc));
break; break;
case LBC_TYPE_USERDATA: case LBC_TYPE_USERDATA:
build.inst(IrCmd::CHECK_TAG, load, build.constTag(LUA_TUSERDATA), build.undef(), build.constInt(1)); build.inst(IrCmd::CHECK_TAG, load, build.constTag(LUA_TUSERDATA), build.vmExit(kVmExitEntryGuardPc));
break; break;
case LBC_TYPE_VECTOR: case LBC_TYPE_VECTOR:
build.inst(IrCmd::CHECK_TAG, load, build.constTag(LUA_TVECTOR), build.undef(), build.constInt(1)); build.inst(IrCmd::CHECK_TAG, load, build.constTag(LUA_TVECTOR), build.vmExit(kVmExitEntryGuardPc));
break; break;
} }
@ -103,11 +107,28 @@ void IrBuilder::buildFunctionIr(Proto* proto)
function.proto = proto; function.proto = proto;
function.variadic = proto->is_vararg != 0; function.variadic = proto->is_vararg != 0;
// Reserve entry block
bool generateTypeChecks = hasTypedParameters(proto);
IrOp entry = generateTypeChecks ? block(IrBlockKind::Internal) : IrOp{};
// Rebuild original control flow blocks // Rebuild original control flow blocks
rebuildBytecodeBasicBlocks(proto); rebuildBytecodeBasicBlocks(proto);
function.bcMapping.resize(proto->sizecode, {~0u, ~0u}); function.bcMapping.resize(proto->sizecode, {~0u, ~0u});
if (generateTypeChecks)
{
beginBlock(entry);
buildArgumentTypeChecks(*this, proto);
inst(IrCmd::JUMP, blockAtInst(0));
}
else
{
entry = blockAtInst(0);
}
function.entryBlock = entry.index;
// Translate all instructions to IR inside blocks // Translate all instructions to IR inside blocks
for (int i = 0; i < proto->sizecode;) for (int i = 0; i < proto->sizecode;)
{ {
@ -123,9 +144,6 @@ void IrBuilder::buildFunctionIr(Proto* proto)
if (instIndexToBlock[i] != kNoAssociatedBlockIndex) if (instIndexToBlock[i] != kNoAssociatedBlockIndex)
beginBlock(blockAtInst(i)); beginBlock(blockAtInst(i));
if (i == 0)
buildArgumentTypeChecks(*this, proto);
// We skip dead bytecode instructions when they appear after block was already terminated // We skip dead bytecode instructions when they appear after block was already terminated
if (!inTerminatedBlock) if (!inTerminatedBlock)
{ {

View file

@ -402,7 +402,7 @@ void toString(IrToStringContext& ctx, IrOp op)
append(ctx.result, "U%d", vmUpvalueOp(op)); append(ctx.result, "U%d", vmUpvalueOp(op));
break; break;
case IrOpKind::VmExit: case IrOpKind::VmExit:
append(ctx.result, "exit(%d)", op.index); append(ctx.result, "exit(%d)", vmExitOp(op));
break; break;
} }
} }

View file

@ -1,10 +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 "IrLoweringA64.h" #include "IrLoweringA64.h"
#include "Luau/CodeGen.h"
#include "Luau/DenseHash.h" #include "Luau/DenseHash.h"
#include "Luau/IrAnalysis.h" #include "Luau/IrData.h"
#include "Luau/IrDump.h"
#include "Luau/IrUtils.h" #include "Luau/IrUtils.h"
#include "EmitCommonA64.h" #include "EmitCommonA64.h"
@ -189,7 +187,7 @@ IrLoweringA64::IrLoweringA64(AssemblyBuilderA64& build, ModuleHelpers& helpers,
}); });
} }
void IrLoweringA64::lowerInst(IrInst& inst, uint32_t index, IrBlock& next) void IrLoweringA64::lowerInst(IrInst& inst, uint32_t index, const IrBlock& next)
{ {
valueTracker.beforeInstLowering(inst); valueTracker.beforeInstLowering(inst);
@ -566,7 +564,7 @@ void IrLoweringA64::lowerInst(IrInst& inst, uint32_t index, IrBlock& next)
break; break;
} }
case IrCmd::JUMP: case IrCmd::JUMP:
if (inst.a.kind == IrOpKind::VmExit) if (inst.a.kind == IrOpKind::Undef || inst.a.kind == IrOpKind::VmExit)
{ {
Label fresh; Label fresh;
build.b(getTargetLabel(inst.a, fresh)); build.b(getTargetLabel(inst.a, fresh));
@ -1047,9 +1045,8 @@ void IrLoweringA64::lowerInst(IrInst& inst, uint32_t index, IrBlock& next)
break; break;
case IrCmd::CHECK_TAG: case IrCmd::CHECK_TAG:
{ {
bool continueInVm = (inst.d.kind == IrOpKind::Constant && intOp(inst.d));
Label fresh; // used when guard aborts execution or jumps to a VM exit Label fresh; // used when guard aborts execution or jumps to a VM exit
Label& fail = continueInVm ? helpers.exitContinueVmClearNativeFlag : getTargetLabel(inst.c, fresh); Label& fail = getTargetLabel(inst.c, fresh);
// To support DebugLuauAbortingChecks, CHECK_TAG with VmReg has to be handled // To support DebugLuauAbortingChecks, CHECK_TAG with VmReg has to be handled
RegisterA64 tag = inst.a.kind == IrOpKind::VmReg ? regs.allocTemp(KindA64::w) : regOp(inst.a); RegisterA64 tag = inst.a.kind == IrOpKind::VmReg ? regs.allocTemp(KindA64::w) : regOp(inst.a);
@ -1066,7 +1063,7 @@ void IrLoweringA64::lowerInst(IrInst& inst, uint32_t index, IrBlock& next)
build.cmp(tag, tagOp(inst.b)); build.cmp(tag, tagOp(inst.b));
build.b(ConditionA64::NotEqual, fail); build.b(ConditionA64::NotEqual, fail);
} }
if (!continueInVm)
finalizeTargetLabel(inst.c, fresh); finalizeTargetLabel(inst.c, fresh);
break; break;
} }
@ -1862,7 +1859,10 @@ void IrLoweringA64::finishFunction()
for (ExitHandler& handler : exitHandlers) for (ExitHandler& handler : exitHandlers)
{ {
LUAU_ASSERT(handler.pcpos != kVmExitEntryGuardPc);
build.setLabel(handler.self); build.setLabel(handler.self);
build.mov(x0, handler.pcpos * sizeof(Instruction)); build.mov(x0, handler.pcpos * sizeof(Instruction));
build.b(helpers.updatePcAndContinueInVm); build.b(helpers.updatePcAndContinueInVm);
} }
@ -1873,12 +1873,12 @@ bool IrLoweringA64::hasError() const
return error || regs.error; return error || regs.error;
} }
bool IrLoweringA64::isFallthroughBlock(IrBlock target, IrBlock next) bool IrLoweringA64::isFallthroughBlock(const IrBlock& target, const IrBlock& next)
{ {
return target.start == next.start; return target.start == next.start;
} }
void IrLoweringA64::jumpOrFallthrough(IrBlock& target, IrBlock& next) void IrLoweringA64::jumpOrFallthrough(IrBlock& target, const IrBlock& next)
{ {
if (!isFallthroughBlock(target, next)) if (!isFallthroughBlock(target, next))
build.b(target.label); build.b(target.label);
@ -1891,7 +1891,11 @@ Label& IrLoweringA64::getTargetLabel(IrOp op, Label& fresh)
if (op.kind == IrOpKind::VmExit) if (op.kind == IrOpKind::VmExit)
{ {
if (uint32_t* index = exitHandlerMap.find(op.index)) // Special exit case that doesn't have to update pcpos
if (vmExitOp(op) == kVmExitEntryGuardPc)
return helpers.exitContinueVmClearNativeFlag;
if (uint32_t* index = exitHandlerMap.find(vmExitOp(op)))
return exitHandlers[*index].self; return exitHandlers[*index].self;
return fresh; return fresh;
@ -1906,10 +1910,10 @@ void IrLoweringA64::finalizeTargetLabel(IrOp op, Label& fresh)
{ {
emitAbort(build, fresh); emitAbort(build, fresh);
} }
else if (op.kind == IrOpKind::VmExit && fresh.id != 0) else if (op.kind == IrOpKind::VmExit && fresh.id != 0 && fresh.id != helpers.exitContinueVmClearNativeFlag.id)
{ {
exitHandlerMap[op.index] = uint32_t(exitHandlers.size()); exitHandlerMap[vmExitOp(op)] = uint32_t(exitHandlers.size());
exitHandlers.push_back({fresh, op.index}); exitHandlers.push_back({fresh, vmExitOp(op)});
} }
} }

View file

@ -25,14 +25,14 @@ struct IrLoweringA64
{ {
IrLoweringA64(AssemblyBuilderA64& build, ModuleHelpers& helpers, IrFunction& function); IrLoweringA64(AssemblyBuilderA64& build, ModuleHelpers& helpers, IrFunction& function);
void lowerInst(IrInst& inst, uint32_t index, IrBlock& next); void lowerInst(IrInst& inst, uint32_t index, const IrBlock& next);
void finishBlock(); void finishBlock();
void finishFunction(); void finishFunction();
bool hasError() const; bool hasError() const;
bool isFallthroughBlock(IrBlock target, IrBlock next); bool isFallthroughBlock(const IrBlock& target, const IrBlock& next);
void jumpOrFallthrough(IrBlock& target, IrBlock& next); void jumpOrFallthrough(IrBlock& target, const IrBlock& next);
Label& getTargetLabel(IrOp op, Label& fresh); Label& getTargetLabel(IrOp op, Label& fresh);
void finalizeTargetLabel(IrOp op, Label& fresh); void finalizeTargetLabel(IrOp op, Label& fresh);

View file

@ -1,19 +1,19 @@
// 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 "IrLoweringX64.h" #include "IrLoweringX64.h"
#include "Luau/CodeGen.h"
#include "Luau/DenseHash.h" #include "Luau/DenseHash.h"
#include "Luau/IrAnalysis.h" #include "Luau/IrData.h"
#include "Luau/IrCallWrapperX64.h"
#include "Luau/IrDump.h"
#include "Luau/IrUtils.h" #include "Luau/IrUtils.h"
#include "Luau/IrCallWrapperX64.h"
#include "EmitBuiltinsX64.h" #include "EmitBuiltinsX64.h"
#include "EmitCommonX64.h" #include "EmitCommonX64.h"
#include "EmitInstructionX64.h" #include "EmitInstructionX64.h"
#include "NativeState.h" #include "NativeState.h"
#include "lstate.h" #include "lstate.h"
#include "lgc.h"
namespace Luau namespace Luau
{ {
@ -59,7 +59,7 @@ void IrLoweringX64::storeDoubleAsFloat(OperandX64 dst, IrOp src)
build.vmovss(dst, tmp.reg); build.vmovss(dst, tmp.reg);
} }
void IrLoweringX64::lowerInst(IrInst& inst, uint32_t index, IrBlock& next) void IrLoweringX64::lowerInst(IrInst& inst, uint32_t index, const IrBlock& next)
{ {
regs.currInstIdx = index; regs.currInstIdx = index;
@ -565,24 +565,7 @@ void IrLoweringX64::lowerInst(IrInst& inst, uint32_t index, IrBlock& next)
break; break;
} }
case IrCmd::JUMP: case IrCmd::JUMP:
if (inst.a.kind == IrOpKind::VmExit) jumpOrAbortOnUndef(inst.a, next);
{
if (uint32_t* index = exitHandlerMap.find(inst.a.index))
{
build.jmp(exitHandlers[*index].self);
}
else
{
Label self;
build.jmp(self);
exitHandlerMap[inst.a.index] = uint32_t(exitHandlers.size());
exitHandlers.push_back({self, inst.a.index});
}
}
else
{
jumpOrFallthrough(blockOp(inst.a), next);
}
break; break;
case IrCmd::JUMP_IF_TRUTHY: case IrCmd::JUMP_IF_TRUTHY:
jumpIfTruthy(build, vmRegOp(inst.a), labelOp(inst.b), labelOp(inst.c)); jumpIfTruthy(build, vmRegOp(inst.a), labelOp(inst.b), labelOp(inst.c));
@ -975,12 +958,9 @@ void IrLoweringX64::lowerInst(IrInst& inst, uint32_t index, IrBlock& next)
callPrepareForN(regs, build, vmRegOp(inst.a), vmRegOp(inst.b), vmRegOp(inst.c)); callPrepareForN(regs, build, vmRegOp(inst.a), vmRegOp(inst.b), vmRegOp(inst.c));
break; break;
case IrCmd::CHECK_TAG: case IrCmd::CHECK_TAG:
{
bool continueInVm = (inst.d.kind == IrOpKind::Constant && intOp(inst.d));
build.cmp(memRegTagOp(inst.a), tagOp(inst.b)); build.cmp(memRegTagOp(inst.a), tagOp(inst.b));
jumpOrAbortOnUndef(ConditionX64::NotEqual, ConditionX64::Equal, inst.c, continueInVm); jumpOrAbortOnUndef(ConditionX64::NotEqual, inst.c, next);
break; break;
}
case IrCmd::CHECK_TRUTHY: case IrCmd::CHECK_TRUTHY:
{ {
// Constant tags which don't require boolean value check should've been removed in constant folding // Constant tags which don't require boolean value check should've been removed in constant folding
@ -992,7 +972,7 @@ void IrLoweringX64::lowerInst(IrInst& inst, uint32_t index, IrBlock& next)
{ {
// Fail to fallback on 'nil' (falsy) // Fail to fallback on 'nil' (falsy)
build.cmp(memRegTagOp(inst.a), LUA_TNIL); build.cmp(memRegTagOp(inst.a), LUA_TNIL);
jumpOrAbortOnUndef(ConditionX64::Equal, ConditionX64::NotEqual, inst.c); jumpOrAbortOnUndef(ConditionX64::Equal, inst.c, next);
// Skip value test if it's not a boolean (truthy) // Skip value test if it's not a boolean (truthy)
build.cmp(memRegTagOp(inst.a), LUA_TBOOLEAN); build.cmp(memRegTagOp(inst.a), LUA_TBOOLEAN);
@ -1001,7 +981,7 @@ void IrLoweringX64::lowerInst(IrInst& inst, uint32_t index, IrBlock& next)
// fail to fallback on 'false' boolean value (falsy) // fail to fallback on 'false' boolean value (falsy)
build.cmp(memRegUintOp(inst.b), 0); build.cmp(memRegUintOp(inst.b), 0);
jumpOrAbortOnUndef(ConditionX64::Equal, ConditionX64::NotEqual, inst.c); jumpOrAbortOnUndef(ConditionX64::Equal, inst.c, next);
if (inst.a.kind != IrOpKind::Constant) if (inst.a.kind != IrOpKind::Constant)
build.setLabel(skip); build.setLabel(skip);
@ -1009,11 +989,11 @@ void IrLoweringX64::lowerInst(IrInst& inst, uint32_t index, IrBlock& next)
} }
case IrCmd::CHECK_READONLY: case IrCmd::CHECK_READONLY:
build.cmp(byte[regOp(inst.a) + offsetof(Table, readonly)], 0); build.cmp(byte[regOp(inst.a) + offsetof(Table, readonly)], 0);
jumpOrAbortOnUndef(ConditionX64::NotEqual, ConditionX64::Equal, inst.b); jumpOrAbortOnUndef(ConditionX64::NotEqual, inst.b, next);
break; break;
case IrCmd::CHECK_NO_METATABLE: case IrCmd::CHECK_NO_METATABLE:
build.cmp(qword[regOp(inst.a) + offsetof(Table, metatable)], 0); build.cmp(qword[regOp(inst.a) + offsetof(Table, metatable)], 0);
jumpOrAbortOnUndef(ConditionX64::NotEqual, ConditionX64::Equal, inst.b); jumpOrAbortOnUndef(ConditionX64::NotEqual, inst.b, next);
break; break;
case IrCmd::CHECK_SAFE_ENV: case IrCmd::CHECK_SAFE_ENV:
{ {
@ -1023,7 +1003,7 @@ void IrLoweringX64::lowerInst(IrInst& inst, uint32_t index, IrBlock& next)
build.mov(tmp.reg, qword[tmp.reg + offsetof(Closure, env)]); build.mov(tmp.reg, qword[tmp.reg + offsetof(Closure, env)]);
build.cmp(byte[tmp.reg + offsetof(Table, safeenv)], 0); build.cmp(byte[tmp.reg + offsetof(Table, safeenv)], 0);
jumpOrAbortOnUndef(ConditionX64::Equal, ConditionX64::NotEqual, inst.a); jumpOrAbortOnUndef(ConditionX64::Equal, inst.a, next);
break; break;
} }
case IrCmd::CHECK_ARRAY_SIZE: case IrCmd::CHECK_ARRAY_SIZE:
@ -1034,7 +1014,7 @@ void IrLoweringX64::lowerInst(IrInst& inst, uint32_t index, IrBlock& next)
else else
LUAU_ASSERT(!"Unsupported instruction form"); LUAU_ASSERT(!"Unsupported instruction form");
jumpOrAbortOnUndef(ConditionX64::BelowEqual, ConditionX64::NotBelowEqual, inst.c); jumpOrAbortOnUndef(ConditionX64::BelowEqual, inst.c, next);
break; break;
case IrCmd::JUMP_SLOT_MATCH: case IrCmd::JUMP_SLOT_MATCH:
case IrCmd::CHECK_SLOT_MATCH: case IrCmd::CHECK_SLOT_MATCH:
@ -1080,7 +1060,7 @@ void IrLoweringX64::lowerInst(IrInst& inst, uint32_t index, IrBlock& next)
build.mov(tmp.reg, dword[regOp(inst.a) + offsetof(LuaNode, key) + kOffsetOfTKeyTagNext]); build.mov(tmp.reg, dword[regOp(inst.a) + offsetof(LuaNode, key) + kOffsetOfTKeyTagNext]);
build.shr(tmp.reg, kTKeyTagBits); build.shr(tmp.reg, kTKeyTagBits);
jumpOrAbortOnUndef(ConditionX64::NotZero, ConditionX64::Zero, inst.b); jumpOrAbortOnUndef(ConditionX64::NotZero, inst.b, next);
break; break;
} }
case IrCmd::INTERRUPT: case IrCmd::INTERRUPT:
@ -1583,7 +1563,10 @@ void IrLoweringX64::finishFunction()
for (ExitHandler& handler : exitHandlers) for (ExitHandler& handler : exitHandlers)
{ {
LUAU_ASSERT(handler.pcpos != kVmExitEntryGuardPc);
build.setLabel(handler.self); build.setLabel(handler.self);
build.mov(edx, handler.pcpos * sizeof(Instruction)); build.mov(edx, handler.pcpos * sizeof(Instruction));
build.jmp(helpers.updatePcAndContinueInVm); build.jmp(helpers.updatePcAndContinueInVm);
} }
@ -1598,50 +1581,81 @@ bool IrLoweringX64::hasError() const
return false; return false;
} }
bool IrLoweringX64::isFallthroughBlock(IrBlock target, IrBlock next) bool IrLoweringX64::isFallthroughBlock(const IrBlock& target, const IrBlock& next)
{ {
return target.start == next.start; return target.start == next.start;
} }
void IrLoweringX64::jumpOrFallthrough(IrBlock& target, IrBlock& next) Label& IrLoweringX64::getTargetLabel(IrOp op, Label& fresh)
{
if (op.kind == IrOpKind::Undef)
return fresh;
if (op.kind == IrOpKind::VmExit)
{
// Special exit case that doesn't have to update pcpos
if (vmExitOp(op) == kVmExitEntryGuardPc)
return helpers.exitContinueVmClearNativeFlag;
if (uint32_t* index = exitHandlerMap.find(vmExitOp(op)))
return exitHandlers[*index].self;
return fresh;
}
return labelOp(op);
}
void IrLoweringX64::finalizeTargetLabel(IrOp op, Label& fresh)
{
if (op.kind == IrOpKind::VmExit && fresh.id != 0 && fresh.id != helpers.exitContinueVmClearNativeFlag.id)
{
exitHandlerMap[vmExitOp(op)] = uint32_t(exitHandlers.size());
exitHandlers.push_back({fresh, vmExitOp(op)});
}
}
void IrLoweringX64::jumpOrFallthrough(IrBlock& target, const IrBlock& next)
{ {
if (!isFallthroughBlock(target, next)) if (!isFallthroughBlock(target, next))
build.jmp(target.label); build.jmp(target.label);
} }
void IrLoweringX64::jumpOrAbortOnUndef(ConditionX64 cond, ConditionX64 condInverse, IrOp targetOrUndef, bool continueInVm) void IrLoweringX64::jumpOrAbortOnUndef(ConditionX64 cond, IrOp target, const IrBlock& next)
{ {
if (targetOrUndef.kind == IrOpKind::Undef) Label fresh;
Label& label = getTargetLabel(target, fresh);
if (target.kind == IrOpKind::Undef)
{ {
if (continueInVm) if (cond == ConditionX64::Count)
{ {
build.jcc(cond, helpers.exitContinueVmClearNativeFlag); build.ud2(); // Unconditional jump to abort is just an abort
return; }
else
{
build.jcc(getReverseCondition(cond), label);
build.ud2();
build.setLabel(label);
}
}
else if (cond == ConditionX64::Count)
{
// Unconditional jump can be skipped if it's a fallthrough
if (target.kind == IrOpKind::VmExit || !isFallthroughBlock(blockOp(target), next))
build.jmp(label);
}
else
{
build.jcc(cond, label);
} }
Label skip; finalizeTargetLabel(target, fresh);
build.jcc(condInverse, skip);
build.ud2();
build.setLabel(skip);
} }
else if (targetOrUndef.kind == IrOpKind::VmExit)
void IrLoweringX64::jumpOrAbortOnUndef(IrOp target, const IrBlock& next)
{ {
if (uint32_t* index = exitHandlerMap.find(targetOrUndef.index)) jumpOrAbortOnUndef(ConditionX64::Count, target, next);
{
build.jcc(cond, exitHandlers[*index].self);
}
else
{
Label self;
build.jcc(cond, self);
exitHandlerMap[targetOrUndef.index] = uint32_t(exitHandlers.size());
exitHandlers.push_back({self, targetOrUndef.index});
}
}
else
{
build.jcc(cond, labelOp(targetOrUndef));
}
} }
OperandX64 IrLoweringX64::memRegDoubleOp(IrOp op) OperandX64 IrLoweringX64::memRegDoubleOp(IrOp op)

View file

@ -27,15 +27,20 @@ struct IrLoweringX64
{ {
IrLoweringX64(AssemblyBuilderX64& build, ModuleHelpers& helpers, IrFunction& function); IrLoweringX64(AssemblyBuilderX64& build, ModuleHelpers& helpers, IrFunction& function);
void lowerInst(IrInst& inst, uint32_t index, IrBlock& next); void lowerInst(IrInst& inst, uint32_t index, const IrBlock& next);
void finishBlock(); void finishBlock();
void finishFunction(); void finishFunction();
bool hasError() const; bool hasError() const;
bool isFallthroughBlock(IrBlock target, IrBlock next); bool isFallthroughBlock(const IrBlock& target, const IrBlock& next);
void jumpOrFallthrough(IrBlock& target, IrBlock& next); void jumpOrFallthrough(IrBlock& target, const IrBlock& next);
void jumpOrAbortOnUndef(ConditionX64 cond, ConditionX64 condInverse, IrOp targetOrUndef, bool continueInVm = false);
Label& getTargetLabel(IrOp op, Label& fresh);
void finalizeTargetLabel(IrOp op, Label& fresh);
void jumpOrAbortOnUndef(ConditionX64 cond, IrOp target, const IrBlock& next);
void jumpOrAbortOnUndef(IrOp target, const IrBlock& next);
void storeDoubleAsFloat(OperandX64 dst, IrOp src); void storeDoubleAsFloat(OperandX64 dst, IrOp src);

View file

@ -23,7 +23,12 @@ constexpr unsigned kBlockSize = 4 * 1024 * 1024;
constexpr unsigned kMaxTotalSize = 256 * 1024 * 1024; constexpr unsigned kMaxTotalSize = 256 * 1024 * 1024;
NativeState::NativeState() NativeState::NativeState()
: codeAllocator(kBlockSize, kMaxTotalSize) : NativeState(nullptr, nullptr)
{
}
NativeState::NativeState(AllocationCallback* allocationCallback, void* allocationCallbackContext)
: codeAllocator{kBlockSize, kMaxTotalSize, allocationCallback, allocationCallbackContext}
{ {
} }

View file

@ -112,6 +112,7 @@ using GateFn = int (*)(lua_State*, Proto*, uintptr_t, NativeContext*);
struct NativeState struct NativeState
{ {
NativeState(); NativeState();
NativeState(AllocationCallback* allocationCallback, void* allocationCallbackContext);
~NativeState(); ~NativeState();
CodeAllocator codeAllocator; CodeAllocator codeAllocator;

View file

@ -2,7 +2,7 @@
#include "Luau/OptimizeConstProp.h" #include "Luau/OptimizeConstProp.h"
#include "Luau/DenseHash.h" #include "Luau/DenseHash.h"
#include "Luau/IrAnalysis.h" #include "Luau/IrData.h"
#include "Luau/IrBuilder.h" #include "Luau/IrBuilder.h"
#include "Luau/IrUtils.h" #include "Luau/IrUtils.h"

View file

@ -26,9 +26,6 @@ LUAU_FASTINTVARIABLE(LuauCompileInlineThreshold, 25)
LUAU_FASTINTVARIABLE(LuauCompileInlineThresholdMaxBoost, 300) LUAU_FASTINTVARIABLE(LuauCompileInlineThresholdMaxBoost, 300)
LUAU_FASTINTVARIABLE(LuauCompileInlineDepth, 5) LUAU_FASTINTVARIABLE(LuauCompileInlineDepth, 5)
LUAU_FASTFLAGVARIABLE(LuauCompileFunctionType, false)
LUAU_FASTFLAGVARIABLE(LuauCompileNativeComment, false)
LUAU_FASTFLAGVARIABLE(LuauCompileFixBuiltinArity, false) LUAU_FASTFLAGVARIABLE(LuauCompileFixBuiltinArity, false)
LUAU_FASTFLAGVARIABLE(LuauCompileFoldMathK, false) LUAU_FASTFLAGVARIABLE(LuauCompileFoldMathK, false)
@ -209,12 +206,9 @@ struct Compiler
setDebugLine(func); setDebugLine(func);
if (FFlag::LuauCompileFunctionType)
{
// note: we move types out of typeMap which is safe because compileFunction is only called once per function // note: we move types out of typeMap which is safe because compileFunction is only called once per function
if (std::string* funcType = typeMap.find(func)) if (std::string* funcType = typeMap.find(func))
bytecode.setFunctionTypeInfo(std::move(*funcType)); bytecode.setFunctionTypeInfo(std::move(*funcType));
}
if (func->vararg) if (func->vararg)
bytecode.emitABC(LOP_PREPVARARGS, uint8_t(self + func->args.size), 0, 0); bytecode.emitABC(LOP_PREPVARARGS, uint8_t(self + func->args.size), 0, 0);
@ -3620,7 +3614,6 @@ struct Compiler
{ {
node->body->visit(this); node->body->visit(this);
if (FFlag::LuauCompileFunctionType)
for (AstLocal* arg : node->args) for (AstLocal* arg : node->args)
hasTypes |= arg->annotation != nullptr; hasTypes |= arg->annotation != nullptr;
@ -3863,7 +3856,7 @@ void compileOrThrow(BytecodeBuilder& bytecode, const ParseResult& parseResult, c
if (hc.header && hc.content.compare(0, 9, "optimize ") == 0) if (hc.header && hc.content.compare(0, 9, "optimize ") == 0)
options.optimizationLevel = std::max(0, std::min(2, atoi(hc.content.c_str() + 9))); options.optimizationLevel = std::max(0, std::min(2, atoi(hc.content.c_str() + 9)));
if (FFlag::LuauCompileNativeComment && hc.header && hc.content == "native") if (hc.header && hc.content == "native")
{ {
mainFlags |= LPF_NATIVE_MODULE; mainFlags |= LPF_NATIVE_MODULE;
options.optimizationLevel = 2; // note: this might be removed in the future in favor of --!optimize options.optimizationLevel = 2; // note: this might be removed in the future in favor of --!optimize
@ -3916,7 +3909,7 @@ void compileOrThrow(BytecodeBuilder& bytecode, const ParseResult& parseResult, c
root->visit(&functionVisitor); root->visit(&functionVisitor);
// computes type information for all functions based on type annotations // computes type information for all functions based on type annotations
if (FFlag::LuauCompileFunctionType && functionVisitor.hasTypes) if (functionVisitor.hasTypes)
buildTypeMap(compiler.typeMap, root, options.vectorType); buildTypeMap(compiler.typeMap, root, options.vectorType);
for (AstExprFunction* expr : functions) for (AstExprFunction* expr : functions)

View file

@ -1,8 +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/BytecodeBuilder.h"
#include "Types.h" #include "Types.h"
#include "Luau/BytecodeBuilder.h"
namespace Luau namespace Luau
{ {

View file

@ -2,6 +2,7 @@
#pragma once #pragma once
#include "Luau/Ast.h" #include "Luau/Ast.h"
#include "Luau/DenseHash.h"
#include <string> #include <string>

View file

@ -163,6 +163,7 @@ target_sources(Luau.Analysis PRIVATE
Analysis/include/Luau/IostreamHelpers.h Analysis/include/Luau/IostreamHelpers.h
Analysis/include/Luau/JsonEmitter.h Analysis/include/Luau/JsonEmitter.h
Analysis/include/Luau/Linter.h Analysis/include/Luau/Linter.h
Analysis/include/Luau/LinterConfig.h
Analysis/include/Luau/LValue.h Analysis/include/Luau/LValue.h
Analysis/include/Luau/Metamethods.h Analysis/include/Luau/Metamethods.h
Analysis/include/Luau/Module.h Analysis/include/Luau/Module.h
@ -221,6 +222,7 @@ target_sources(Luau.Analysis PRIVATE
Analysis/src/IostreamHelpers.cpp Analysis/src/IostreamHelpers.cpp
Analysis/src/JsonEmitter.cpp Analysis/src/JsonEmitter.cpp
Analysis/src/Linter.cpp Analysis/src/Linter.cpp
Analysis/src/LinterConfig.cpp
Analysis/src/LValue.cpp Analysis/src/LValue.cpp
Analysis/src/Module.cpp Analysis/src/Module.cpp
Analysis/src/Normalize.cpp Analysis/src/Normalize.cpp

View file

@ -7,8 +7,6 @@
#include <math.h> #include <math.h>
#include <time.h> #include <time.h>
LUAU_FASTFLAGVARIABLE(LuauFasterNoise, false)
#undef PI #undef PI
#define PI (3.14159265358979323846) #define PI (3.14159265358979323846)
#define RADIANS_PER_DEGREE (PI / 180.0) #define RADIANS_PER_DEGREE (PI / 180.0)
@ -277,27 +275,6 @@ static int math_randomseed(lua_State* L)
return 0; return 0;
} }
// TODO: Delete with LuauFasterNoise
static const unsigned char kPerlin[512] = {151, 160, 137, 91, 90, 15, 131, 13, 201, 95, 96, 53, 194, 233, 7, 225, 140, 36, 103, 30, 69, 142, 8, 99,
37, 240, 21, 10, 23, 190, 6, 148, 247, 120, 234, 75, 0, 26, 197, 62, 94, 252, 219, 203, 117, 35, 11, 32, 57, 177, 33, 88, 237, 149, 56, 87, 174,
20, 125, 136, 171, 168, 68, 175, 74, 165, 71, 134, 139, 48, 27, 166, 77, 146, 158, 231, 83, 111, 229, 122, 60, 211, 133, 230, 220, 105, 92, 41,
55, 46, 245, 40, 244, 102, 143, 54, 65, 25, 63, 161, 1, 216, 80, 73, 209, 76, 132, 187, 208, 89, 18, 169, 200, 196, 135, 130, 116, 188, 159, 86,
164, 100, 109, 198, 173, 186, 3, 64, 52, 217, 226, 250, 124, 123, 5, 202, 38, 147, 118, 126, 255, 82, 85, 212, 207, 206, 59, 227, 47, 16, 58, 17,
182, 189, 28, 42, 223, 183, 170, 213, 119, 248, 152, 2, 44, 154, 163, 70, 221, 153, 101, 155, 167, 43, 172, 9, 129, 22, 39, 253, 19, 98, 108, 110,
79, 113, 224, 232, 178, 185, 112, 104, 218, 246, 97, 228, 251, 34, 242, 193, 238, 210, 144, 12, 191, 179, 162, 241, 81, 51, 145, 235, 249, 14,
239, 107, 49, 192, 214, 31, 181, 199, 106, 157, 184, 84, 204, 176, 115, 121, 50, 45, 127, 4, 150, 254, 138, 236, 205, 93, 222, 114, 67, 29, 24,
72, 243, 141, 128, 195, 78, 66, 215, 61, 156, 180,
151, 160, 137, 91, 90, 15, 131, 13, 201, 95, 96, 53, 194, 233, 7, 225, 140, 36, 103, 30, 69, 142, 8, 99, 37, 240, 21, 10, 23, 190, 6, 148, 247,
120, 234, 75, 0, 26, 197, 62, 94, 252, 219, 203, 117, 35, 11, 32, 57, 177, 33, 88, 237, 149, 56, 87, 174, 20, 125, 136, 171, 168, 68, 175, 74,
165, 71, 134, 139, 48, 27, 166, 77, 146, 158, 231, 83, 111, 229, 122, 60, 211, 133, 230, 220, 105, 92, 41, 55, 46, 245, 40, 244, 102, 143, 54, 65,
25, 63, 161, 1, 216, 80, 73, 209, 76, 132, 187, 208, 89, 18, 169, 200, 196, 135, 130, 116, 188, 159, 86, 164, 100, 109, 198, 173, 186, 3, 64, 52,
217, 226, 250, 124, 123, 5, 202, 38, 147, 118, 126, 255, 82, 85, 212, 207, 206, 59, 227, 47, 16, 58, 17, 182, 189, 28, 42, 223, 183, 170, 213,
119, 248, 152, 2, 44, 154, 163, 70, 221, 153, 101, 155, 167, 43, 172, 9, 129, 22, 39, 253, 19, 98, 108, 110, 79, 113, 224, 232, 178, 185, 112,
104, 218, 246, 97, 228, 251, 34, 242, 193, 238, 210, 144, 12, 191, 179, 162, 241, 81, 51, 145, 235, 249, 14, 239, 107, 49, 192, 214, 31, 181, 199,
106, 157, 184, 84, 204, 176, 115, 121, 50, 45, 127, 4, 150, 254, 138, 236, 205, 93, 222, 114, 67, 29, 24, 72, 243, 141, 128, 195, 78, 66, 215, 61,
156, 180};
static const unsigned char kPerlinHash[257] = {151, 160, 137, 91, 90, 15, 131, 13, 201, 95, 96, 53, 194, 233, 7, 225, 140, 36, 103, 30, 69, 142, 8, 99, static const unsigned char kPerlinHash[257] = {151, 160, 137, 91, 90, 15, 131, 13, 201, 95, 96, 53, 194, 233, 7, 225, 140, 36, 103, 30, 69, 142, 8, 99,
37, 240, 21, 10, 23, 190, 6, 148, 247, 120, 234, 75, 0, 26, 197, 62, 94, 252, 219, 203, 117, 35, 11, 32, 57, 177, 33, 88, 237, 149, 56, 87, 174, 37, 240, 21, 10, 23, 190, 6, 148, 247, 120, 234, 75, 0, 26, 197, 62, 94, 252, 219, 203, 117, 35, 11, 32, 57, 177, 33, 88, 237, 149, 56, 87, 174,
20, 125, 136, 171, 168, 68, 175, 74, 165, 71, 134, 139, 48, 27, 166, 77, 146, 158, 231, 83, 111, 229, 122, 60, 211, 133, 230, 220, 105, 92, 41, 20, 125, 136, 171, 168, 68, 175, 74, 165, 71, 134, 139, 48, 27, 166, 77, 146, 158, 231, 83, 111, 229, 122, 60, 211, 133, 230, 220, 105, 92, 41,
@ -321,61 +298,14 @@ static float perlin_lerp(float t, float a, float b)
return a + t * (b - a); return a + t * (b - a);
} }
static float grad(unsigned char hash, float x, float y, float z)
{
LUAU_ASSERT(!FFlag::LuauFasterNoise);
unsigned char h = hash & 15;
float u = (h < 8) ? x : y;
float v = (h < 4) ? y : (h == 12 || h == 14) ? x : z;
return (h & 1 ? -u : u) + (h & 2 ? -v : v);
}
static float perlin_grad(int hash, float x, float y, float z) static float perlin_grad(int hash, float x, float y, float z)
{ {
const float* g = kPerlinGrad[hash & 15]; const float* g = kPerlinGrad[hash & 15];
return g[0] * x + g[1] * y + g[2] * z; return g[0] * x + g[1] * y + g[2] * z;
} }
static float perlin_dep(float x, float y, float z)
{
LUAU_ASSERT(!FFlag::LuauFasterNoise);
float xflr = floorf(x);
float yflr = floorf(y);
float zflr = floorf(z);
int xi = int(xflr) & 255;
int yi = int(yflr) & 255;
int zi = int(zflr) & 255;
float xf = x - xflr;
float yf = y - yflr;
float zf = z - zflr;
float u = perlin_fade(xf);
float v = perlin_fade(yf);
float w = perlin_fade(zf);
const unsigned char* p = kPerlin;
int a = p[xi] + yi;
int aa = p[a] + zi;
int ab = p[a + 1] + zi;
int b = p[xi + 1] + yi;
int ba = p[b] + zi;
int bb = p[b + 1] + zi;
return perlin_lerp(w,
perlin_lerp(v, perlin_lerp(u, grad(p[aa], xf, yf, zf), grad(p[ba], xf - 1, yf, zf)),
perlin_lerp(u, grad(p[ab], xf, yf - 1, zf), grad(p[bb], xf - 1, yf - 1, zf))),
perlin_lerp(v, perlin_lerp(u, grad(p[aa + 1], xf, yf, zf - 1), grad(p[ba + 1], xf - 1, yf, zf - 1)),
perlin_lerp(u, grad(p[ab + 1], xf, yf - 1, zf - 1), grad(p[bb + 1], xf - 1, yf - 1, zf - 1))));
}
static float perlin(float x, float y, float z) static float perlin(float x, float y, float z)
{ {
LUAU_ASSERT(FFlag::LuauFasterNoise);
float xflr = floorf(x); float xflr = floorf(x);
float yflr = floorf(y); float yflr = floorf(y);
float zflr = floorf(z); float zflr = floorf(z);
@ -411,8 +341,6 @@ static float perlin(float x, float y, float z)
} }
static int math_noise(lua_State* L) static int math_noise(lua_State* L)
{
if (FFlag::LuauFasterNoise)
{ {
int nx, ny, nz; int nx, ny, nz;
double x = lua_tonumberx(L, 1, &nx); double x = lua_tonumberx(L, 1, &nx);
@ -428,18 +356,6 @@ static int math_noise(lua_State* L)
lua_pushnumber(L, r); lua_pushnumber(L, r);
return 1; return 1;
} }
else
{
double x = luaL_checknumber(L, 1);
double y = luaL_optnumber(L, 2, 0.0);
double z = luaL_optnumber(L, 3, 0.0);
double r = perlin_dep((float)x, (float)y, (float)z);
lua_pushnumber(L, r);
return 1;
}
}
static int math_clamp(lua_State* L) static int math_clamp(lua_State* L)
{ {

View file

@ -9,6 +9,7 @@
#include <stdio.h> #include <stdio.h>
LUAU_FASTFLAGVARIABLE(LuauFasterInterp, false) LUAU_FASTFLAGVARIABLE(LuauFasterInterp, false)
LUAU_FASTFLAGVARIABLE(LuauFasterFormatS, false)
// macro to `unsign' a character // macro to `unsign' a character
#define uchar(c) ((unsigned char)(c)) #define uchar(c) ((unsigned char)(c))
@ -1028,6 +1029,22 @@ static int str_format(lua_State* L)
{ {
size_t l; size_t l;
const char* s = luaL_checklstring(L, arg, &l); const char* s = luaL_checklstring(L, arg, &l);
if (FFlag::LuauFasterFormatS)
{
// no precision and string is too long to be formatted, or no format necessary to begin with
if (form[2] == '\0' || (!strchr(form, '.') && l >= 100))
{
luaL_addlstring(&b, s, l, -1);
continue; // skip the `luaL_addlstring' at the end
}
else
{
snprintf(buff, sizeof(buff), form, s);
break;
}
}
else
{
if (!strchr(form, '.') && l >= 100) if (!strchr(form, '.') && l >= 100)
{ {
/* no precision and string is too long to be formatted; /* no precision and string is too long to be formatted;
@ -1042,6 +1059,7 @@ static int str_format(lua_State* L)
break; break;
} }
} }
}
case '*': case '*':
{ {
if (FFlag::LuauFasterInterp || formatItemSize != 1) if (FFlag::LuauFasterInterp || formatItemSize != 1)

View file

@ -10,8 +10,6 @@
#include "ldebug.h" #include "ldebug.h"
#include "lvm.h" #include "lvm.h"
LUAU_FASTFLAGVARIABLE(LuauFasterTableConcat, false)
static int foreachi(lua_State* L) static int foreachi(lua_State* L)
{ {
luaL_checktype(L, 1, LUA_TTABLE); luaL_checktype(L, 1, LUA_TTABLE);
@ -222,7 +220,7 @@ static int tmove(lua_State* L)
static void addfield(lua_State* L, luaL_Buffer* b, int i) static void addfield(lua_State* L, luaL_Buffer* b, int i)
{ {
int tt = lua_rawgeti(L, 1, i); int tt = lua_rawgeti(L, 1, i);
if (FFlag::LuauFasterTableConcat ? (tt != LUA_TSTRING && tt != LUA_TNUMBER) : !lua_isstring(L, -1)) if (tt != LUA_TSTRING && tt != LUA_TNUMBER)
luaL_error(L, "invalid value (%s) at index %d in table for 'concat'", luaL_typename(L, -1), i); luaL_error(L, "invalid value (%s) at index %d in table for 'concat'", luaL_typename(L, -1), i);
luaL_addvalue(b); luaL_addvalue(b);
} }

View file

@ -13,8 +13,6 @@
#include <string.h> #include <string.h>
LUAU_FASTFLAGVARIABLE(LuauLoadCheckGC, false)
// TODO: RAII deallocation doesn't work for longjmp builds if a memory error happens // TODO: RAII deallocation doesn't work for longjmp builds if a memory error happens
template<typename T> template<typename T>
struct TempBuffer struct TempBuffer
@ -181,7 +179,6 @@ int luau_load(lua_State* L, const char* chunkname, const char* data, size_t size
} }
// we will allocate a fair amount of memory so check GC before we do // we will allocate a fair amount of memory so check GC before we do
if (FFlag::LuauLoadCheckGC)
luaC_checkGC(L); luaC_checkGC(L);
// pause GC for the duration of deserialization - some objects we're creating aren't rooted // pause GC for the duration of deserialization - some objects we're creating aren't rooted

View file

@ -36,6 +36,13 @@ bench.runCode(function()
end end
end, "interp: interp number") end, "interp: interp number")
bench.runCode(function()
local ok = "hello!"
for j=1,1e6 do
local _ = string.format("j=%s", ok)
end
end, "interp: %s format")
bench.runCode(function() bench.runCode(function()
local ok = "hello!" local ok = "hello!"
for j=1,1e6 do for j=1,1e6 do

View file

@ -47,6 +47,56 @@ TEST_CASE("CodeAllocation")
CHECK(nativeEntry == nativeData + kCodeAlignment); CHECK(nativeEntry == nativeData + kCodeAlignment);
} }
TEST_CASE("CodeAllocationCallbacks")
{
struct AllocationData
{
size_t bytesAllocated = 0;
size_t bytesFreed = 0;
};
AllocationData allocationData{};
const auto allocationCallback = [](void* context, void* oldPointer, size_t oldSize, void* newPointer, size_t newSize)
{
AllocationData& allocationData = *static_cast<AllocationData*>(context);
if (oldPointer != nullptr)
{
CHECK(oldSize != 0);
allocationData.bytesFreed += oldSize;
}
if (newPointer != nullptr)
{
CHECK(newSize != 0);
allocationData.bytesAllocated += newSize;
}
};
const size_t blockSize = 1024 * 1024;
const size_t maxTotalSize = 1024 * 1024;
{
CodeAllocator allocator(blockSize, maxTotalSize, allocationCallback, &allocationData);
uint8_t* nativeData = nullptr;
size_t sizeNativeData = 0;
uint8_t* nativeEntry = nullptr;
std::vector<uint8_t> code;
code.resize(128);
REQUIRE(allocator.allocate(nullptr, 0, code.data(), code.size(), nativeData, sizeNativeData, nativeEntry));
CHECK(allocationData.bytesAllocated == blockSize);
CHECK(allocationData.bytesFreed == 0);
}
CHECK(allocationData.bytesAllocated == blockSize);
CHECK(allocationData.bytesFreed == blockSize);
}
TEST_CASE("CodeAllocationFailure") TEST_CASE("CodeAllocationFailure")
{ {
size_t blockSize = 3000; size_t blockSize = 3000;

View file

@ -7100,8 +7100,6 @@ L1: RETURN R3 1
TEST_CASE("EncodedTypeTable") TEST_CASE("EncodedTypeTable")
{ {
ScopedFastFlag sff("LuauCompileFunctionType", true);
CHECK_EQ("\n" + compileTypeTable(R"( CHECK_EQ("\n" + compileTypeTable(R"(
function myfunc(test: string, num: number) function myfunc(test: string, num: number)
print(test) print(test)
@ -7153,8 +7151,6 @@ Str:test(234)
TEST_CASE("HostTypesAreUserdata") TEST_CASE("HostTypesAreUserdata")
{ {
ScopedFastFlag sff("LuauCompileFunctionType", true);
CHECK_EQ("\n" + compileTypeTable(R"( CHECK_EQ("\n" + compileTypeTable(R"(
function myfunc(test: string, num: number) function myfunc(test: string, num: number)
print(test) print(test)
@ -7181,8 +7177,6 @@ end
TEST_CASE("HostTypesVector") TEST_CASE("HostTypesVector")
{ {
ScopedFastFlag sff("LuauCompileFunctionType", true);
CHECK_EQ("\n" + compileTypeTable(R"( CHECK_EQ("\n" + compileTypeTable(R"(
function myfunc(test: Instance, pos: Vector3) function myfunc(test: Instance, pos: Vector3)
end end
@ -7206,8 +7200,6 @@ end
TEST_CASE("TypeAliasScoping") TEST_CASE("TypeAliasScoping")
{ {
ScopedFastFlag sff("LuauCompileFunctionType", true);
CHECK_EQ("\n" + compileTypeTable(R"( CHECK_EQ("\n" + compileTypeTable(R"(
do do
type Part = number type Part = number
@ -7242,8 +7234,6 @@ type Instance = string
TEST_CASE("TypeAliasResolve") TEST_CASE("TypeAliasResolve")
{ {
ScopedFastFlag sff("LuauCompileFunctionType", true);
CHECK_EQ("\n" + compileTypeTable(R"( CHECK_EQ("\n" + compileTypeTable(R"(
type Foo1 = number type Foo1 = number
type Foo2 = { number } type Foo2 = { number }

View file

@ -266,6 +266,12 @@ static void* limitedRealloc(void* ud, void* ptr, size_t osize, size_t nsize)
TEST_SUITE_BEGIN("Conformance"); TEST_SUITE_BEGIN("Conformance");
TEST_CASE("CodegenSupported")
{
if (codegen && !luau_codegen_supported())
MESSAGE("Native code generation is not supported by the current configuration and will be disabled");
}
TEST_CASE("Assert") TEST_CASE("Assert")
{ {
runConformance("assert.lua"); runConformance("assert.lua");
@ -1726,7 +1732,6 @@ TEST_CASE("Native")
TEST_CASE("NativeTypeAnnotations") TEST_CASE("NativeTypeAnnotations")
{ {
ScopedFastFlag bytecodeVersion4("BytecodeVersion4", true); ScopedFastFlag bytecodeVersion4("BytecodeVersion4", true);
ScopedFastFlag luauCompileFunctionType("LuauCompileFunctionType", true);
// This tests requires code to run natively, otherwise all 'is_native' checks will fail // This tests requires code to run natively, otherwise all 'is_native' checks will fail
if (!codegen || !luau_codegen_supported()) if (!codegen || !luau_codegen_supported())

View file

@ -1528,4 +1528,43 @@ TEST_CASE_FIXTURE(DifferFixture, "equal_generictp_cyclic")
compareTypesEq("foo", "almostFoo"); compareTypesEq("foo", "almostFoo");
} }
TEST_CASE_FIXTURE(DifferFixture, "symbol_forward")
{
CheckResult result = check(R"(
local foo = 5
local almostFoo = "five"
)");
LUAU_REQUIRE_NO_ERRORS(result);
INFO(Luau::toString(requireType("foo")));
INFO(Luau::toString(requireType("almostFoo")));
compareTypesNe("foo", "almostFoo",
R"(DiffError: these two types are not equal because the left type at foo has type number, while the right type at almostFoo has type string)",
true);
}
TEST_CASE_FIXTURE(DifferFixture, "newlines")
{
CheckResult result = check(R"(
local foo = 5
local almostFoo = "five"
)");
LUAU_REQUIRE_NO_ERRORS(result);
INFO(Luau::toString(requireType("foo")));
INFO(Luau::toString(requireType("almostFoo")));
compareTypesNe("foo", "almostFoo",
R"(DiffError: these two types are not equal because the left type at
foo
has type
number,
while the right type at
almostFoo
has type
string)",
true, true);
}
TEST_SUITE_END(); TEST_SUITE_END();

View file

@ -160,14 +160,37 @@ void createSomeClasses(Frontend* frontend);
template<typename BaseFixture> template<typename BaseFixture>
struct DifferFixtureGeneric : BaseFixture struct DifferFixtureGeneric : BaseFixture
{ {
void compareNe(TypeId left, TypeId right, const std::string& expectedMessage) std::string normalizeWhitespace(std::string msg)
{
std::string normalizedMsg = "";
bool wasWhitespace = true;
for (char c : msg)
{
bool isWhitespace = c == ' ' || c == '\n';
if (wasWhitespace && isWhitespace)
continue;
normalizedMsg += isWhitespace ? ' ' : c;
wasWhitespace = isWhitespace;
}
if (wasWhitespace)
normalizedMsg.pop_back();
return normalizedMsg;
}
void compareNe(TypeId left, TypeId right, const std::string& expectedMessage, bool multiLine)
{
compareNe(left, std::nullopt, right, std::nullopt, expectedMessage, multiLine);
}
void compareNe(TypeId left, std::optional<std::string> symbolLeft, TypeId right, std::optional<std::string> symbolRight,
const std::string& expectedMessage, bool multiLine)
{ {
std::string diffMessage; std::string diffMessage;
try try
{ {
DifferResult diffRes = diff(left, right); DifferResult diffRes = diffWithSymbols(left, right, symbolLeft, symbolRight);
REQUIRE_MESSAGE(diffRes.diffError.has_value(), "Differ did not report type error, even though types are unequal"); REQUIRE_MESSAGE(diffRes.diffError.has_value(), "Differ did not report type error, even though types are unequal");
diffMessage = diffRes.diffError->toString(); diffMessage = diffRes.diffError->toString(multiLine);
} }
catch (const InternalCompilerError& e) catch (const InternalCompilerError& e)
{ {
@ -176,9 +199,19 @@ struct DifferFixtureGeneric : BaseFixture
CHECK_EQ(expectedMessage, diffMessage); CHECK_EQ(expectedMessage, diffMessage);
} }
void compareTypesNe(const std::string& leftSymbol, const std::string& rightSymbol, const std::string& expectedMessage) void compareTypesNe(const std::string& leftSymbol, const std::string& rightSymbol, const std::string& expectedMessage, bool forwardSymbol = false,
bool multiLine = false)
{ {
compareNe(BaseFixture::requireType(leftSymbol), BaseFixture::requireType(rightSymbol), expectedMessage); if (forwardSymbol)
{
compareNe(
BaseFixture::requireType(leftSymbol), leftSymbol, BaseFixture::requireType(rightSymbol), rightSymbol, expectedMessage, multiLine);
}
else
{
compareNe(
BaseFixture::requireType(leftSymbol), std::nullopt, BaseFixture::requireType(rightSymbol), std::nullopt, expectedMessage, multiLine);
}
} }
void compareEq(TypeId left, TypeId right) void compareEq(TypeId left, TypeId right)

View file

@ -1657,8 +1657,6 @@ _ = (math.random() < 0.5 and false) or 42 -- currently ignored
TEST_CASE_FIXTURE(Fixture, "WrongComment") TEST_CASE_FIXTURE(Fixture, "WrongComment")
{ {
ScopedFastFlag sff("LuauLintNativeComment", true);
LintResult result = lint(R"( LintResult result = lint(R"(
--!strict --!strict
--!struct --!struct

View file

@ -1,5 +1,6 @@
// This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details // This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details
#include "Luau/Clone.h" #include "Luau/Clone.h"
#include "Luau/Common.h"
#include "Luau/Module.h" #include "Luau/Module.h"
#include "Luau/Scope.h" #include "Luau/Scope.h"
#include "Luau/RecursionCounter.h" #include "Luau/RecursionCounter.h"
@ -7,11 +8,13 @@
#include "Fixture.h" #include "Fixture.h"
#include "ScopedFlags.h"
#include "doctest.h" #include "doctest.h"
using namespace Luau; using namespace Luau;
LUAU_FASTFLAG(DebugLuauDeferredConstraintResolution); LUAU_FASTFLAG(DebugLuauDeferredConstraintResolution);
LUAU_FASTFLAG(LuauStacklessTypeClone)
TEST_SUITE_BEGIN("ModuleTests"); TEST_SUITE_BEGIN("ModuleTests");
@ -78,7 +81,7 @@ TEST_CASE_FIXTURE(Fixture, "is_within_comment_parse_result")
TEST_CASE_FIXTURE(Fixture, "dont_clone_persistent_primitive") TEST_CASE_FIXTURE(Fixture, "dont_clone_persistent_primitive")
{ {
TypeArena dest; TypeArena dest;
CloneState cloneState; CloneState cloneState{builtinTypes};
// numberType is persistent. We leave it as-is. // numberType is persistent. We leave it as-is.
TypeId newNumber = clone(builtinTypes->numberType, dest, cloneState); TypeId newNumber = clone(builtinTypes->numberType, dest, cloneState);
@ -88,7 +91,7 @@ TEST_CASE_FIXTURE(Fixture, "dont_clone_persistent_primitive")
TEST_CASE_FIXTURE(Fixture, "deepClone_non_persistent_primitive") TEST_CASE_FIXTURE(Fixture, "deepClone_non_persistent_primitive")
{ {
TypeArena dest; TypeArena dest;
CloneState cloneState; CloneState cloneState{builtinTypes};
// Create a new number type that isn't persistent // Create a new number type that isn't persistent
unfreeze(frontend.globals.globalTypes); unfreeze(frontend.globals.globalTypes);
@ -129,7 +132,7 @@ TEST_CASE_FIXTURE(Fixture, "deepClone_cyclic_table")
TypeId ty = requireType("Cyclic"); TypeId ty = requireType("Cyclic");
TypeArena dest; TypeArena dest;
CloneState cloneState; CloneState cloneState{builtinTypes};
TypeId cloneTy = clone(ty, dest, cloneState); TypeId cloneTy = clone(ty, dest, cloneState);
TableType* ttv = getMutable<TableType>(cloneTy); TableType* ttv = getMutable<TableType>(cloneTy);
@ -147,7 +150,7 @@ TEST_CASE_FIXTURE(Fixture, "deepClone_cyclic_table")
REQUIRE(methodReturnType); REQUIRE(methodReturnType);
CHECK_MESSAGE(methodReturnType == cloneTy, toString(methodType, {true}) << " should be pointer identical to " << toString(cloneTy, {true})); CHECK_MESSAGE(methodReturnType == cloneTy, toString(methodType, {true}) << " should be pointer identical to " << toString(cloneTy, {true}));
CHECK_EQ(2, dest.typePacks.size()); // one for the function args, and another for its return type CHECK_EQ(FFlag::LuauStacklessTypeClone ? 1 : 2, dest.typePacks.size()); // one for the function args, and another for its return type
CHECK_EQ(2, dest.types.size()); // One table and one function CHECK_EQ(2, dest.types.size()); // One table and one function
} }
@ -165,7 +168,7 @@ TEST_CASE_FIXTURE(Fixture, "deepClone_cyclic_table_2")
TypeArena dest; TypeArena dest;
CloneState cloneState; CloneState cloneState{builtinTypes};
TypeId cloneTy = clone(tableTy, dest, cloneState); TypeId cloneTy = clone(tableTy, dest, cloneState);
TableType* ctt = getMutable<TableType>(cloneTy); TableType* ctt = getMutable<TableType>(cloneTy);
REQUIRE(ctt); REQUIRE(ctt);
@ -209,7 +212,7 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "builtin_types_point_into_globalTypes_arena")
TEST_CASE_FIXTURE(Fixture, "deepClone_union") TEST_CASE_FIXTURE(Fixture, "deepClone_union")
{ {
TypeArena dest; TypeArena dest;
CloneState cloneState; CloneState cloneState{builtinTypes};
unfreeze(frontend.globals.globalTypes); unfreeze(frontend.globals.globalTypes);
TypeId oldUnion = frontend.globals.globalTypes.addType(UnionType{{builtinTypes->numberType, builtinTypes->stringType}}); TypeId oldUnion = frontend.globals.globalTypes.addType(UnionType{{builtinTypes->numberType, builtinTypes->stringType}});
@ -224,7 +227,7 @@ TEST_CASE_FIXTURE(Fixture, "deepClone_union")
TEST_CASE_FIXTURE(Fixture, "deepClone_intersection") TEST_CASE_FIXTURE(Fixture, "deepClone_intersection")
{ {
TypeArena dest; TypeArena dest;
CloneState cloneState; CloneState cloneState{builtinTypes};
unfreeze(frontend.globals.globalTypes); unfreeze(frontend.globals.globalTypes);
TypeId oldIntersection = frontend.globals.globalTypes.addType(IntersectionType{{builtinTypes->numberType, builtinTypes->stringType}}); TypeId oldIntersection = frontend.globals.globalTypes.addType(IntersectionType{{builtinTypes->numberType, builtinTypes->stringType}});
@ -251,7 +254,7 @@ TEST_CASE_FIXTURE(Fixture, "clone_class")
std::nullopt, &exampleMetaClass, {}, {}, "Test"}}; std::nullopt, &exampleMetaClass, {}, {}, "Test"}};
TypeArena dest; TypeArena dest;
CloneState cloneState; CloneState cloneState{builtinTypes};
TypeId cloned = clone(&exampleClass, dest, cloneState); TypeId cloned = clone(&exampleClass, dest, cloneState);
const ClassType* ctv = get<ClassType>(cloned); const ClassType* ctv = get<ClassType>(cloned);
@ -274,12 +277,12 @@ TEST_CASE_FIXTURE(Fixture, "clone_free_types")
TypePackVar freeTp(FreeTypePack{TypeLevel{}}); TypePackVar freeTp(FreeTypePack{TypeLevel{}});
TypeArena dest; TypeArena dest;
CloneState cloneState; CloneState cloneState{builtinTypes};
TypeId clonedTy = clone(freeTy, dest, cloneState); TypeId clonedTy = clone(freeTy, dest, cloneState);
CHECK(get<FreeType>(clonedTy)); CHECK(get<FreeType>(clonedTy));
cloneState = {}; cloneState = {builtinTypes};
TypePackId clonedTp = clone(&freeTp, dest, cloneState); TypePackId clonedTp = clone(&freeTp, dest, cloneState);
CHECK(get<FreeTypePack>(clonedTp)); CHECK(get<FreeTypePack>(clonedTp));
} }
@ -291,7 +294,7 @@ TEST_CASE_FIXTURE(Fixture, "clone_free_tables")
ttv->state = TableState::Free; ttv->state = TableState::Free;
TypeArena dest; TypeArena dest;
CloneState cloneState; CloneState cloneState{builtinTypes};
TypeId cloned = clone(&tableTy, dest, cloneState); TypeId cloned = clone(&tableTy, dest, cloneState);
const TableType* clonedTtv = get<TableType>(cloned); const TableType* clonedTtv = get<TableType>(cloned);
@ -332,6 +335,8 @@ TEST_CASE_FIXTURE(Fixture, "clone_recursion_limit")
#else #else
int limit = 400; int limit = 400;
#endif #endif
ScopedFastFlag sff{"LuauStacklessTypeClone", false};
ScopedFastInt luauTypeCloneRecursionLimit{"LuauTypeCloneRecursionLimit", limit}; ScopedFastInt luauTypeCloneRecursionLimit{"LuauTypeCloneRecursionLimit", limit};
TypeArena src; TypeArena src;
@ -348,11 +353,39 @@ TEST_CASE_FIXTURE(Fixture, "clone_recursion_limit")
} }
TypeArena dest; TypeArena dest;
CloneState cloneState; CloneState cloneState{builtinTypes};
CHECK_THROWS_AS(clone(table, dest, cloneState), RecursionLimitException); CHECK_THROWS_AS(clone(table, dest, cloneState), RecursionLimitException);
} }
TEST_CASE_FIXTURE(Fixture, "clone_iteration_limit")
{
ScopedFastFlag sff{"LuauStacklessTypeClone", true};
ScopedFastInt sfi{"LuauTypeCloneIterationLimit", 500};
TypeArena src;
TypeId table = src.addType(TableType{});
TypeId nested = table;
for (int i = 0; i < 2500; i++)
{
TableType* ttv = getMutable<TableType>(nested);
ttv->props["a"].setType(src.addType(TableType{}));
nested = ttv->props["a"].type();
}
TypeArena dest;
CloneState cloneState{builtinTypes};
TypeId ty = clone(table, dest, cloneState);
CHECK(get<ErrorType>(ty));
// Cloning it again is an important test.
TypeId ty2 = clone(table, dest, cloneState);
CHECK(get<ErrorType>(ty2));
}
// Unions should never be cyclic, but we should clone them correctly even if // Unions should never be cyclic, but we should clone them correctly even if
// they are. // they are.
TEST_CASE_FIXTURE(Fixture, "clone_cyclic_union") TEST_CASE_FIXTURE(Fixture, "clone_cyclic_union")
@ -368,7 +401,7 @@ TEST_CASE_FIXTURE(Fixture, "clone_cyclic_union")
uu->options.push_back(u); uu->options.push_back(u);
TypeArena dest; TypeArena dest;
CloneState cloneState; CloneState cloneState{builtinTypes};
TypeId cloned = clone(u, dest, cloneState); TypeId cloned = clone(u, dest, cloneState);
REQUIRE(cloned); REQUIRE(cloned);

View file

@ -831,4 +831,39 @@ TEST_CASE_FIXTURE(Fixture, "tostring_unsee_ttv_if_array")
CHECK(toString(requireType("y")) == "({string}, {string}) -> ()"); CHECK(toString(requireType("y")) == "({string}, {string}) -> ()");
} }
TEST_CASE_FIXTURE(Fixture, "tostring_error_mismatch")
{
ScopedFastFlag sff[] = {
{"LuauIndentTypeMismatch", true},
};
ScopedFastInt sfi[] = {
{"LuauIndentTypeMismatchMaxTypeLength", 10},
};
CheckResult result = check(R"(
--!strict
function f1() : {a : number, b : string, c : { d : number}}
return { a = 1, b = "a", c = {d = "a"}}
end
)");
std::string expected = R"(Type
'{ a: number, b: string, c: { d: string } }'
could not be converted into
'{| a: number, b: string, c: {| d: number |} |}'
caused by:
Property 'c' is not compatible.
Type
'{ d: string }'
could not be converted into
'{| d: number |}'
caused by:
Property 'd' is not compatible.
Type 'string' could not be converted into 'number' in an invariant context)";
std::string actual = toString(result.errors[0]);
LUAU_REQUIRE_ERROR_COUNT(1, result);
CHECK(expected == actual);
}
TEST_SUITE_END(); TEST_SUITE_END();

View file

@ -187,8 +187,11 @@ TEST_CASE_FIXTURE(Fixture, "mutually_recursive_aliases")
TEST_CASE_FIXTURE(Fixture, "generic_aliases") TEST_CASE_FIXTURE(Fixture, "generic_aliases")
{ {
ScopedFastFlag sff_DebugLuauDeferredConstraintResolution{"DebugLuauDeferredConstraintResolution", true}; ScopedFastFlag sff[] = {
{"DebugLuauDeferredConstraintResolution", true},
{"LuauIndentTypeMismatch", true},
};
ScopedFastInt sfi{"LuauIndentTypeMismatchMaxTypeLength", 10};
CheckResult result = check(R"( CheckResult result = check(R"(
type T<a> = { v: a } type T<a> = { v: a }
local x: T<number> = { v = 123 } local x: T<number> = { v = 123 }
@ -197,18 +200,22 @@ TEST_CASE_FIXTURE(Fixture, "generic_aliases")
)"); )");
LUAU_REQUIRE_ERROR_COUNT(1, result); LUAU_REQUIRE_ERROR_COUNT(1, result);
const std::string expected = R"(Type 'bad' could not be converted into 'T<number>'
const char* expectedError = "Type 'bad' could not be converted into 'T<number>'\n" caused by:
"caused by:\n" Property 'v' is not compatible.
" Property 'v' is not compatible. Type 'string' could not be converted into 'number' in an invariant context"; Type 'string' could not be converted into 'number' in an invariant context)";
CHECK(result.errors[0].location == Location{{4, 31}, {4, 44}}); CHECK(result.errors[0].location == Location{{4, 31}, {4, 44}});
CHECK(toString(result.errors[0]) == expectedError); CHECK_EQ(expected, toString(result.errors[0]));
} }
TEST_CASE_FIXTURE(Fixture, "dependent_generic_aliases") TEST_CASE_FIXTURE(Fixture, "dependent_generic_aliases")
{ {
ScopedFastFlag sff_DebugLuauDeferredConstraintResolution{"DebugLuauDeferredConstraintResolution", true}; ScopedFastFlag sff[] = {
{"DebugLuauDeferredConstraintResolution", true},
{"LuauIndentTypeMismatch", true},
};
ScopedFastInt sfi{"LuauIndentTypeMismatchMaxTypeLength", 10};
CheckResult result = check(R"( CheckResult result = check(R"(
type T<a> = { v: a } type T<a> = { v: a }
@ -218,15 +225,16 @@ TEST_CASE_FIXTURE(Fixture, "dependent_generic_aliases")
)"); )");
LUAU_REQUIRE_ERROR_COUNT(1, result); LUAU_REQUIRE_ERROR_COUNT(1, result);
const std::string expected = R"(Type 'bad' could not be converted into 'U<number>'
std::string expectedError = "Type 'bad' could not be converted into 'U<number>'\n" caused by:
"caused by:\n" Property 't' is not compatible.
" Property 't' is not compatible. Type '{ v: string }' could not be converted into 'T<number>'\n" Type '{ v: string }' could not be converted into 'T<number>'
"caused by:\n" caused by:
" Property 'v' is not compatible. Type 'string' could not be converted into 'number' in an invariant context"; Property 'v' is not compatible.
Type 'string' could not be converted into 'number' in an invariant context)";
CHECK(result.errors[0].location == Location{{4, 31}, {4, 52}}); CHECK(result.errors[0].location == Location{{4, 31}, {4, 52}});
CHECK(toString(result.errors[0]) == expectedError); CHECK_EQ(expected, toString(result.errors[0]));
} }
TEST_CASE_FIXTURE(Fixture, "mutually_recursive_generic_aliases") TEST_CASE_FIXTURE(Fixture, "mutually_recursive_generic_aliases")
@ -261,7 +269,7 @@ TEST_CASE_FIXTURE(Fixture, "mutually_recursive_types_errors")
// We had a UAF in this example caused by not cloning type function arguments // We had a UAF in this example caused by not cloning type function arguments
ModulePtr module = frontend.moduleResolver.getModule("MainModule"); ModulePtr module = frontend.moduleResolver.getModule("MainModule");
unfreeze(module->interfaceTypes); unfreeze(module->interfaceTypes);
copyErrors(module->errors, module->interfaceTypes); copyErrors(module->errors, module->interfaceTypes, builtinTypes);
freeze(module->interfaceTypes); freeze(module->interfaceTypes);
module->internalTypes.clear(); module->internalTypes.clear();
module->astTypes.clear(); module->astTypes.clear();

View file

@ -132,7 +132,11 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "sort_with_predicate")
TEST_CASE_FIXTURE(BuiltinsFixture, "sort_with_bad_predicate") TEST_CASE_FIXTURE(BuiltinsFixture, "sort_with_bad_predicate")
{ {
ScopedFastFlag sff{"LuauAlwaysCommitInferencesOfFunctionCalls", true}; ScopedFastFlag sff[] = {
{"LuauAlwaysCommitInferencesOfFunctionCalls", true},
{"LuauIndentTypeMismatch", true},
};
ScopedFastInt sfi{"LuauIndentTypeMismatchMaxTypeLength", 10};
CheckResult result = check(R"( CheckResult result = check(R"(
--!strict --!strict
@ -142,12 +146,20 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "sort_with_bad_predicate")
)"); )");
LUAU_REQUIRE_ERROR_COUNT(1, result); LUAU_REQUIRE_ERROR_COUNT(1, result);
CHECK_EQ(R"(Type '(number, number) -> boolean' could not be converted into '((string, string) -> boolean)?' const std::string expected = R"(Type
'(number, number) -> boolean'
could not be converted into
'((string, string) -> boolean)?'
caused by: caused by:
None of the union options are compatible. For example: Type '(number, number) -> boolean' could not be converted into '(string, string) -> boolean' None of the union options are compatible. For example:
Type
'(number, number) -> boolean'
could not be converted into
'(string, string) -> boolean'
caused by: caused by:
Argument #1 type is not compatible. Type 'string' could not be converted into 'number')", Argument #1 type is not compatible.
toString(result.errors[0])); Type 'string' could not be converted into 'number')";
CHECK_EQ(expected, toString(result.errors[0]));
} }
TEST_CASE_FIXTURE(Fixture, "strings_have_methods") TEST_CASE_FIXTURE(Fixture, "strings_have_methods")

View file

@ -367,8 +367,11 @@ b.X = 2 -- real Vector2.X is also read-only
TEST_CASE_FIXTURE(ClassFixture, "detailed_class_unification_error") TEST_CASE_FIXTURE(ClassFixture, "detailed_class_unification_error")
{ {
ScopedFastFlag sff{"LuauAlwaysCommitInferencesOfFunctionCalls", true}; ScopedFastFlag sff[] = {
{"LuauAlwaysCommitInferencesOfFunctionCalls", true},
{"LuauIndentTypeMismatch", true},
};
ScopedFastInt sfi{"LuauIndentTypeMismatchMaxTypeLength", 10};
CheckResult result = check(R"( CheckResult result = check(R"(
local function foo(v) local function foo(v)
return v.X :: number + string.len(v.Y) return v.X :: number + string.len(v.Y)
@ -380,10 +383,11 @@ b(a)
)"); )");
LUAU_REQUIRE_ERROR_COUNT(1, result); LUAU_REQUIRE_ERROR_COUNT(1, result);
const std::string expected = R"(Type 'Vector2' could not be converted into '{- X: number, Y: string -}'
CHECK_EQ(toString(result.errors[0]), R"(Type 'Vector2' could not be converted into '{- X: number, Y: string -}'
caused by: caused by:
Property 'Y' is not compatible. Type 'number' could not be converted into 'string')"); Property 'Y' is not compatible.
Type 'number' could not be converted into 'string')";
CHECK_EQ(expected, toString(result.errors[0]));
} }
TEST_CASE_FIXTURE(ClassFixture, "class_type_mismatch_with_name_conflict") TEST_CASE_FIXTURE(ClassFixture, "class_type_mismatch_with_name_conflict")
@ -453,6 +457,8 @@ TEST_CASE_FIXTURE(ClassFixture, "index_instance_property_nonstrict")
TEST_CASE_FIXTURE(ClassFixture, "type_mismatch_invariance_required_for_error") TEST_CASE_FIXTURE(ClassFixture, "type_mismatch_invariance_required_for_error")
{ {
ScopedFastFlag sff{"LuauIndentTypeMismatch", true};
ScopedFastInt sfi{"LuauIndentTypeMismatchMaxTypeLength", 10};
CheckResult result = check(R"( CheckResult result = check(R"(
type A = { x: ChildClass } type A = { x: ChildClass }
type B = { x: BaseClass } type B = { x: BaseClass }
@ -462,9 +468,11 @@ local b: B = a
)"); )");
LUAU_REQUIRE_ERRORS(result); LUAU_REQUIRE_ERRORS(result);
CHECK_EQ(toString(result.errors[0]), R"(Type 'A' could not be converted into 'B' const std::string expected = R"(Type 'A' could not be converted into 'B'
caused by: caused by:
Property 'x' is not compatible. Type 'ChildClass' could not be converted into 'BaseClass' in an invariant context)"); Property 'x' is not compatible.
Type 'ChildClass' could not be converted into 'BaseClass' in an invariant context)";
CHECK_EQ(expected, toString(result.errors[0]));
} }
TEST_CASE_FIXTURE(ClassFixture, "callable_classes") TEST_CASE_FIXTURE(ClassFixture, "callable_classes")

View file

@ -1095,6 +1095,9 @@ TEST_CASE_FIXTURE(Fixture, "return_type_by_overload")
TEST_CASE_FIXTURE(BuiltinsFixture, "infer_anonymous_function_arguments") TEST_CASE_FIXTURE(BuiltinsFixture, "infer_anonymous_function_arguments")
{ {
ScopedFastFlag sff{"LuauIndentTypeMismatch", true};
ScopedFastInt sfi{"LuauIndentTypeMismatchMaxTypeLength", 10};
// Simple direct arg to arg propagation // Simple direct arg to arg propagation
CheckResult result = check(R"( CheckResult result = check(R"(
type Table = { x: number, y: number } type Table = { x: number, y: number }
@ -1150,129 +1153,27 @@ f(function(a, b, c, ...) return a + b end)
LUAU_REQUIRE_ERRORS(result); LUAU_REQUIRE_ERRORS(result);
std::string expected;
if (FFlag::LuauInstantiateInSubtyping) if (FFlag::LuauInstantiateInSubtyping)
{ {
CHECK_EQ(R"(Type '<a>(number, number, a) -> number' could not be converted into '(number, number) -> number' expected = R"(Type
'<a>(number, number, a) -> number'
could not be converted into
'(number, number) -> number'
caused by: caused by:
Argument count mismatch. Function expects 3 arguments, but only 2 are specified)", Argument count mismatch. Function expects 3 arguments, but only 2 are specified)";
toString(result.errors[0]));
} }
else else
{ {
CHECK_EQ(R"(Type '(number, number, a) -> number' could not be converted into '(number, number) -> number' expected = R"(Type
'(number, number, a) -> number'
could not be converted into
'(number, number) -> number'
caused by: caused by:
Argument count mismatch. Function expects 3 arguments, but only 2 are specified)", Argument count mismatch. Function expects 3 arguments, but only 2 are specified)";
toString(result.errors[0]));
} }
// Infer from variadic packs into elements CHECK_EQ(expected, toString(result.errors[0]));
result = check(R"(
function f(a: (...number) -> number) return a(1, 2) end
f(function(a, b) return a + b end)
)");
LUAU_REQUIRE_NO_ERRORS(result);
// Infer from variadic packs into variadic packs
result = check(R"(
type Table = { x: number, y: number }
function f(a: (...Table) -> number) return a({x = 1, y = 2}, {x = 3, y = 4}) end
f(function(a, ...) local b = ... return b.z end)
)");
LUAU_REQUIRE_ERRORS(result);
CHECK_EQ("Key 'z' not found in table 'Table'", toString(result.errors[0]));
// Return type inference
result = check(R"(
type Table = { x: number, y: number }
function f(a: (number) -> Table) return a(4) end
f(function(x) return x * 2 end)
)");
LUAU_REQUIRE_ERROR_COUNT(1, result);
CHECK_EQ("Type 'number' could not be converted into 'Table'", toString(result.errors[0]));
// Return type doesn't inference 'nil'
result = check(R"(
function f(a: (number) -> nil) return a(4) end
f(function(x) print(x) end)
)");
LUAU_REQUIRE_NO_ERRORS(result);
}
TEST_CASE_FIXTURE(BuiltinsFixture, "infer_anonymous_function_arguments")
{
// Simple direct arg to arg propagation
CheckResult result = check(R"(
type Table = { x: number, y: number }
local function f(a: (Table) -> number) return a({x = 1, y = 2}) end
f(function(a) return a.x + a.y end)
)");
LUAU_REQUIRE_NO_ERRORS(result);
// An optional function is accepted, but since we already provide a function, nil can be ignored
result = check(R"(
type Table = { x: number, y: number }
local function f(a: ((Table) -> number)?) if a then return a({x = 1, y = 2}) else return 0 end end
f(function(a) return a.x + a.y end)
)");
LUAU_REQUIRE_NO_ERRORS(result);
// Make sure self calls match correct index
result = check(R"(
type Table = { x: number, y: number }
local x = {}
x.b = {x = 1, y = 2}
function x:f(a: (Table) -> number) return a(self.b) end
x:f(function(a) return a.x + a.y end)
)");
LUAU_REQUIRE_NO_ERRORS(result);
// Mix inferred and explicit argument types
result = check(R"(
function f(a: (a: number, b: number, c: boolean) -> number) return a(1, 2, true) end
f(function(a: number, b, c) return c and a + b or b - a end)
)");
LUAU_REQUIRE_NO_ERRORS(result);
// Anonymous function has a variadic pack
result = check(R"(
type Table = { x: number, y: number }
local function f(a: (Table) -> number) return a({x = 1, y = 2}) end
f(function(...) return select(1, ...).z end)
)");
LUAU_REQUIRE_ERRORS(result);
CHECK_EQ("Key 'z' not found in table 'Table'", toString(result.errors[0]));
// Can't accept more arguments than provided
result = check(R"(
function f(a: (a: number, b: number) -> number) return a(1, 2) end
f(function(a, b, c, ...) return a + b end)
)");
LUAU_REQUIRE_ERRORS(result);
if (FFlag::LuauInstantiateInSubtyping)
{
CHECK_EQ(R"(Type '<a>(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'
caused by:
Argument count mismatch. Function expects 3 arguments, but only 2 are specified)",
toString(result.errors[0]));
}
// Infer from variadic packs into elements // Infer from variadic packs into elements
result = check(R"( result = check(R"(
@ -1376,6 +1277,8 @@ end
TEST_CASE_FIXTURE(Fixture, "error_detailed_function_mismatch_arg_count") TEST_CASE_FIXTURE(Fixture, "error_detailed_function_mismatch_arg_count")
{ {
ScopedFastFlag sff{"LuauIndentTypeMismatch", true};
ScopedFastInt sfi{"LuauIndentTypeMismatchMaxTypeLength", 10};
CheckResult result = check(R"( CheckResult result = check(R"(
type A = (number, number) -> string type A = (number, number) -> string
type B = (number) -> string type B = (number) -> string
@ -1385,13 +1288,20 @@ local b: B = a
)"); )");
LUAU_REQUIRE_ERROR_COUNT(1, result); LUAU_REQUIRE_ERROR_COUNT(1, result);
CHECK_EQ(toString(result.errors[0]), R"(Type '(number, number) -> string' could not be converted into '(number) -> string' const std::string expected = R"(Type
'(number, number) -> string'
could not be converted into
'(number) -> string'
caused by: caused by:
Argument count mismatch. Function expects 2 arguments, but only 1 is specified)"); Argument count mismatch. Function expects 2 arguments, but only 1 is specified)";
CHECK_EQ(expected, toString(result.errors[0]));
} }
TEST_CASE_FIXTURE(Fixture, "error_detailed_function_mismatch_arg") TEST_CASE_FIXTURE(Fixture, "error_detailed_function_mismatch_arg")
{ {
ScopedFastFlag sff{"LuauIndentTypeMismatch", true};
ScopedFastInt sfi{"LuauIndentTypeMismatchMaxTypeLength", 10};
CheckResult result = check(R"( CheckResult result = check(R"(
type A = (number, number) -> string type A = (number, number) -> string
type B = (number, string) -> string type B = (number, string) -> string
@ -1401,13 +1311,21 @@ local b: B = a
)"); )");
LUAU_REQUIRE_ERROR_COUNT(1, result); 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' const std::string expected = R"(Type
'(number, number) -> string'
could not be converted into
'(number, string) -> string'
caused by: caused by:
Argument #2 type is not compatible. Type 'string' could not be converted into 'number')"); Argument #2 type is not compatible.
Type 'string' could not be converted into 'number')";
CHECK_EQ(expected, toString(result.errors[0]));
} }
TEST_CASE_FIXTURE(Fixture, "error_detailed_function_mismatch_ret_count") TEST_CASE_FIXTURE(Fixture, "error_detailed_function_mismatch_ret_count")
{ {
ScopedFastFlag sff{"LuauIndentTypeMismatch", true};
ScopedFastInt sfi{"LuauIndentTypeMismatchMaxTypeLength", 10};
CheckResult result = check(R"( CheckResult result = check(R"(
type A = (number, number) -> (number) type A = (number, number) -> (number)
type B = (number, number) -> (number, boolean) type B = (number, number) -> (number, boolean)
@ -1417,13 +1335,20 @@ local b: B = a
)"); )");
LUAU_REQUIRE_ERROR_COUNT(1, result); 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)' const std::string expected = R"(Type
'(number, number) -> number'
could not be converted into
'(number, number) -> (number, boolean)'
caused by: caused by:
Function only returns 1 value, but 2 are required here)"); Function only returns 1 value, but 2 are required here)";
CHECK_EQ(expected, toString(result.errors[0]));
} }
TEST_CASE_FIXTURE(Fixture, "error_detailed_function_mismatch_ret") TEST_CASE_FIXTURE(Fixture, "error_detailed_function_mismatch_ret")
{ {
ScopedFastFlag sff{"LuauIndentTypeMismatch", true};
ScopedFastInt sfi{"LuauIndentTypeMismatchMaxTypeLength", 10};
CheckResult result = check(R"( CheckResult result = check(R"(
type A = (number, number) -> string type A = (number, number) -> string
type B = (number, number) -> number type B = (number, number) -> number
@ -1433,13 +1358,21 @@ local b: B = a
)"); )");
LUAU_REQUIRE_ERROR_COUNT(1, result); 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' const std::string expected = R"(Type
'(number, number) -> string'
could not be converted into
'(number, number) -> number'
caused by: caused by:
Return type is not compatible. Type 'string' could not be converted into 'number')"); Return type is not compatible.
Type 'string' could not be converted into 'number')";
CHECK_EQ(expected, toString(result.errors[0]));
} }
TEST_CASE_FIXTURE(Fixture, "error_detailed_function_mismatch_ret_mult") TEST_CASE_FIXTURE(Fixture, "error_detailed_function_mismatch_ret_mult")
{ {
ScopedFastFlag sff{"LuauIndentTypeMismatch", true};
ScopedFastInt sfi{"LuauIndentTypeMismatchMaxTypeLength", 10};
CheckResult result = check(R"( CheckResult result = check(R"(
type A = (number, number) -> (number, string) type A = (number, number) -> (number, string)
type B = (number, number) -> (number, boolean) type B = (number, number) -> (number, boolean)
@ -1449,10 +1382,14 @@ local b: B = a
)"); )");
LUAU_REQUIRE_ERROR_COUNT(1, result); LUAU_REQUIRE_ERROR_COUNT(1, result);
CHECK_EQ(toString(result.errors[0]), const std::string expected = R"(Type
R"(Type '(number, number) -> (number, string)' could not be converted into '(number, number) -> (number, boolean)' '(number, number) -> (number, string)'
could not be converted into
'(number, number) -> (number, boolean)'
caused by: caused by:
Return #2 type is not compatible. Type 'string' could not be converted into 'boolean')"); Return #2 type is not compatible.
Type 'string' could not be converted into 'boolean')";
CHECK_EQ(expected, toString(result.errors[0]));
} }
TEST_CASE_FIXTURE(BuiltinsFixture, "function_decl_quantify_right_type") TEST_CASE_FIXTURE(BuiltinsFixture, "function_decl_quantify_right_type")
@ -1561,6 +1498,9 @@ TEST_CASE_FIXTURE(Fixture, "inferred_higher_order_functions_are_quantified_at_th
TEST_CASE_FIXTURE(BuiltinsFixture, "function_decl_non_self_unsealed_overwrite") TEST_CASE_FIXTURE(BuiltinsFixture, "function_decl_non_self_unsealed_overwrite")
{ {
ScopedFastFlag sff{"LuauIndentTypeMismatch", true};
ScopedFastInt sfi{"LuauIndentTypeMismatchMaxTypeLength", 10};
CheckResult result = check(R"( CheckResult result = check(R"(
local t = { f = nil :: ((x: number) -> number)? } local t = { f = nil :: ((x: number) -> number)? }
@ -1575,11 +1515,19 @@ end
)"); )");
LUAU_REQUIRE_ERROR_COUNT(2, result); LUAU_REQUIRE_ERROR_COUNT(2, result);
CHECK_EQ(toString(result.errors[0]), R"(Type '(string) -> string' could not be converted into '((number) -> number)?' CHECK_EQ(toString(result.errors[0]), R"(Type
'(string) -> string'
could not be converted into
'((number) -> number)?'
caused by: caused by:
None of the union options are compatible. For example: Type '(string) -> string' could not be converted into '(number) -> number' None of the union options are compatible. For example:
Type
'(string) -> string'
could not be converted into
'(number) -> number'
caused by: caused by:
Argument #1 type is not compatible. Type 'number' could not be converted into 'string')"); Argument #1 type is not compatible.
Type 'number' could not be converted into 'string')");
CHECK_EQ(toString(result.errors[1]), R"(Type 'string' could not be converted into 'number')"); CHECK_EQ(toString(result.errors[1]), R"(Type 'string' could not be converted into 'number')");
} }
@ -1595,6 +1543,9 @@ TEST_CASE_FIXTURE(Fixture, "strict_mode_ok_with_missing_arguments")
TEST_CASE_FIXTURE(Fixture, "function_statement_sealed_table_assignment_through_indexer") TEST_CASE_FIXTURE(Fixture, "function_statement_sealed_table_assignment_through_indexer")
{ {
ScopedFastFlag sff{"LuauIndentTypeMismatch", true};
ScopedFastInt sfi{"LuauIndentTypeMismatchMaxTypeLength", 10};
CheckResult result = check(R"( CheckResult result = check(R"(
local t: {[string]: () -> number} = {} local t: {[string]: () -> number} = {}
@ -1603,7 +1554,10 @@ function t:b() return 2 end -- not OK
)"); )");
LUAU_REQUIRE_ERROR_COUNT(1, result); LUAU_REQUIRE_ERROR_COUNT(1, result);
CHECK_EQ(R"(Type '(*error-type*) -> number' could not be converted into '() -> number' CHECK_EQ(R"(Type
'(*error-type*) -> number'
could not be converted into
'() -> number'
caused by: caused by:
Argument count mismatch. Function expects 1 argument, but none are specified)", Argument count mismatch. Function expects 1 argument, but none are specified)",
toString(result.errors[0])); toString(result.errors[0]));
@ -1800,6 +1754,9 @@ foo(string.find("hello", "e"))
TEST_CASE_FIXTURE(Fixture, "luau_subtyping_is_np_hard") TEST_CASE_FIXTURE(Fixture, "luau_subtyping_is_np_hard")
{ {
ScopedFastFlag sff{"LuauIndentTypeMismatch", true};
ScopedFastInt sfi{"LuauIndentTypeMismatchMaxTypeLength", 10};
CheckResult result = check(R"( CheckResult result = check(R"(
--!strict --!strict
@ -1832,11 +1789,11 @@ z = y -- Not OK, so the line is colorable
)"); )");
LUAU_REQUIRE_ERROR_COUNT(1, result); LUAU_REQUIRE_ERROR_COUNT(1, result);
CHECK_EQ(toString(result.errors[0]), const std::string expected = R"(Type
"Type '((\"blue\" | \"red\") -> (\"blue\" | \"red\") -> (\"blue\" | \"red\") -> boolean) & ((\"blue\" | \"red\") -> (\"blue\") -> (\"blue\") " '(("blue" | "red") -> ("blue" | "red") -> ("blue" | "red") -> boolean) & (("blue" | "red") -> ("blue") -> ("blue") -> false) & (("blue" | "red") -> ("red") -> ("red") -> false) & (("blue") -> ("blue") -> ("blue" | "red") -> false) & (("red") -> ("red") -> ("blue" | "red") -> false)'
"-> false) & ((\"blue\" | \"red\") -> (\"red\") -> (\"red\") -> false) & ((\"blue\") -> (\"blue\") -> (\"blue\" | \"red\") -> false) & " could not be converted into
"((\"red\") -> (\"red\") -> (\"blue\" | \"red\") -> false)' could not be converted into '(\"blue\" | \"red\") -> (\"blue\" | \"red\") -> " '("blue" | "red") -> ("blue" | "red") -> ("blue" | "red") -> false'; none of the intersection parts are compatible)";
"(\"blue\" | \"red\") -> false'; none of the intersection parts are compatible"); CHECK_EQ(expected, toString(result.errors[0]));
} }
TEST_CASE_FIXTURE(Fixture, "function_is_supertype_of_concrete_functions") TEST_CASE_FIXTURE(Fixture, "function_is_supertype_of_concrete_functions")
@ -1994,7 +1951,11 @@ TEST_CASE_FIXTURE(Fixture, "function_exprs_are_generalized_at_signature_scope_no
TEST_CASE_FIXTURE(BuiltinsFixture, "param_1_and_2_both_takes_the_same_generic_but_their_arguments_are_incompatible") TEST_CASE_FIXTURE(BuiltinsFixture, "param_1_and_2_both_takes_the_same_generic_but_their_arguments_are_incompatible")
{ {
ScopedFastFlag sff{"LuauAlwaysCommitInferencesOfFunctionCalls", true}; ScopedFastFlag sff[] = {
{"LuauIndentTypeMismatch", true},
{"LuauAlwaysCommitInferencesOfFunctionCalls", true},
};
ScopedFastInt sfi{"LuauIndentTypeMismatchMaxTypeLength", 10};
CheckResult result = check(R"( CheckResult result = check(R"(
local function foo<a>(x: a, y: a?) local function foo<a>(x: a, y: a?)
@ -2038,11 +1999,12 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "param_1_and_2_both_takes_the_same_generic_bu
LUAU_REQUIRE_ERROR_COUNT(2, result); LUAU_REQUIRE_ERROR_COUNT(2, result);
CHECK_EQ(toString(result.errors[0]), R"(Type '{ x: number }' could not be converted into 'vec2?' const std::string expected = R"(Type '{ x: number }' could not be converted into 'vec2?'
caused by: caused by:
None of the union options are compatible. For example: Table type '{ x: number }' not compatible with type 'vec2' because the former is missing field 'y')"); None of the union options are compatible. For example:
Table type '{ x: number }' not compatible with type 'vec2' because the former is missing field 'y')";
CHECK_EQ(toString(result.errors[1]), "Type 'vec2' could not be converted into 'number'"); CHECK_EQ(expected, toString(result.errors[0]));
CHECK_EQ("Type 'vec2' could not be converted into 'number'", toString(result.errors[1]));
} }
TEST_CASE_FIXTURE(BuiltinsFixture, "param_1_and_2_both_takes_the_same_generic_but_their_arguments_are_incompatible_2") TEST_CASE_FIXTURE(BuiltinsFixture, "param_1_and_2_both_takes_the_same_generic_but_their_arguments_are_incompatible_2")

View file

@ -713,6 +713,9 @@ end
TEST_CASE_FIXTURE(Fixture, "generic_functions_should_be_memory_safe") TEST_CASE_FIXTURE(Fixture, "generic_functions_should_be_memory_safe")
{ {
ScopedFastFlag sff{"LuauIndentTypeMismatch", true};
ScopedFastInt sfi{"LuauIndentTypeMismatchMaxTypeLength", 10};
CheckResult result = check(R"( CheckResult result = check(R"(
--!strict --!strict
-- At one point this produced a UAF -- At one point this produced a UAF
@ -725,12 +728,14 @@ y.a.c = y
)"); )");
LUAU_REQUIRE_ERRORS(result); LUAU_REQUIRE_ERRORS(result);
CHECK_EQ(toString(result.errors[0]), const std::string expected = R"(Type 'y' could not be converted into 'T<string>'
R"(Type 'y' could not be converted into 'T<string>'
caused by: caused by:
Property 'a' is not compatible. Type '{ c: T<string>?, d: number }' could not be converted into 'U<string>' Property 'a' is not compatible.
Type '{ c: T<string>?, d: number }' could not be converted into 'U<string>'
caused by: caused by:
Property 'd' is not compatible. Type 'number' could not be converted into 'string' in an invariant context)"); Property 'd' is not compatible.
Type 'number' could not be converted into 'string' in an invariant context)";
CHECK_EQ(expected, toString(result.errors[0]));
} }
TEST_CASE_FIXTURE(Fixture, "generic_type_pack_unification1") TEST_CASE_FIXTURE(Fixture, "generic_type_pack_unification1")

View file

@ -317,6 +317,9 @@ TEST_CASE_FIXTURE(Fixture, "table_intersection_write_sealed")
TEST_CASE_FIXTURE(Fixture, "table_intersection_write_sealed_indirect") TEST_CASE_FIXTURE(Fixture, "table_intersection_write_sealed_indirect")
{ {
ScopedFastFlag sff{"LuauIndentTypeMismatch", true};
ScopedFastInt sfi{"LuauIndentTypeMismatchMaxTypeLength", 10};
CheckResult result = check(R"( CheckResult result = check(R"(
type X = { x: (number) -> number } type X = { x: (number) -> number }
type Y = { y: (string) -> string } type Y = { y: (string) -> string }
@ -333,9 +336,13 @@ TEST_CASE_FIXTURE(Fixture, "table_intersection_write_sealed_indirect")
)"); )");
LUAU_REQUIRE_ERROR_COUNT(4, result); LUAU_REQUIRE_ERROR_COUNT(4, result);
CHECK_EQ(toString(result.errors[0]), R"(Type '(string, number) -> string' could not be converted into '(string) -> string' const std::string expected = R"(Type
'(string, number) -> string'
could not be converted into
'(string) -> string'
caused by: caused by:
Argument count mismatch. Function expects 2 arguments, but only 1 is specified)"); Argument count mismatch. Function expects 2 arguments, but only 1 is specified)";
CHECK_EQ(expected, toString(result.errors[0]));
CHECK_EQ(toString(result.errors[1]), "Cannot add property 'z' to table 'X & Y'"); CHECK_EQ(toString(result.errors[1]), "Cannot add property 'z' to table 'X & Y'");
CHECK_EQ(toString(result.errors[2]), "Type 'number' could not be converted into 'string'"); CHECK_EQ(toString(result.errors[2]), "Type 'number' could not be converted into 'string'");
CHECK_EQ(toString(result.errors[3]), "Cannot add property 'w' to table 'X & Y'"); CHECK_EQ(toString(result.errors[3]), "Cannot add property 'w' to table 'X & Y'");
@ -343,6 +350,9 @@ caused by:
TEST_CASE_FIXTURE(Fixture, "table_write_sealed_indirect") TEST_CASE_FIXTURE(Fixture, "table_write_sealed_indirect")
{ {
ScopedFastFlag sff{"LuauIndentTypeMismatch", true};
ScopedFastInt sfi{"LuauIndentTypeMismatchMaxTypeLength", 10};
// After normalization, previous 'table_intersection_write_sealed_indirect' is identical to this one // After normalization, previous 'table_intersection_write_sealed_indirect' is identical to this one
CheckResult result = check(R"( CheckResult result = check(R"(
type XY = { x: (number) -> number, y: (string) -> string } type XY = { x: (number) -> number, y: (string) -> string }
@ -357,9 +367,14 @@ TEST_CASE_FIXTURE(Fixture, "table_write_sealed_indirect")
)"); )");
LUAU_REQUIRE_ERROR_COUNT(4, result); LUAU_REQUIRE_ERROR_COUNT(4, result);
CHECK_EQ(toString(result.errors[0]), R"(Type '(string, number) -> string' could not be converted into '(string) -> string' const std::string expected = R"(Type
'(string, number) -> string'
could not be converted into
'(string) -> string'
caused by: caused by:
Argument count mismatch. Function expects 2 arguments, but only 1 is specified)"); Argument count mismatch. Function expects 2 arguments, but only 1 is specified)";
CHECK_EQ(expected, toString(result.errors[0]));
CHECK_EQ(toString(result.errors[1]), "Cannot add property 'z' to table 'XY'"); CHECK_EQ(toString(result.errors[1]), "Cannot add property 'z' to table 'XY'");
CHECK_EQ(toString(result.errors[2]), "Type 'number' could not be converted into 'string'"); CHECK_EQ(toString(result.errors[2]), "Type 'number' could not be converted into 'string'");
CHECK_EQ(toString(result.errors[3]), "Cannot add property 'w' to table 'XY'"); CHECK_EQ(toString(result.errors[3]), "Cannot add property 'w' to table 'XY'");
@ -377,6 +392,9 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "table_intersection_setmetatable")
TEST_CASE_FIXTURE(Fixture, "error_detailed_intersection_part") TEST_CASE_FIXTURE(Fixture, "error_detailed_intersection_part")
{ {
ScopedFastFlag sff{"LuauIndentTypeMismatch", true};
ScopedFastInt sfi{"LuauIndentTypeMismatchMaxTypeLength", 10};
CheckResult result = check(R"( CheckResult result = check(R"(
type X = { x: number } type X = { x: number }
type Y = { y: number } type Y = { y: number }
@ -386,9 +404,11 @@ local a: XYZ = 3
)"); )");
LUAU_REQUIRE_ERROR_COUNT(1, result); LUAU_REQUIRE_ERROR_COUNT(1, result);
CHECK_EQ(toString(result.errors[0]), R"(Type 'number' could not be converted into 'X & Y & Z' const std::string expected = R"(Type 'number' could not be converted into 'X & Y & Z'
caused by: caused by:
Not all intersection parts are compatible. Type 'number' could not be converted into 'X')"); Not all intersection parts are compatible.
Type 'number' could not be converted into 'X')";
CHECK_EQ(expected, toString(result.errors[0]));
} }
TEST_CASE_FIXTURE(Fixture, "error_detailed_intersection_all") TEST_CASE_FIXTURE(Fixture, "error_detailed_intersection_all")
@ -462,6 +482,9 @@ TEST_CASE_FIXTURE(Fixture, "intersect_false_and_bool_and_false")
TEST_CASE_FIXTURE(Fixture, "intersect_saturate_overloaded_functions") TEST_CASE_FIXTURE(Fixture, "intersect_saturate_overloaded_functions")
{ {
ScopedFastFlag sff{"LuauIndentTypeMismatch", true};
ScopedFastInt sfi{"LuauIndentTypeMismatchMaxTypeLength", 10};
CheckResult result = check(R"( CheckResult result = check(R"(
local x : ((number?) -> number?) & ((string?) -> string?) local x : ((number?) -> number?) & ((string?) -> string?)
local y : (nil) -> nil = x -- OK local y : (nil) -> nil = x -- OK
@ -469,12 +492,18 @@ TEST_CASE_FIXTURE(Fixture, "intersect_saturate_overloaded_functions")
)"); )");
LUAU_REQUIRE_ERROR_COUNT(1, result); LUAU_REQUIRE_ERROR_COUNT(1, result);
CHECK_EQ(toString(result.errors[0]), "Type '((number?) -> number?) & ((string?) -> string?)' could not be converted into '(number) -> number'; " const std::string expected = R"(Type
"none of the intersection parts are compatible"); '((number?) -> number?) & ((string?) -> string?)'
could not be converted into
'(number) -> number'; none of the intersection parts are compatible)";
CHECK_EQ(expected, toString(result.errors[0]));
} }
TEST_CASE_FIXTURE(Fixture, "union_saturate_overloaded_functions") TEST_CASE_FIXTURE(Fixture, "union_saturate_overloaded_functions")
{ {
ScopedFastFlag sff{"LuauIndentTypeMismatch", true};
ScopedFastInt sfi{"LuauIndentTypeMismatchMaxTypeLength", 10};
CheckResult result = check(R"( CheckResult result = check(R"(
local x : ((number) -> number) & ((string) -> string) local x : ((number) -> number) & ((string) -> string)
local y : ((number | string) -> (number | string)) = x -- OK local y : ((number | string) -> (number | string)) = x -- OK
@ -482,12 +511,18 @@ TEST_CASE_FIXTURE(Fixture, "union_saturate_overloaded_functions")
)"); )");
LUAU_REQUIRE_ERROR_COUNT(1, result); LUAU_REQUIRE_ERROR_COUNT(1, result);
CHECK_EQ(toString(result.errors[0]), "Type '((number) -> number) & ((string) -> string)' could not be converted into '(boolean | number) -> " const std::string expected = R"(Type
"boolean | number'; none of the intersection parts are compatible"); '((number) -> number) & ((string) -> string)'
could not be converted into
'(boolean | number) -> boolean | number'; none of the intersection parts are compatible)";
CHECK_EQ(expected, toString(result.errors[0]));
} }
TEST_CASE_FIXTURE(Fixture, "intersection_of_tables") TEST_CASE_FIXTURE(Fixture, "intersection_of_tables")
{ {
ScopedFastFlag sff{"LuauIndentTypeMismatch", true};
ScopedFastInt sfi{"LuauIndentTypeMismatchMaxTypeLength", 10};
CheckResult result = check(R"( CheckResult result = check(R"(
local x : { p : number?, q : string? } & { p : number?, q : number?, r : number? } local x : { p : number?, q : string? } & { p : number?, q : number?, r : number? }
local y : { p : number?, q : nil, r : number? } = x -- OK local y : { p : number?, q : nil, r : number? } = x -- OK
@ -495,12 +530,18 @@ TEST_CASE_FIXTURE(Fixture, "intersection_of_tables")
)"); )");
LUAU_REQUIRE_ERROR_COUNT(1, result); LUAU_REQUIRE_ERROR_COUNT(1, result);
CHECK_EQ(toString(result.errors[0]), "Type '{| p: number?, q: number?, r: number? |} & {| p: number?, q: string? |}' could not be converted into " const std::string expected = R"(Type
"'{| p: nil |}'; none of the intersection parts are compatible"); '{| p: number?, q: number?, r: number? |} & {| p: number?, q: string? |}'
could not be converted into
'{| p: nil |}'; none of the intersection parts are compatible)";
CHECK_EQ(expected, toString(result.errors[0]));
} }
TEST_CASE_FIXTURE(Fixture, "intersection_of_tables_with_top_properties") TEST_CASE_FIXTURE(Fixture, "intersection_of_tables_with_top_properties")
{ {
ScopedFastFlag sff{"LuauIndentTypeMismatch", true};
ScopedFastInt sfi{"LuauIndentTypeMismatchMaxTypeLength", 10};
CheckResult result = check(R"( CheckResult result = check(R"(
local x : { p : number?, q : any } & { p : unknown, q : string? } local x : { p : number?, q : any } & { p : unknown, q : string? }
local y : { p : number?, q : string? } = x -- OK local y : { p : number?, q : string? } = x -- OK
@ -532,9 +573,11 @@ TEST_CASE_FIXTURE(Fixture, "intersection_of_tables_with_top_properties")
else else
{ {
LUAU_REQUIRE_ERROR_COUNT(1, result); LUAU_REQUIRE_ERROR_COUNT(1, result);
CHECK_EQ(toString(result.errors[0]), const std::string expected = R"(Type
"Type '{| p: number?, q: any |} & {| p: unknown, q: string? |}' could not be converted into '{| p: string?, " '{| p: number?, q: any |} & {| p: unknown, q: string? |}'
"q: number? |}'; none of the intersection parts are compatible"); could not be converted into
'{| p: string?, q: number? |}'; none of the intersection parts are compatible)";
CHECK_EQ(expected, toString(result.errors[0]));
} }
} }
@ -551,6 +594,9 @@ TEST_CASE_FIXTURE(Fixture, "intersection_of_tables_with_never_properties")
TEST_CASE_FIXTURE(Fixture, "overloaded_functions_returning_intersections") TEST_CASE_FIXTURE(Fixture, "overloaded_functions_returning_intersections")
{ {
ScopedFastFlag sff{"LuauIndentTypeMismatch", true};
ScopedFastInt sfi{"LuauIndentTypeMismatchMaxTypeLength", 10};
CheckResult result = check(R"( CheckResult result = check(R"(
local x : ((number?) -> ({ p : number } & { q : number })) & ((string?) -> ({ p : number } & { r : number })) local x : ((number?) -> ({ p : number } & { q : number })) & ((string?) -> ({ p : number } & { r : number }))
local y : (nil) -> { p : number, q : number, r : number} = x -- OK local y : (nil) -> { p : number, q : number, r : number} = x -- OK
@ -558,13 +604,18 @@ TEST_CASE_FIXTURE(Fixture, "overloaded_functions_returning_intersections")
)"); )");
LUAU_REQUIRE_ERROR_COUNT(1, result); LUAU_REQUIRE_ERROR_COUNT(1, result);
CHECK_EQ(toString(result.errors[0]), const std::string expected = R"(Type
"Type '((number?) -> {| p: number |} & {| q: number |}) & ((string?) -> {| p: number |} & {| r: number |})' could not be converted into " '((number?) -> {| p: number |} & {| q: number |}) & ((string?) -> {| p: number |} & {| r: number |})'
"'(number?) -> {| p: number, q: number, r: number |}'; none of the intersection parts are compatible"); could not be converted into
'(number?) -> {| p: number, q: number, r: number |}'; none of the intersection parts are compatible)";
CHECK_EQ(expected, toString(result.errors[0]));
} }
TEST_CASE_FIXTURE(Fixture, "overloaded_functions_mentioning_generic") TEST_CASE_FIXTURE(Fixture, "overloaded_functions_mentioning_generic")
{ {
ScopedFastFlag sff{"LuauIndentTypeMismatch", true};
ScopedFastInt sfi{"LuauIndentTypeMismatchMaxTypeLength", 10};
CheckResult result = check(R"( CheckResult result = check(R"(
function f<a>() function f<a>()
local x : ((number?) -> (a | number)) & ((string?) -> (a | string)) local x : ((number?) -> (a | number)) & ((string?) -> (a | string))
@ -574,12 +625,18 @@ TEST_CASE_FIXTURE(Fixture, "overloaded_functions_mentioning_generic")
)"); )");
LUAU_REQUIRE_ERROR_COUNT(1, result); LUAU_REQUIRE_ERROR_COUNT(1, result);
CHECK_EQ(toString(result.errors[0]), "Type '((number?) -> a | number) & ((string?) -> a | string)' could not be converted into '(number?) -> a'; " const std::string expected = R"(Type
"none of the intersection parts are compatible"); '((number?) -> a | number) & ((string?) -> a | string)'
could not be converted into
'(number?) -> a'; none of the intersection parts are compatible)";
CHECK_EQ(expected, toString(result.errors[0]));
} }
TEST_CASE_FIXTURE(Fixture, "overloaded_functions_mentioning_generics") TEST_CASE_FIXTURE(Fixture, "overloaded_functions_mentioning_generics")
{ {
ScopedFastFlag sff{"LuauIndentTypeMismatch", true};
ScopedFastInt sfi{"LuauIndentTypeMismatchMaxTypeLength", 10};
CheckResult result = check(R"( CheckResult result = check(R"(
function f<a,b,c>() function f<a,b,c>()
local x : ((a?) -> (a | b)) & ((c?) -> (b | c)) local x : ((a?) -> (a | b)) & ((c?) -> (b | c))
@ -589,12 +646,18 @@ TEST_CASE_FIXTURE(Fixture, "overloaded_functions_mentioning_generics")
)"); )");
LUAU_REQUIRE_ERROR_COUNT(1, result); LUAU_REQUIRE_ERROR_COUNT(1, result);
CHECK_EQ(toString(result.errors[0]), const std::string expected = R"(Type
"Type '((a?) -> a | b) & ((c?) -> b | c)' could not be converted into '(a?) -> (a & c) | b'; none of the intersection parts are compatible"); '((a?) -> a | b) & ((c?) -> b | c)'
could not be converted into
'(a?) -> (a & c) | b'; none of the intersection parts are compatible)";
CHECK_EQ(expected, toString(result.errors[0]));
} }
TEST_CASE_FIXTURE(Fixture, "overloaded_functions_mentioning_generic_packs") TEST_CASE_FIXTURE(Fixture, "overloaded_functions_mentioning_generic_packs")
{ {
ScopedFastFlag sff{"LuauIndentTypeMismatch", true};
ScopedFastInt sfi{"LuauIndentTypeMismatchMaxTypeLength", 10};
CheckResult result = check(R"( CheckResult result = check(R"(
function f<a...,b...>() function f<a...,b...>()
local x : ((number?, a...) -> (number?, b...)) & ((string?, a...) -> (string?, b...)) local x : ((number?, a...) -> (number?, b...)) & ((string?, a...) -> (string?, b...))
@ -604,12 +667,18 @@ TEST_CASE_FIXTURE(Fixture, "overloaded_functions_mentioning_generic_packs")
)"); )");
LUAU_REQUIRE_ERROR_COUNT(1, result); LUAU_REQUIRE_ERROR_COUNT(1, result);
CHECK_EQ(toString(result.errors[0]), "Type '((number?, a...) -> (number?, b...)) & ((string?, a...) -> (string?, b...))' could not be converted " const std::string expected = R"(Type
"into '(nil, b...) -> (nil, a...)'; none of the intersection parts are compatible"); '((number?, a...) -> (number?, b...)) & ((string?, a...) -> (string?, b...))'
could not be converted into
'(nil, b...) -> (nil, a...)'; none of the intersection parts are compatible)";
CHECK_EQ(expected, toString(result.errors[0]));
} }
TEST_CASE_FIXTURE(Fixture, "overloadeded_functions_with_unknown_result") TEST_CASE_FIXTURE(Fixture, "overloadeded_functions_with_unknown_result")
{ {
ScopedFastFlag sff{"LuauIndentTypeMismatch", true};
ScopedFastInt sfi{"LuauIndentTypeMismatchMaxTypeLength", 10};
CheckResult result = check(R"( CheckResult result = check(R"(
function f<a...,b...>() function f<a...,b...>()
local x : ((number) -> number) & ((nil) -> unknown) local x : ((number) -> number) & ((nil) -> unknown)
@ -619,12 +688,18 @@ TEST_CASE_FIXTURE(Fixture, "overloadeded_functions_with_unknown_result")
)"); )");
LUAU_REQUIRE_ERROR_COUNT(1, result); LUAU_REQUIRE_ERROR_COUNT(1, result);
CHECK_EQ(toString(result.errors[0]), "Type '((nil) -> unknown) & ((number) -> number)' could not be converted into '(number?) -> number?'; none " const std::string expected = R"(Type
"of the intersection parts are compatible"); '((nil) -> unknown) & ((number) -> number)'
could not be converted into
'(number?) -> number?'; none of the intersection parts are compatible)";
CHECK_EQ(expected, toString(result.errors[0]));
} }
TEST_CASE_FIXTURE(Fixture, "overloadeded_functions_with_unknown_arguments") TEST_CASE_FIXTURE(Fixture, "overloadeded_functions_with_unknown_arguments")
{ {
ScopedFastFlag sff{"LuauIndentTypeMismatch", true};
ScopedFastInt sfi{"LuauIndentTypeMismatchMaxTypeLength", 10};
CheckResult result = check(R"( CheckResult result = check(R"(
function f<a...,b...>() function f<a...,b...>()
local x : ((number) -> number?) & ((unknown) -> string?) local x : ((number) -> number?) & ((unknown) -> string?)
@ -634,12 +709,18 @@ TEST_CASE_FIXTURE(Fixture, "overloadeded_functions_with_unknown_arguments")
)"); )");
LUAU_REQUIRE_ERROR_COUNT(1, result); LUAU_REQUIRE_ERROR_COUNT(1, result);
CHECK_EQ(toString(result.errors[0]), "Type '((number) -> number?) & ((unknown) -> string?)' could not be converted into '(number?) -> nil'; none " const std::string expected = R"(Type
"of the intersection parts are compatible"); '((number) -> number?) & ((unknown) -> string?)'
could not be converted into
'(number?) -> nil'; none of the intersection parts are compatible)";
CHECK_EQ(expected, toString(result.errors[0]));
} }
TEST_CASE_FIXTURE(Fixture, "overloadeded_functions_with_never_result") TEST_CASE_FIXTURE(Fixture, "overloadeded_functions_with_never_result")
{ {
ScopedFastFlag sff{"LuauIndentTypeMismatch", true};
ScopedFastInt sfi{"LuauIndentTypeMismatchMaxTypeLength", 10};
CheckResult result = check(R"( CheckResult result = check(R"(
function f<a...,b...>() function f<a...,b...>()
local x : ((number) -> number) & ((nil) -> never) local x : ((number) -> number) & ((nil) -> never)
@ -649,12 +730,18 @@ TEST_CASE_FIXTURE(Fixture, "overloadeded_functions_with_never_result")
)"); )");
LUAU_REQUIRE_ERROR_COUNT(1, result); LUAU_REQUIRE_ERROR_COUNT(1, result);
CHECK_EQ(toString(result.errors[0]), "Type '((nil) -> never) & ((number) -> number)' could not be converted into '(number?) -> never'; none of " const std::string expected = R"(Type
"the intersection parts are compatible"); '((nil) -> never) & ((number) -> number)'
could not be converted into
'(number?) -> never'; none of the intersection parts are compatible)";
CHECK_EQ(expected, toString(result.errors[0]));
} }
TEST_CASE_FIXTURE(Fixture, "overloadeded_functions_with_never_arguments") TEST_CASE_FIXTURE(Fixture, "overloadeded_functions_with_never_arguments")
{ {
ScopedFastFlag sff{"LuauIndentTypeMismatch", true};
ScopedFastInt sfi{"LuauIndentTypeMismatchMaxTypeLength", 10};
CheckResult result = check(R"( CheckResult result = check(R"(
function f<a...,b...>() function f<a...,b...>()
local x : ((number) -> number?) & ((never) -> string?) local x : ((number) -> number?) & ((never) -> string?)
@ -664,12 +751,18 @@ TEST_CASE_FIXTURE(Fixture, "overloadeded_functions_with_never_arguments")
)"); )");
LUAU_REQUIRE_ERROR_COUNT(1, result); LUAU_REQUIRE_ERROR_COUNT(1, result);
CHECK_EQ(toString(result.errors[0]), "Type '((never) -> string?) & ((number) -> number?)' could not be converted into '(number?) -> nil'; none " const std::string expected = R"(Type
"of the intersection parts are compatible"); '((never) -> string?) & ((number) -> number?)'
could not be converted into
'(number?) -> nil'; none of the intersection parts are compatible)";
CHECK_EQ(expected, toString(result.errors[0]));
} }
TEST_CASE_FIXTURE(Fixture, "overloadeded_functions_with_overlapping_results_and_variadics") TEST_CASE_FIXTURE(Fixture, "overloadeded_functions_with_overlapping_results_and_variadics")
{ {
ScopedFastFlag sff{"LuauIndentTypeMismatch", true};
ScopedFastInt sfi{"LuauIndentTypeMismatchMaxTypeLength", 10};
CheckResult result = check(R"( CheckResult result = check(R"(
local x : ((string?) -> (string | number)) & ((number?) -> ...number) local x : ((string?) -> (string | number)) & ((number?) -> ...number)
local y : ((nil) -> (number, number?)) = x -- OK local y : ((nil) -> (number, number?)) = x -- OK
@ -677,8 +770,11 @@ TEST_CASE_FIXTURE(Fixture, "overloadeded_functions_with_overlapping_results_and_
)"); )");
LUAU_REQUIRE_ERROR_COUNT(1, result); LUAU_REQUIRE_ERROR_COUNT(1, result);
CHECK_EQ(toString(result.errors[0]), "Type '((number?) -> (...number)) & ((string?) -> number | string)' could not be converted into '(number | " const std::string expected = R"(Type
"string) -> (number, number?)'; none of the intersection parts are compatible"); '((number?) -> (...number)) & ((string?) -> number | string)'
could not be converted into
'(number | string) -> (number, number?)'; none of the intersection parts are compatible)";
CHECK_EQ(expected, toString(result.errors[0]));
} }
TEST_CASE_FIXTURE(Fixture, "overloadeded_functions_with_weird_typepacks_1") TEST_CASE_FIXTURE(Fixture, "overloadeded_functions_with_weird_typepacks_1")
@ -713,6 +809,9 @@ TEST_CASE_FIXTURE(Fixture, "overloadeded_functions_with_weird_typepacks_2")
TEST_CASE_FIXTURE(Fixture, "overloadeded_functions_with_weird_typepacks_3") TEST_CASE_FIXTURE(Fixture, "overloadeded_functions_with_weird_typepacks_3")
{ {
ScopedFastFlag sff{"LuauIndentTypeMismatch", true};
ScopedFastInt sfi{"LuauIndentTypeMismatchMaxTypeLength", 10};
CheckResult result = check(R"( CheckResult result = check(R"(
function f<a...>() function f<a...>()
local x : (() -> a...) & (() -> (number?,a...)) local x : (() -> a...) & (() -> (number?,a...))
@ -722,12 +821,18 @@ TEST_CASE_FIXTURE(Fixture, "overloadeded_functions_with_weird_typepacks_3")
)"); )");
LUAU_REQUIRE_ERROR_COUNT(1, result); LUAU_REQUIRE_ERROR_COUNT(1, result);
CHECK_EQ(toString(result.errors[0]), const std::string expected = R"(Type
"Type '(() -> (a...)) & (() -> (number?, a...))' could not be converted into '() -> number'; none of the intersection parts are compatible"); '(() -> (a...)) & (() -> (number?, a...))'
could not be converted into
'() -> number'; none of the intersection parts are compatible)";
CHECK_EQ(expected, toString(result.errors[0]));
} }
TEST_CASE_FIXTURE(Fixture, "overloadeded_functions_with_weird_typepacks_4") TEST_CASE_FIXTURE(Fixture, "overloadeded_functions_with_weird_typepacks_4")
{ {
ScopedFastFlag sff{"LuauIndentTypeMismatch", true};
ScopedFastInt sfi{"LuauIndentTypeMismatchMaxTypeLength", 10};
CheckResult result = check(R"( CheckResult result = check(R"(
function f<a...>() function f<a...>()
local x : ((a...) -> ()) & ((number,a...) -> number) local x : ((a...) -> ()) & ((number,a...) -> number)
@ -737,8 +842,11 @@ TEST_CASE_FIXTURE(Fixture, "overloadeded_functions_with_weird_typepacks_4")
)"); )");
LUAU_REQUIRE_ERROR_COUNT(1, result); LUAU_REQUIRE_ERROR_COUNT(1, result);
CHECK_EQ(toString(result.errors[0]), "Type '((a...) -> ()) & ((number, a...) -> number)' could not be converted into '(number?) -> ()'; none of " const std::string expected = R"(Type
"the intersection parts are compatible"); '((a...) -> ()) & ((number, a...) -> number)'
could not be converted into
'(number?) -> ()'; none of the intersection parts are compatible)";
CHECK_EQ(expected, toString(result.errors[0]));
} }
TEST_CASE_FIXTURE(BuiltinsFixture, "intersect_metatables") TEST_CASE_FIXTURE(BuiltinsFixture, "intersect_metatables")
@ -897,8 +1005,6 @@ local y = x.Bar
TEST_CASE_FIXTURE(BuiltinsFixture, "index_property_table_intersection_2") TEST_CASE_FIXTURE(BuiltinsFixture, "index_property_table_intersection_2")
{ {
ScopedFastFlag sff{"LuauIndexTableIntersectionStringExpr", true};
CheckResult result = check(R"( CheckResult result = check(R"(
type Foo = { type Foo = {
Bar: string, Bar: string,

View file

@ -389,6 +389,9 @@ type Table = typeof(tbl)
TEST_CASE_FIXTURE(BuiltinsFixture, "module_type_conflict") TEST_CASE_FIXTURE(BuiltinsFixture, "module_type_conflict")
{ {
ScopedFastFlag sff{"LuauIndentTypeMismatch", true};
ScopedFastInt sfi{"LuauIndentTypeMismatchMaxTypeLength", 10};
fileResolver.source["game/A"] = R"( fileResolver.source["game/A"] = R"(
export type T = { x: number } export type T = { x: number }
return {} return {}
@ -407,15 +410,19 @@ local b: B.T = a
)"; )";
CheckResult result = frontend.check("game/C"); CheckResult result = frontend.check("game/C");
LUAU_REQUIRE_ERROR_COUNT(1, result); const std::string expected = R"(Type 'T' from 'game/A' could not be converted into 'T' from 'game/B'
CHECK_EQ(toString(result.errors[0]), R"(Type 'T' from 'game/A' could not be converted into 'T' from 'game/B'
caused by: caused by:
Property 'x' is not compatible. Type 'number' could not be converted into 'string' in an invariant context)"); Property 'x' is not compatible.
Type 'number' could not be converted into 'string' in an invariant context)";
LUAU_REQUIRE_ERROR_COUNT(1, result);
CHECK_EQ(expected, toString(result.errors[0]));
} }
TEST_CASE_FIXTURE(BuiltinsFixture, "module_type_conflict_instantiated") TEST_CASE_FIXTURE(BuiltinsFixture, "module_type_conflict_instantiated")
{ {
ScopedFastFlag sff{"LuauIndentTypeMismatch", true};
ScopedFastInt sfi{"LuauIndentTypeMismatchMaxTypeLength", 10};
fileResolver.source["game/A"] = R"( fileResolver.source["game/A"] = R"(
export type Wrap<T> = { x: T } export type Wrap<T> = { x: T }
return {} return {}
@ -441,11 +448,12 @@ local b: B.T = a
)"; )";
CheckResult result = frontend.check("game/D"); CheckResult result = frontend.check("game/D");
LUAU_REQUIRE_ERROR_COUNT(1, result); const std::string expected = R"(Type 'T' from 'game/B' could not be converted into 'T' from 'game/C'
CHECK_EQ(toString(result.errors[0]), R"(Type 'T' from 'game/B' could not be converted into 'T' from 'game/C'
caused by: caused by:
Property 'x' is not compatible. Type 'number' could not be converted into 'string' in an invariant context)"); Property 'x' is not compatible.
Type 'number' could not be converted into 'string' in an invariant context)";
LUAU_REQUIRE_ERROR_COUNT(1, result);
CHECK_EQ(expected, toString(result.errors[0]));
} }
TEST_CASE_FIXTURE(BuiltinsFixture, "constrained_anyification_clone_immutable_types") TEST_CASE_FIXTURE(BuiltinsFixture, "constrained_anyification_clone_immutable_types")

View file

@ -9,6 +9,7 @@
#include "Fixture.h" #include "Fixture.h"
#include "ScopedFlags.h"
#include "doctest.h" #include "doctest.h"
using namespace Luau; using namespace Luau;
@ -404,4 +405,92 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "cycle_between_object_constructor_and_alias")
CHECK_MESSAGE(get<MetatableType>(follow(aliasType)), "Expected metatable type but got: " << toString(aliasType)); CHECK_MESSAGE(get<MetatableType>(follow(aliasType)), "Expected metatable type but got: " << toString(aliasType));
} }
TEST_CASE_FIXTURE(BuiltinsFixture, "promise_type_error_too_complex")
{
ScopedFastFlag sff{"LuauStacklessTypeClone", true};
frontend.options.retainFullTypeGraphs = false;
// Used `luau-reduce` tool to extract a minimal reproduction.
// Credit: https://github.com/evaera/roblox-lua-promise/blob/v4.0.0/lib/init.lua
CheckResult result = check(R"(
--!strict
local Promise = {}
Promise.prototype = {}
Promise.__index = Promise.prototype
function Promise._new(traceback, callback, parent)
if parent ~= nil and not Promise.is(parent)then
end
local self = {
_parent = parent,
}
parent._consumers[self] = true
setmetatable(self, Promise)
self:_reject()
return self
end
function Promise.resolve(...)
return Promise._new(debug.traceback(nil, 2), function(resolve)
end)
end
function Promise.reject(...)
return Promise._new(debug.traceback(nil, 2), function(_, reject)
end)
end
function Promise._try(traceback, callback, ...)
return Promise._new(traceback, function(resolve)
end)
end
function Promise.try(callback, ...)
return Promise._try(debug.traceback(nil, 2), callback, ...)
end
function Promise._all(traceback, promises, amount)
if #promises == 0 or amount == 0 then
return Promise.resolve({})
end
return Promise._new(traceback, function(resolve, reject, onCancel)
end)
end
function Promise.all(promises)
return Promise._all(debug.traceback(nil, 2), promises)
end
function Promise.allSettled(promises)
return Promise.resolve({})
end
function Promise.race(promises)
return Promise._new(debug.traceback(nil, 2), function(resolve, reject, onCancel)
end)
end
function Promise.each(list, predicate)
return Promise._new(debug.traceback(nil, 2), function(resolve, reject, onCancel)
local predicatePromise = Promise.resolve(predicate(value, index))
local success, result = predicatePromise:await()
end)
end
function Promise.is(object)
end
function Promise.prototype:_reject(...)
self:_finalize()
end
)");
LUAU_REQUIRE_ERRORS(result);
}
TEST_SUITE_END(); TEST_SUITE_END();

View file

@ -787,6 +787,9 @@ TEST_CASE_FIXTURE(IsSubtypeFixture, "functions_with_mismatching_arity_but_any_is
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 sff{"LuauIndentTypeMismatch", true};
ScopedFastInt sfi{"LuauIndentTypeMismatchMaxTypeLength", 10};
CheckResult result = check(R"( CheckResult result = check(R"(
local t: {x: number?} = {x = nil} local t: {x: number?} = {x = nil}
@ -796,11 +799,14 @@ TEST_CASE_FIXTURE(Fixture, "assign_table_with_refined_property_with_a_similar_ty
)"); )");
LUAU_REQUIRE_ERROR_COUNT(1, result); LUAU_REQUIRE_ERROR_COUNT(1, result);
const std::string expected = R"(Type
CHECK_EQ(R"(Type '{| x: number? |}' could not be converted into '{| x: number |}' '{| x: number? |}'
could not be converted into
'{| x: number |}'
caused by: caused by:
Property 'x' is not compatible. Type 'number?' could not be converted into 'number' in an invariant context)", Property 'x' is not compatible.
toString(result.errors[0])); Type 'number?' could not be converted into 'number' in an invariant context)";
CHECK_EQ(expected, toString(result.errors[0]));
} }
TEST_CASE_FIXTURE(BuiltinsFixture, "table_insert_with_a_singleton_argument") TEST_CASE_FIXTURE(BuiltinsFixture, "table_insert_with_a_singleton_argument")

View file

@ -316,6 +316,9 @@ TEST_CASE_FIXTURE(Fixture, "table_properties_type_error_escapes")
TEST_CASE_FIXTURE(Fixture, "error_detailed_tagged_union_mismatch_string") TEST_CASE_FIXTURE(Fixture, "error_detailed_tagged_union_mismatch_string")
{ {
ScopedFastFlag sff{"LuauIndentTypeMismatch", true};
ScopedFastInt sfi{"LuauIndentTypeMismatchMaxTypeLength", 10};
CheckResult result = check(R"( CheckResult result = check(R"(
type Cat = { tag: 'cat', catfood: string } type Cat = { tag: 'cat', catfood: string }
type Dog = { tag: 'dog', dogfood: string } type Dog = { tag: 'dog', dogfood: string }
@ -325,14 +328,18 @@ local a: Animal = { tag = 'cat', cafood = 'something' }
)"); )");
LUAU_REQUIRE_ERROR_COUNT(1, result); LUAU_REQUIRE_ERROR_COUNT(1, result);
CHECK_EQ(R"(Type 'a' could not be converted into 'Cat | Dog' const std::string expected = R"(Type 'a' could not be converted into 'Cat | Dog'
caused by: 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')", None of the union options are compatible. For example:
toString(result.errors[0])); Table type 'a' not compatible with type 'Cat' because the former is missing field 'catfood')";
CHECK_EQ(expected, toString(result.errors[0]));
} }
TEST_CASE_FIXTURE(Fixture, "error_detailed_tagged_union_mismatch_bool") TEST_CASE_FIXTURE(Fixture, "error_detailed_tagged_union_mismatch_bool")
{ {
ScopedFastFlag sff{"LuauIndentTypeMismatch", true};
ScopedFastInt sfi{"LuauIndentTypeMismatchMaxTypeLength", 10};
CheckResult result = check(R"( CheckResult result = check(R"(
type Good = { success: true, result: string } type Good = { success: true, result: string }
type Bad = { success: false, error: string } type Bad = { success: false, error: string }
@ -342,18 +349,20 @@ local a: Result = { success = false, result = 'something' }
)"); )");
LUAU_REQUIRE_ERROR_COUNT(1, result); LUAU_REQUIRE_ERROR_COUNT(1, result);
CHECK_EQ(R"(Type 'a' could not be converted into 'Bad | Good' const std::string expected = R"(Type 'a' could not be converted into 'Bad | Good'
caused by: 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')", None of the union options are compatible. For example:
toString(result.errors[0])); Table type 'a' not compatible with type 'Bad' because the former is missing field 'error')";
CHECK_EQ(expected, toString(result.errors[0]));
} }
TEST_CASE_FIXTURE(Fixture, "parametric_tagged_union_alias") TEST_CASE_FIXTURE(Fixture, "parametric_tagged_union_alias")
{ {
ScopedFastFlag sff[] = { ScopedFastFlag sff[] = {
{"DebugLuauDeferredConstraintResolution", true}, {"DebugLuauDeferredConstraintResolution", true},
{"LuauIndentTypeMismatch", true},
}; };
ScopedFastInt sfi{"LuauIndentTypeMismatchMaxTypeLength", 10};
CheckResult result = check(R"( CheckResult result = check(R"(
type Ok<T> = {success: true, result: T} type Ok<T> = {success: true, result: T}
type Err<T> = {success: false, error: T} type Err<T> = {success: false, error: T}
@ -365,10 +374,10 @@ TEST_CASE_FIXTURE(Fixture, "parametric_tagged_union_alias")
LUAU_REQUIRE_ERROR_COUNT(1, result); LUAU_REQUIRE_ERROR_COUNT(1, result);
const std::string expectedError = "Type 'a' could not be converted into 'Err<number> | Ok<string>'\n" const std::string expectedError = R"(Type 'a' could not be converted into 'Err<number> | Ok<string>'
"caused by:\n" caused by:
" None of the union options are compatible. For example: Table type 'a'" None of the union options are compatible. For example:
" not compatible with type 'Err<number>' because the former is missing field 'error'"; Table type 'a' not compatible with type 'Err<number>' because the former is missing field 'error')";
CHECK(toString(result.errors[0]) == expectedError); CHECK(toString(result.errors[0]) == expectedError);
} }

View file

@ -2083,6 +2083,8 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "table_insert_should_cope_with_optional_prope
TEST_CASE_FIXTURE(Fixture, "error_detailed_prop") TEST_CASE_FIXTURE(Fixture, "error_detailed_prop")
{ {
ScopedFastFlag sff[] = {{"LuauIndentTypeMismatch", true}};
ScopedFastInt sfi[] = {{"LuauIndentTypeMismatchMaxTypeLength", 10}};
CheckResult result = check(R"( CheckResult result = check(R"(
type A = { x: number, y: number } type A = { x: number, y: number }
type B = { x: number, y: string } type B = { x: number, y: string }
@ -2092,13 +2094,17 @@ local b: B = a
)"); )");
LUAU_REQUIRE_ERRORS(result); LUAU_REQUIRE_ERRORS(result);
CHECK_EQ(toString(result.errors[0]), R"(Type 'A' could not be converted into 'B' const std::string expected = R"(Type 'A' could not be converted into 'B'
caused by: caused by:
Property 'y' is not compatible. Type 'number' could not be converted into 'string' in an invariant context)"); Property 'y' is not compatible.
Type 'number' could not be converted into 'string' in an invariant context)";
CHECK_EQ(expected, toString(result.errors[0]));
} }
TEST_CASE_FIXTURE(Fixture, "error_detailed_prop_nested") TEST_CASE_FIXTURE(Fixture, "error_detailed_prop_nested")
{ {
ScopedFastFlag sff[] = {{"LuauIndentTypeMismatch", true}};
ScopedFastInt sfi[] = {{"LuauIndentTypeMismatchMaxTypeLength", 10}};
CheckResult result = check(R"( CheckResult result = check(R"(
type AS = { x: number, y: number } type AS = { x: number, y: number }
type BS = { x: number, y: string } type BS = { x: number, y: string }
@ -2111,15 +2117,21 @@ local b: B = a
)"); )");
LUAU_REQUIRE_ERRORS(result); LUAU_REQUIRE_ERRORS(result);
CHECK_EQ(toString(result.errors[0]), R"(Type 'A' could not be converted into 'B' const std::string expected = R"(Type 'A' could not be converted into 'B'
caused by: caused by:
Property 'b' is not compatible. Type 'AS' could not be converted into 'BS' Property 'b' is not compatible.
Type 'AS' could not be converted into 'BS'
caused by: caused by:
Property 'y' is not compatible. Type 'number' could not be converted into 'string' in an invariant context)"); Property 'y' is not compatible.
Type 'number' could not be converted into 'string' in an invariant context)";
CHECK_EQ(expected, toString(result.errors[0]));
} }
TEST_CASE_FIXTURE(BuiltinsFixture, "error_detailed_metatable_prop") TEST_CASE_FIXTURE(BuiltinsFixture, "error_detailed_metatable_prop")
{ {
ScopedFastFlag sff{"LuauIndentTypeMismatch", true};
ScopedFastInt sfi{"LuauIndentTypeMismatchMaxTypeLength", 10};
CheckResult result = check(R"( CheckResult result = check(R"(
local a1 = setmetatable({ x = 2, y = 3 }, { __call = function(s) end }); local a1 = setmetatable({ x = 2, y = 3 }, { __call = function(s) end });
local b1 = setmetatable({ x = 2, y = "hello" }, { __call = function(s) end }); local b1 = setmetatable({ x = 2, y = "hello" }, { __call = function(s) end });
@ -2130,33 +2142,68 @@ local b2 = setmetatable({ x = 2, y = 4 }, { __call = function(s, t) end });
local c2: typeof(a2) = b2 local c2: typeof(a2) = b2
)"); )");
LUAU_REQUIRE_ERROR_COUNT(2, result); const std::string expected1 = R"(Type 'b1' could not be converted into 'a1'
CHECK_EQ(toString(result.errors[0]), R"(Type 'b1' could not be converted into 'a1'
caused by: caused by:
Type '{ x: number, y: string }' could not be converted into '{ x: number, y: number }' Type
'{ x: number, y: string }'
could not be converted into
'{ x: number, y: number }'
caused by: caused by:
Property 'y' is not compatible. Type 'string' could not be converted into 'number' in an invariant context)"); Property 'y' is not compatible.
Type 'string' could not be converted into 'number' in an invariant context)";
const std::string expected2 = R"(Type 'b2' could not be converted into 'a2'
caused by:
Type
'{ __call: <a, b>(a, b) -> () }'
could not be converted into
'{ __call: <a>(a) -> () }'
caused by:
Property '__call' is not compatible.
Type
'<a, b>(a, b) -> ()'
could not be converted into
'<a>(a) -> ()'; different number of generic type parameters)";
const std::string expected3 = R"(Type 'b2' could not be converted into 'a2'
caused by:
Type
'{ __call: <a, b>(a, b) -> () }'
could not be converted into
'{ __call: <a>(a) -> () }'
caused by:
Property '__call' is not compatible.
Type
'<a, b>(a, b) -> ()'
could not be converted into
'<a>(a) -> ()'; different number of generic type parameters)";
LUAU_REQUIRE_ERROR_COUNT(2, result);
CHECK_EQ(expected1, toString(result.errors[0]));
if (FFlag::LuauInstantiateInSubtyping) if (FFlag::LuauInstantiateInSubtyping)
{ {
CHECK_EQ(toString(result.errors[1]), R"(Type 'b2' could not be converted into 'a2' CHECK_EQ(expected2, toString(result.errors[1]));
caused by:
Type '{ __call: <a, b>(a, b) -> () }' could not be converted into '{ __call: <a>(a) -> () }'
caused by:
Property '__call' is not compatible. Type '<a, b>(a, b) -> ()' could not be converted into '<a>(a) -> ()'; different number of generic type parameters)");
} }
else else
{ {
CHECK_EQ(toString(result.errors[1]), R"(Type 'b2' could not be converted into 'a2' std::string expected3 = 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) -> ()'; different number of generic type parameters)"); Property '__call' is not compatible.
Type
'(a, b) -> ()'
could not be converted into
'<a>(a) -> ()'; different number of generic type parameters)";
CHECK_EQ(expected3, toString(result.errors[1]));
} }
} }
TEST_CASE_FIXTURE(Fixture, "error_detailed_indexer_key") TEST_CASE_FIXTURE(Fixture, "error_detailed_indexer_key")
{ {
ScopedFastFlag sff[] = {{"LuauIndentTypeMismatch", true}};
ScopedFastInt sfi[] = {{"LuauIndentTypeMismatchMaxTypeLength", 10}};
CheckResult result = check(R"( CheckResult result = check(R"(
type A = { [number]: string } type A = { [number]: string }
type B = { [string]: string } type B = { [string]: string }
@ -2166,13 +2213,17 @@ TEST_CASE_FIXTURE(Fixture, "error_detailed_indexer_key")
)"); )");
LUAU_REQUIRE_ERRORS(result); LUAU_REQUIRE_ERRORS(result);
CHECK_EQ(toString(result.errors[0]), R"(Type 'A' could not be converted into 'B' const std::string expected = R"(Type 'A' could not be converted into 'B'
caused by: caused by:
Property '[indexer key]' is not compatible. Type 'number' could not be converted into 'string' in an invariant context)"); Property '[indexer key]' is not compatible.
Type 'number' could not be converted into 'string' in an invariant context)";
CHECK_EQ(expected, toString(result.errors[0]));
} }
TEST_CASE_FIXTURE(Fixture, "error_detailed_indexer_value") TEST_CASE_FIXTURE(Fixture, "error_detailed_indexer_value")
{ {
ScopedFastFlag sff[] = {{"LuauIndentTypeMismatch", true}};
ScopedFastInt sfi[] = {{"LuauIndentTypeMismatchMaxTypeLength", 10}};
CheckResult result = check(R"( CheckResult result = check(R"(
type A = { [number]: number } type A = { [number]: number }
type B = { [number]: string } type B = { [number]: string }
@ -2182,9 +2233,11 @@ TEST_CASE_FIXTURE(Fixture, "error_detailed_indexer_value")
)"); )");
LUAU_REQUIRE_ERRORS(result); LUAU_REQUIRE_ERRORS(result);
CHECK_EQ(toString(result.errors[0]), R"(Type 'A' could not be converted into 'B' const std::string expected = R"(Type 'A' could not be converted into 'B'
caused by: caused by:
Property '[indexer value]' is not compatible. Type 'number' could not be converted into 'string' in an invariant context)"); Property '[indexer value]' is not compatible.
Type 'number' could not be converted into 'string' in an invariant context)";
CHECK_EQ(expected, toString(result.errors[0]));
} }
TEST_CASE_FIXTURE(Fixture, "explicitly_typed_table") TEST_CASE_FIXTURE(Fixture, "explicitly_typed_table")
@ -2204,6 +2257,8 @@ a.p = { x = 9 }
TEST_CASE_FIXTURE(Fixture, "explicitly_typed_table_error") TEST_CASE_FIXTURE(Fixture, "explicitly_typed_table_error")
{ {
ScopedFastFlag sff[] = {{"LuauIndentTypeMismatch", true}};
ScopedFastInt sfi[] = {{"LuauIndentTypeMismatchMaxTypeLength", 10}};
CheckResult result = check(R"( CheckResult result = check(R"(
--!strict --!strict
type Super = { x : number } type Super = { x : number }
@ -2218,9 +2273,11 @@ local y: number = tmp.p.y
)"); )");
LUAU_REQUIRE_ERROR_COUNT(1, result); LUAU_REQUIRE_ERROR_COUNT(1, result);
CHECK_EQ(toString(result.errors[0]), R"(Type 'tmp' could not be converted into 'HasSuper' const std::string expected = R"(Type 'tmp' could not be converted into 'HasSuper'
caused by: caused by:
Property 'p' is not compatible. Table type '{ x: number, y: number }' not compatible with type 'Super' because the former has extra field 'y')"); Property 'p' is not compatible.
Table type '{ x: number, y: number }' not compatible with type 'Super' because the former has extra field 'y')";
CHECK_EQ(expected, toString(result.errors[0]));
} }
TEST_CASE_FIXTURE(Fixture, "explicitly_typed_table_with_indexer") TEST_CASE_FIXTURE(Fixture, "explicitly_typed_table_with_indexer")
@ -3302,7 +3359,13 @@ TEST_CASE_FIXTURE(Fixture, "scalar_is_a_subtype_of_a_compatible_polymorphic_shap
TEST_CASE_FIXTURE(Fixture, "scalar_is_not_a_subtype_of_a_compatible_polymorphic_shape_type") TEST_CASE_FIXTURE(Fixture, "scalar_is_not_a_subtype_of_a_compatible_polymorphic_shape_type")
{ {
ScopedFastFlag sff{"LuauAlwaysCommitInferencesOfFunctionCalls", true}; ScopedFastInt sfi[] = {{"LuauIndentTypeMismatchMaxTypeLength", 10}};
ScopedFastFlag sff[] = {
{"LuauAlwaysCommitInferencesOfFunctionCalls", true},
{"LuauIndentTypeMismatch", true},
};
CheckResult result = check(R"( CheckResult result = check(R"(
local function f(s) local function f(s)
@ -3316,22 +3379,32 @@ TEST_CASE_FIXTURE(Fixture, "scalar_is_not_a_subtype_of_a_compatible_polymorphic_
LUAU_REQUIRE_ERROR_COUNT(3, result); LUAU_REQUIRE_ERROR_COUNT(3, result);
CHECK_EQ(R"(Type 'string' could not be converted into 't1 where t1 = {- absolutely_no_scalar_has_this_method: (t1) -> (a...) -}' const std::string expected1 =
R"(Type 'string' could not be converted into 't1 where t1 = {- absolutely_no_scalar_has_this_method: (t1) -> (a...) -}'
caused by: caused by:
The former's metatable does not satisfy the requirements. Table type 'typeof(string)' not compatible with type 't1 where t1 = {- absolutely_no_scalar_has_this_method: (t1) -> (a...) -}' because the former is missing field 'absolutely_no_scalar_has_this_method')", The former's metatable does not satisfy the requirements.
toString(result.errors[0])); Table type 'typeof(string)' not compatible with type 't1 where t1 = {- absolutely_no_scalar_has_this_method: (t1) -> (a...) -}' because the former is missing field 'absolutely_no_scalar_has_this_method')";
CHECK_EQ(expected1, toString(result.errors[0]));
CHECK_EQ(R"(Type '"bar"' could not be converted into 't1 where t1 = {- absolutely_no_scalar_has_this_method: (t1) -> (a...) -}'
caused by:
The former's metatable does not satisfy the requirements. Table type 'typeof(string)' not compatible with type 't1 where t1 = {- absolutely_no_scalar_has_this_method: (t1) -> (a...) -}' because the former is missing field 'absolutely_no_scalar_has_this_method')",
toString(result.errors[1]));
CHECK_EQ(R"(Type '"bar" | "baz"' could not be converted into 't1 where t1 = {- absolutely_no_scalar_has_this_method: (t1) -> (a...) -}' const std::string expected2 =
R"(Type '"bar"' could not be converted into 't1 where t1 = {- absolutely_no_scalar_has_this_method: (t1) -> (a...) -}'
caused by: caused by:
Not all union options are compatible. Type '"bar"' could not be converted into 't1 where t1 = {- absolutely_no_scalar_has_this_method: (t1) -> (a...) -}' The former's metatable does not satisfy the requirements.
Table type 'typeof(string)' not compatible with type 't1 where t1 = {- absolutely_no_scalar_has_this_method: (t1) -> (a...) -}' because the former is missing field 'absolutely_no_scalar_has_this_method')";
CHECK_EQ(expected2, toString(result.errors[1]));
const std::string expected3 = R"(Type
'"bar" | "baz"'
could not be converted into
't1 where t1 = {- absolutely_no_scalar_has_this_method: (t1) -> (a...) -}'
caused by: caused by:
The former's metatable does not satisfy the requirements. Table type 'typeof(string)' not compatible with type 't1 where t1 = {- absolutely_no_scalar_has_this_method: (t1) -> (a...) -}' because the former is missing field 'absolutely_no_scalar_has_this_method')", Not all union options are compatible.
toString(result.errors[2])); Type '"bar"' could not be converted into 't1 where t1 = {- absolutely_no_scalar_has_this_method: (t1) -> (a...) -}'
caused by:
The former's metatable does not satisfy the requirements.
Table type 'typeof(string)' not compatible with type 't1 where t1 = {- absolutely_no_scalar_has_this_method: (t1) -> (a...) -}' because the former is missing field 'absolutely_no_scalar_has_this_method')";
CHECK_EQ(expected3, toString(result.errors[2]));
} }
TEST_CASE_FIXTURE(Fixture, "a_free_shape_can_turn_into_a_scalar_if_it_is_compatible") TEST_CASE_FIXTURE(Fixture, "a_free_shape_can_turn_into_a_scalar_if_it_is_compatible")
@ -3349,6 +3422,9 @@ TEST_CASE_FIXTURE(Fixture, "a_free_shape_can_turn_into_a_scalar_if_it_is_compati
TEST_CASE_FIXTURE(Fixture, "a_free_shape_cannot_turn_into_a_scalar_if_it_is_not_compatible") TEST_CASE_FIXTURE(Fixture, "a_free_shape_cannot_turn_into_a_scalar_if_it_is_not_compatible")
{ {
ScopedFastFlag sff{"LuauIndentTypeMismatch", true};
ScopedFastInt sfi{"LuauIndentTypeMismatchMaxTypeLength", 10};
CheckResult result = check(R"( CheckResult result = check(R"(
local function f(s): string local function f(s): string
local foo = s:absolutely_no_scalar_has_this_method() local foo = s:absolutely_no_scalar_has_this_method()
@ -3356,11 +3432,13 @@ TEST_CASE_FIXTURE(Fixture, "a_free_shape_cannot_turn_into_a_scalar_if_it_is_not_
end end
)"); )");
LUAU_REQUIRE_ERROR_COUNT(1, result); const std::string expected =
CHECK_EQ(R"(Type 't1 where t1 = {+ absolutely_no_scalar_has_this_method: (t1) -> (a, b...) +}' could not be converted into 'string' R"(Type 't1 where t1 = {+ absolutely_no_scalar_has_this_method: (t1) -> (a, b...) +}' could not be converted into 'string'
caused by: caused by:
The former's metatable does not satisfy the requirements. Table type 'typeof(string)' not compatible with type 't1 where t1 = {+ absolutely_no_scalar_has_this_method: (t1) -> (a, b...) +}' because the former is missing field 'absolutely_no_scalar_has_this_method')", The former's metatable does not satisfy the requirements.
toString(result.errors[0])); Table type 'typeof(string)' not compatible with type 't1 where t1 = {+ absolutely_no_scalar_has_this_method: (t1) -> (a, b...) +}' because the former is missing field 'absolutely_no_scalar_has_this_method')";
LUAU_REQUIRE_ERROR_COUNT(1, result);
CHECK_EQ(expected, toString(result.errors[0]));
CHECK_EQ("<a, b...>(t1) -> string where t1 = {+ absolutely_no_scalar_has_this_method: (t1) -> (a, b...) +}", toString(requireType("f"))); CHECK_EQ("<a, b...>(t1) -> string where t1 = {+ absolutely_no_scalar_has_this_method: (t1) -> (a, b...) +}", toString(requireType("f")));
} }

View file

@ -992,6 +992,9 @@ end
TEST_CASE_FIXTURE(Fixture, "cli_50041_committing_txnlog_in_apollo_client_error") TEST_CASE_FIXTURE(Fixture, "cli_50041_committing_txnlog_in_apollo_client_error")
{ {
ScopedFastFlag sff{"LuauIndentTypeMismatch", true};
ScopedFastInt sfi{"LuauIndentTypeMismatchMaxTypeLength", 10};
CheckResult result = check(R"( CheckResult result = check(R"(
--!strict --!strict
--!nolint --!nolint
@ -1028,16 +1031,23 @@ TEST_CASE_FIXTURE(Fixture, "cli_50041_committing_txnlog_in_apollo_client_error")
// unsound. // unsound.
LUAU_REQUIRE_ERROR_COUNT(1, result); LUAU_REQUIRE_ERROR_COUNT(1, result);
const std::string expected = R"(Type 'Policies' from 'MainModule' could not be converted into 'Policies' from 'MainModule'
CHECK_EQ(
R"(Type 'Policies' from 'MainModule' could not be converted into 'Policies' from 'MainModule'
caused by: caused by:
Property 'getStoreFieldName' is not compatible. Type '(Policies, FieldSpecifier & {| from: number? |}) -> (a, b...)' could not be converted into '(Policies, FieldSpecifier) -> string' Property 'getStoreFieldName' is not compatible.
Type
'(Policies, FieldSpecifier & {| from: number? |}) -> (a, b...)'
could not be converted into
'(Policies, FieldSpecifier) -> string'
caused by: caused by:
Argument #2 type is not compatible. Type 'FieldSpecifier' could not be converted into 'FieldSpecifier & {| from: number? |}' Argument #2 type is not compatible.
Type
'FieldSpecifier'
could not be converted into
'FieldSpecifier & {| from: number? |}'
caused by: caused by:
Not all intersection parts are compatible. Table type 'FieldSpecifier' not compatible with type '{| from: number? |}' because the former has extra field 'fieldName')", Not all intersection parts are compatible.
toString(result.errors[0])); Table type 'FieldSpecifier' not compatible with type '{| from: number? |}' because the former has extra field 'fieldName')";
CHECK_EQ(expected, toString(result.errors[0]));
} }
else else
{ {
@ -1205,8 +1215,6 @@ end
TEST_CASE_FIXTURE(BuiltinsFixture, "typechecking_in_type_guards") TEST_CASE_FIXTURE(BuiltinsFixture, "typechecking_in_type_guards")
{ {
ScopedFastFlag sff{"LuauTypecheckTypeguards", true};
CheckResult result = check(R"( CheckResult result = check(R"(
local a = type(foo) == 'nil' local a = type(foo) == 'nil'
local b = typeof(foo) ~= 'nil' local b = typeof(foo) ~= 'nil'

View file

@ -345,7 +345,9 @@ TEST_CASE_FIXTURE(TryUnifyFixture, "metatables_unify_against_shape_of_free_table
{ {
ScopedFastFlag sff[] = { ScopedFastFlag sff[] = {
{"LuauTransitiveSubtyping", true}, {"LuauTransitiveSubtyping", true},
{"LuauIndentTypeMismatch", true},
}; };
ScopedFastInt sfi{"LuauIndentTypeMismatchMaxTypeLength", 10};
TableType::Props freeProps{ TableType::Props freeProps{
{"foo", {builtinTypes->numberType}}, {"foo", {builtinTypes->numberType}},
@ -373,11 +375,13 @@ TEST_CASE_FIXTURE(TryUnifyFixture, "metatables_unify_against_shape_of_free_table
state.log.commit(); state.log.commit();
REQUIRE_EQ(state.errors.size(), 1); REQUIRE_EQ(state.errors.size(), 1);
const std::string expected = R"(Type
std::string expected = "Type '{ @metatable {| __index: {| foo: string |} |}, { } }' could not be converted into '{- foo: number -}'\n" '{ @metatable {| __index: {| foo: string |} |}, { } }'
"caused by:\n" could not be converted into
" Type 'number' could not be converted into 'string'"; '{- foo: number -}'
CHECK_EQ(toString(state.errors[0]), expected); caused by:
Type 'number' could not be converted into 'string')";
CHECK_EQ(expected, toString(state.errors[0]));
} }
TEST_CASE_FIXTURE(TryUnifyFixture, "fuzz_tail_unification_issue") TEST_CASE_FIXTURE(TryUnifyFixture, "fuzz_tail_unification_issue")

View file

@ -872,6 +872,9 @@ type R = { m: F<R> }
TEST_CASE_FIXTURE(Fixture, "pack_tail_unification_check") TEST_CASE_FIXTURE(Fixture, "pack_tail_unification_check")
{ {
ScopedFastFlag sff{"LuauIndentTypeMismatch", true};
ScopedFastInt sfi{"LuauIndentTypeMismatchMaxTypeLength", 10};
CheckResult result = check(R"( CheckResult result = check(R"(
local a: () -> (number, ...string) local a: () -> (number, ...string)
local b: () -> (number, ...boolean) local b: () -> (number, ...boolean)
@ -879,9 +882,13 @@ a = b
)"); )");
LUAU_REQUIRE_ERROR_COUNT(1, result); LUAU_REQUIRE_ERROR_COUNT(1, result);
CHECK_EQ(toString(result.errors[0]), R"(Type '() -> (number, ...boolean)' could not be converted into '() -> (number, ...string)' const std::string expected = R"(Type
'() -> (number, ...boolean)'
could not be converted into
'() -> (number, ...string)'
caused by: caused by:
Type 'boolean' could not be converted into 'string')"); Type 'boolean' could not be converted into 'string')";
CHECK_EQ(expected, toString(result.errors[0]));
} }
// TODO: File a Jira about this // TODO: File a Jira about this

View file

@ -459,6 +459,8 @@ local oh : boolean = t.y
TEST_CASE_FIXTURE(Fixture, "error_detailed_union_part") TEST_CASE_FIXTURE(Fixture, "error_detailed_union_part")
{ {
ScopedFastFlag sff{"LuauIndentTypeMismatch", true};
ScopedFastInt sfi{"LuauIndentTypeMismatchMaxTypeLength", 10};
CheckResult result = check(R"( CheckResult result = check(R"(
type X = { x: number } type X = { x: number }
type Y = { y: number } type Y = { y: number }
@ -471,9 +473,11 @@ local b: { w: number } = a
)"); )");
LUAU_REQUIRE_ERROR_COUNT(1, result); LUAU_REQUIRE_ERROR_COUNT(1, result);
CHECK_EQ(toString(result.errors[0]), R"(Type 'X | Y | Z' could not be converted into '{| w: number |}' const std::string expected = R"(Type 'X | Y | Z' could not be converted into '{| w: number |}'
caused by: caused by:
Not all union options are compatible. Table type 'X' not compatible with type '{| w: number |}' because the former is missing field 'w')"); Not all union options are compatible.
Table type 'X' not compatible with type '{| w: number |}' because the former is missing field 'w')";
CHECK_EQ(expected, toString(result.errors[0]));
} }
TEST_CASE_FIXTURE(Fixture, "error_detailed_union_all") TEST_CASE_FIXTURE(Fixture, "error_detailed_union_all")
@ -494,6 +498,8 @@ local a: XYZ = { w = 4 }
TEST_CASE_FIXTURE(Fixture, "error_detailed_optional") TEST_CASE_FIXTURE(Fixture, "error_detailed_optional")
{ {
ScopedFastFlag sff{"LuauIndentTypeMismatch", true};
ScopedFastInt sfi{"LuauIndentTypeMismatchMaxTypeLength", 10};
CheckResult result = check(R"( CheckResult result = check(R"(
type X = { x: number } type X = { x: number }
@ -501,9 +507,11 @@ local a: X? = { w = 4 }
)"); )");
LUAU_REQUIRE_ERROR_COUNT(1, result); LUAU_REQUIRE_ERROR_COUNT(1, result);
CHECK_EQ(toString(result.errors[0]), R"(Type 'a' could not be converted into 'X?' const std::string expected = R"(Type 'a' could not be converted into 'X?'
caused by: 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')"); None of the union options are compatible. For example:
Table type 'a' not compatible with type 'X' because the former is missing field 'x')";
CHECK_EQ(expected, toString(result.errors[0]));
} }
// We had a bug where a cyclic union caused a stack overflow. // We had a bug where a cyclic union caused a stack overflow.
@ -524,6 +532,9 @@ TEST_CASE_FIXTURE(Fixture, "dont_allow_cyclic_unions_to_be_inferred")
TEST_CASE_FIXTURE(BuiltinsFixture, "table_union_write_indirect") TEST_CASE_FIXTURE(BuiltinsFixture, "table_union_write_indirect")
{ {
ScopedFastFlag sff{"LuauIndentTypeMismatch", true};
ScopedFastInt sfi{"LuauIndentTypeMismatchMaxTypeLength", 10};
CheckResult result = check(R"( CheckResult result = check(R"(
type A = { x: number, y: (number) -> string } | { z: number, y: (number) -> string } type A = { x: number, y: (number) -> string } | { z: number, y: (number) -> string }
@ -540,8 +551,11 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "table_union_write_indirect")
LUAU_REQUIRE_ERROR_COUNT(1, result); LUAU_REQUIRE_ERROR_COUNT(1, result);
// NOTE: union normalization will improve this message // NOTE: union normalization will improve this message
CHECK_EQ(toString(result.errors[0]), const std::string expected = R"(Type
R"(Type '(string) -> number' could not be converted into '((number) -> string) | ((number) -> string)'; none of the union options are compatible)"); '(string) -> number'
could not be converted into
'((number) -> string) | ((number) -> string)'; none of the union options are compatible)";
CHECK_EQ(expected, toString(result.errors[0]));
} }
TEST_CASE_FIXTURE(Fixture, "union_true_and_false") TEST_CASE_FIXTURE(Fixture, "union_true_and_false")
@ -606,6 +620,8 @@ TEST_CASE_FIXTURE(Fixture, "union_of_functions_mentioning_generics")
TEST_CASE_FIXTURE(Fixture, "union_of_functions_mentioning_generic_typepacks") TEST_CASE_FIXTURE(Fixture, "union_of_functions_mentioning_generic_typepacks")
{ {
ScopedFastFlag sff{"LuauIndentTypeMismatch", true};
ScopedFastInt sfi{"LuauIndentTypeMismatchMaxTypeLength", 10};
CheckResult result = check(R"( CheckResult result = check(R"(
function f<a...>() function f<a...>()
local x : (number, a...) -> (number?, a...) local x : (number, a...) -> (number?, a...)
@ -615,12 +631,17 @@ TEST_CASE_FIXTURE(Fixture, "union_of_functions_mentioning_generic_typepacks")
)"); )");
LUAU_REQUIRE_ERROR_COUNT(1, result); LUAU_REQUIRE_ERROR_COUNT(1, result);
CHECK_EQ(toString(result.errors[0]), "Type '(number, a...) -> (number?, a...)' could not be converted into '((number) -> number) | ((number?, " const std::string expected = R"(Type
"a...) -> (number?, a...))'; none of the union options are compatible"); '(number, a...) -> (number?, a...)'
could not be converted into
'((number) -> number) | ((number?, a...) -> (number?, a...))'; none of the union options are compatible)";
CHECK_EQ(expected, toString(result.errors[0]));
} }
TEST_CASE_FIXTURE(Fixture, "union_of_functions_with_mismatching_arg_arities") TEST_CASE_FIXTURE(Fixture, "union_of_functions_with_mismatching_arg_arities")
{ {
ScopedFastFlag sff{"LuauIndentTypeMismatch", true};
ScopedFastInt sfi{"LuauIndentTypeMismatchMaxTypeLength", 10};
CheckResult result = check(R"( CheckResult result = check(R"(
local x : (number) -> number? local x : (number) -> number?
local y : ((number?) -> number) | ((number | string) -> nil) = x -- OK local y : ((number?) -> number) | ((number | string) -> nil) = x -- OK
@ -628,12 +649,18 @@ TEST_CASE_FIXTURE(Fixture, "union_of_functions_with_mismatching_arg_arities")
)"); )");
LUAU_REQUIRE_ERROR_COUNT(1, result); LUAU_REQUIRE_ERROR_COUNT(1, result);
CHECK_EQ(toString(result.errors[0]), "Type '(number) -> number?' could not be converted into '((number) -> nil) | ((number, string?) -> " const std::string expected = R"(Type
"number)'; none of the union options are compatible"); '(number) -> number?'
could not be converted into
'((number) -> nil) | ((number, string?) -> number)'; none of the union options are compatible)";
CHECK_EQ(expected, toString(result.errors[0]));
} }
TEST_CASE_FIXTURE(Fixture, "union_of_functions_with_mismatching_result_arities") TEST_CASE_FIXTURE(Fixture, "union_of_functions_with_mismatching_result_arities")
{ {
ScopedFastFlag sff{"LuauIndentTypeMismatch", true};
ScopedFastInt sfi{"LuauIndentTypeMismatchMaxTypeLength", 10};
CheckResult result = check(R"( CheckResult result = check(R"(
local x : () -> (number | string) local x : () -> (number | string)
local y : (() -> number) | (() -> string) = x -- OK local y : (() -> number) | (() -> string) = x -- OK
@ -641,12 +668,18 @@ TEST_CASE_FIXTURE(Fixture, "union_of_functions_with_mismatching_result_arities")
)"); )");
LUAU_REQUIRE_ERROR_COUNT(1, result); LUAU_REQUIRE_ERROR_COUNT(1, result);
CHECK_EQ(toString(result.errors[0]), "Type '() -> number | string' could not be converted into '(() -> (string, string)) | (() -> number)'; none " const std::string expected = R"(Type
"of the union options are compatible"); '() -> number | string'
could not be converted into
'(() -> (string, string)) | (() -> number)'; none of the union options are compatible)";
CHECK_EQ(expected, toString(result.errors[0]));
} }
TEST_CASE_FIXTURE(Fixture, "union_of_functions_with_variadics") TEST_CASE_FIXTURE(Fixture, "union_of_functions_with_variadics")
{ {
ScopedFastFlag sff{"LuauIndentTypeMismatch", true};
ScopedFastInt sfi{"LuauIndentTypeMismatchMaxTypeLength", 10};
CheckResult result = check(R"( CheckResult result = check(R"(
local x : (...nil) -> (...number?) local x : (...nil) -> (...number?)
local y : ((...string?) -> (...number)) | ((...number?) -> nil) = x -- OK local y : ((...string?) -> (...number)) | ((...number?) -> nil) = x -- OK
@ -654,12 +687,18 @@ TEST_CASE_FIXTURE(Fixture, "union_of_functions_with_variadics")
)"); )");
LUAU_REQUIRE_ERROR_COUNT(1, result); LUAU_REQUIRE_ERROR_COUNT(1, result);
CHECK_EQ(toString(result.errors[0]), "Type '(...nil) -> (...number?)' could not be converted into '((...string?) -> (...number)) | ((...string?) " const std::string expected = R"(Type
"-> nil)'; none of the union options are compatible"); '(...nil) -> (...number?)'
could not be converted into
'((...string?) -> (...number)) | ((...string?) -> nil)'; none of the union options are compatible)";
CHECK_EQ(expected, toString(result.errors[0]));
} }
TEST_CASE_FIXTURE(Fixture, "union_of_functions_with_mismatching_arg_variadics") TEST_CASE_FIXTURE(Fixture, "union_of_functions_with_mismatching_arg_variadics")
{ {
ScopedFastFlag sff{"LuauIndentTypeMismatch", true};
ScopedFastInt sfi{"LuauIndentTypeMismatchMaxTypeLength", 10};
CheckResult result = check(R"( CheckResult result = check(R"(
local x : (number) -> () local x : (number) -> ()
local y : ((number?) -> ()) | ((...number) -> ()) = x -- OK local y : ((number?) -> ()) | ((...number) -> ()) = x -- OK
@ -667,12 +706,18 @@ TEST_CASE_FIXTURE(Fixture, "union_of_functions_with_mismatching_arg_variadics")
)"); )");
LUAU_REQUIRE_ERROR_COUNT(1, result); LUAU_REQUIRE_ERROR_COUNT(1, result);
CHECK_EQ(toString(result.errors[0]), const std::string expected = R"(Type
"Type '(number) -> ()' could not be converted into '((...number?) -> ()) | ((number?) -> ())'; none of the union options are compatible"); '(number) -> ()'
could not be converted into
'((...number?) -> ()) | ((number?) -> ())'; none of the union options are compatible)";
CHECK_EQ(expected, toString(result.errors[0]));
} }
TEST_CASE_FIXTURE(Fixture, "union_of_functions_with_mismatching_result_variadics") TEST_CASE_FIXTURE(Fixture, "union_of_functions_with_mismatching_result_variadics")
{ {
ScopedFastFlag sff{"LuauIndentTypeMismatch", true};
ScopedFastInt sfi{"LuauIndentTypeMismatchMaxTypeLength", 10};
CheckResult result = check(R"( CheckResult result = check(R"(
local x : () -> (number?, ...number) local x : () -> (number?, ...number)
local y : (() -> (...number)) | (() -> nil) = x -- OK local y : (() -> (...number)) | (() -> nil) = x -- OK
@ -680,8 +725,11 @@ TEST_CASE_FIXTURE(Fixture, "union_of_functions_with_mismatching_result_variadics
)"); )");
LUAU_REQUIRE_ERROR_COUNT(1, result); LUAU_REQUIRE_ERROR_COUNT(1, result);
CHECK_EQ(toString(result.errors[0]), "Type '() -> (number?, ...number)' could not be converted into '(() -> (...number)) | (() -> number)'; none " const std::string expected = R"(Type
"of the union options are compatible"); '() -> (number?, ...number)'
could not be converted into
'(() -> (...number)) | (() -> number)'; none of the union options are compatible)";
CHECK_EQ(expected, toString(result.errors[0]));
} }
TEST_CASE_FIXTURE(Fixture, "less_greedy_unification_with_union_types") TEST_CASE_FIXTURE(Fixture, "less_greedy_unification_with_union_types")

View file

@ -14,6 +14,17 @@ assert((function(x, y)
return c, b, t, t1, t2 return c, b, t, t1, t2
end)(5, 10) == 50) end)(5, 10) == 50)
assert((function(x)
local oops -- split to prevent inlining
function oops()
end
-- x is checked to be a number here; we can not execute a reentry from oops() because optimizer assumes this holds until return
local y = math.abs(x)
oops()
return y * x
end)("42") == 1764)
local function fuzzfail1(...) local function fuzzfail1(...)
repeat repeat
_ = nil _ = nil

View file

@ -68,5 +68,16 @@ ecall(checkuserdata, 2)
call(checkvector, vector(1, 2, 3)) call(checkvector, vector(1, 2, 3))
ecall(checkvector, 2) ecall(checkvector, 2)
local function mutation_causes_bad_exit(a: number, count: number, sum: number)
repeat
a = 's'
sum += count
pcall(function() end)
count -= 1
until count == 0
return sum
end
assert(call(mutation_causes_bad_exit, 5, 10, 0) == 55)
return('OK') return('OK')

View file

@ -161,8 +161,10 @@ struct BoostLikeReporter : doctest::IReporter
} }
void log_message(const doctest::MessageData& md) override void log_message(const doctest::MessageData& md) override
{ // {
printf("%s(%d): ERROR: %s\n", md.m_file, md.m_line, md.m_string.c_str()); const char* severity = (md.m_severity & doctest::assertType::is_warn) ? "WARNING" : "ERROR";
printf("%s(%d): %s: %s\n", md.m_file, md.m_line, severity, md.m_string.c_str());
} }
// called when a test case is skipped either because it doesn't pass the filters, has a skip decorator // called when a test case is skipped either because it doesn't pass the filters, has a skip decorator