Sync to upstream/release/548 (#699)

- Fix rare type checking bugs with invalid generic types escaping the
module scope
- Fix type checking of variadic type packs in certain cases
- Implement type normalization, which resolves a large set of various
issues with unions/intersections in type checker
- Improve parse errors for trailing commas in function calls and type
lists
- Reduce profiling skew when using --profile with very high frequencies
- Improve performance of `lua_getinfo` (`debug.info`, `debug.traceback`
and profiling overhead are now 20% faster/smaller)
- Improve performance of polymorphic comparisons (1-2% lift on some
benchmarks)
- Improve performance of closure creation (1-2% lift on some benchmarks)
- Improve string comparison performance (4% lift on string sorting)
This commit is contained in:
Arseny Kapoulkine 2022-10-06 17:23:29 -07:00 committed by GitHub
parent cc26ef16df
commit d5a2a1585e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
65 changed files with 4001 additions and 481 deletions

View file

@ -7,6 +7,7 @@
#include "Luau/Constraint.h" #include "Luau/Constraint.h"
#include "Luau/TypeVar.h" #include "Luau/TypeVar.h"
#include "Luau/ToString.h" #include "Luau/ToString.h"
#include "Luau/Normalize.h"
#include <vector> #include <vector>
@ -44,6 +45,7 @@ struct ConstraintSolver
TypeArena* arena; TypeArena* arena;
NotNull<SingletonTypes> singletonTypes; NotNull<SingletonTypes> singletonTypes;
InternalErrorReporter iceReporter; InternalErrorReporter iceReporter;
NotNull<Normalizer> normalizer;
// The entire set of constraints that the solver is trying to resolve. // The entire set of constraints that the solver is trying to resolve.
std::vector<NotNull<Constraint>> constraints; std::vector<NotNull<Constraint>> constraints;
NotNull<Scope> rootScope; NotNull<Scope> rootScope;
@ -74,9 +76,12 @@ struct ConstraintSolver
DcrLogger* logger; DcrLogger* logger;
explicit ConstraintSolver(TypeArena* arena, NotNull<SingletonTypes> singletonTypes, NotNull<Scope> rootScope, ModuleName moduleName, explicit ConstraintSolver(NotNull<Normalizer> normalizer, NotNull<Scope> rootScope, ModuleName moduleName,
NotNull<ModuleResolver> moduleResolver, std::vector<RequireCycle> requireCycles, DcrLogger* logger); NotNull<ModuleResolver> moduleResolver, std::vector<RequireCycle> requireCycles, DcrLogger* logger);
// Randomize the order in which to dispatch constraints
void randomize(unsigned seed);
/** /**
* Attempts to dispatch all pending constraints and reach a type solution * Attempts to dispatch all pending constraints and reach a type solution
* that satisfies all of the constraints. * that satisfies all of the constraints.
@ -85,8 +90,9 @@ struct ConstraintSolver
bool done(); bool done();
/** Attempt to dispatch a constraint. Returns true if it was successful. /** Attempt to dispatch a constraint. Returns true if it was successful. If
* If tryDispatch() returns false, the constraint remains in the unsolved set and will be retried later. * tryDispatch() returns false, the constraint remains in the unsolved set
* and will be retried later.
*/ */
bool tryDispatch(NotNull<const Constraint> c, bool force); bool tryDispatch(NotNull<const Constraint> c, bool force);

View file

@ -16,7 +16,7 @@ struct TypeMismatch
TypeMismatch() = default; TypeMismatch() = default;
TypeMismatch(TypeId wantedType, TypeId givenType); TypeMismatch(TypeId wantedType, TypeId givenType);
TypeMismatch(TypeId wantedType, TypeId givenType, std::string reason); TypeMismatch(TypeId wantedType, TypeId givenType, std::string reason);
TypeMismatch(TypeId wantedType, TypeId givenType, std::string reason, TypeError error); TypeMismatch(TypeId wantedType, TypeId givenType, std::string reason, std::optional<TypeError> error);
TypeId wantedType = nullptr; TypeId wantedType = nullptr;
TypeId givenType = nullptr; TypeId givenType = nullptr;

View file

@ -83,8 +83,13 @@ struct FrontendOptions
// is complete. // is complete.
bool retainFullTypeGraphs = false; bool retainFullTypeGraphs = false;
// Run typechecking only in mode required for autocomplete (strict mode in order to get more precise type information) // Run typechecking only in mode required for autocomplete (strict mode in
// order to get more precise type information)
bool forAutocomplete = false; bool forAutocomplete = false;
// If not empty, randomly shuffle the constraint set before attempting to
// solve. Use this value to seed the random number generator.
std::optional<unsigned> randomizeConstraintResolutionSeed;
}; };
struct CheckResult struct CheckResult

View file

@ -1,9 +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
#pragma once #pragma once
#include "Luau/Module.h"
#include "Luau/NotNull.h" #include "Luau/NotNull.h"
#include "Luau/TypeVar.h" #include "Luau/TypeVar.h"
#include "Luau/UnifierSharedState.h"
#include <memory> #include <memory>
@ -29,4 +29,231 @@ std::pair<TypePackId, bool> normalize(
std::pair<TypePackId, bool> normalize(TypePackId ty, NotNull<Module> module, NotNull<SingletonTypes> singletonTypes, InternalErrorReporter& ice); std::pair<TypePackId, bool> normalize(TypePackId ty, NotNull<Module> module, NotNull<SingletonTypes> singletonTypes, InternalErrorReporter& ice);
std::pair<TypePackId, bool> normalize(TypePackId ty, const ModulePtr& module, NotNull<SingletonTypes> singletonTypes, InternalErrorReporter& ice); std::pair<TypePackId, bool> normalize(TypePackId ty, const ModulePtr& module, NotNull<SingletonTypes> singletonTypes, InternalErrorReporter& ice);
class TypeIds
{
private:
std::unordered_set<TypeId> types;
std::vector<TypeId> order;
std::size_t hash = 0;
public:
using iterator = std::vector<TypeId>::iterator;
using const_iterator = std::vector<TypeId>::const_iterator;
TypeIds(const TypeIds&) = delete;
TypeIds(TypeIds&&) = default;
TypeIds() = default;
~TypeIds() = default;
TypeIds& operator=(TypeIds&&) = default;
void insert(TypeId ty);
/// Erase every element that does not also occur in tys
void retain(const TypeIds& tys);
void clear();
iterator begin();
iterator end();
const_iterator begin() const;
const_iterator end() const;
iterator erase(const_iterator it);
size_t size() const;
bool empty() const;
size_t count(TypeId ty) const;
template<class Iterator>
void insert(Iterator begin, Iterator end)
{
for (Iterator it = begin; it != end; ++it)
insert(*it);
}
bool operator ==(const TypeIds& there) const;
size_t getHash() const;
};
} // namespace Luau
template<> struct std::hash<Luau::TypeIds>
{
std::size_t operator()(const Luau::TypeIds& tys) const
{
return tys.getHash();
}
};
template<> struct std::hash<const Luau::TypeIds*>
{
std::size_t operator()(const Luau::TypeIds* tys) const
{
return tys->getHash();
}
};
template<> struct std::equal_to<Luau::TypeIds>
{
bool operator()(const Luau::TypeIds& here, const Luau::TypeIds& there) const
{
return here == there;
}
};
template<> struct std::equal_to<const Luau::TypeIds*>
{
bool operator()(const Luau::TypeIds* here, const Luau::TypeIds* there) const
{
return *here == *there;
}
};
namespace Luau
{
// A normalized string type is either `string` (represented by `nullopt`)
// or a union of string singletons.
using NormalizedStringType = std::optional<std::map<std::string, TypeId>>;
// A normalized function type is either `never` (represented by `nullopt`)
// or an intersection of function types.
// NOTE: type normalization can fail on function types with generics
// (e.g. because we do not support unions and intersections of generic type packs),
// so this type may contain `error`.
using NormalizedFunctionType = std::optional<TypeIds>;
// A normalized generic/free type is a union, where each option is of the form (X & T) where
// * X is either a free type or a generic
// * T is a normalized type.
struct NormalizedType;
using NormalizedTyvars = std::unordered_map<TypeId, std::unique_ptr<NormalizedType>>;
// A normalized type is either any, unknown, or one of the form P | T | F | G where
// * P is a union of primitive types (including singletons, classes and the error type)
// * T is a union of table types
// * F is a union of an intersection of function types
// * G is a union of generic/free normalized types, intersected with a normalized type
struct NormalizedType
{
// The top part of the type.
// This type is either never, unknown, or any.
// If this type is not never, all the other fields are null.
TypeId tops;
// The boolean part of the type.
// This type is either never, boolean type, or a boolean singleton.
TypeId booleans;
// The class part of the type.
// Each element of this set is a class, and none of the classes are subclasses of each other.
TypeIds classes;
// The error part of the type.
// This type is either never or the error type.
TypeId errors;
// The nil part of the type.
// This type is either never or nil.
TypeId nils;
// The number part of the type.
// This type is either never or number.
TypeId numbers;
// The string part of the type.
// This may be the `string` type, or a union of singletons.
NormalizedStringType strings = std::map<std::string,TypeId>{};
// The thread part of the type.
// This type is either never or thread.
TypeId threads;
// The (meta)table part of the type.
// Each element of this set is a (meta)table type.
TypeIds tables;
// The function part of the type.
NormalizedFunctionType functions;
// The generic/free part of the type.
NormalizedTyvars tyvars;
NormalizedType(NotNull<SingletonTypes> singletonTypes);
NormalizedType(const NormalizedType&) = delete;
NormalizedType(NormalizedType&&) = default;
NormalizedType() = delete;
~NormalizedType() = default;
NormalizedType& operator=(NormalizedType&&) = default;
NormalizedType& operator=(NormalizedType&) = delete;
};
class Normalizer
{
std::unordered_map<TypeId, std::unique_ptr<NormalizedType>> cachedNormals;
std::unordered_map<const TypeIds*, TypeId> cachedIntersections;
std::unordered_map<const TypeIds*, TypeId> cachedUnions;
std::unordered_map<const TypeIds*, std::unique_ptr<TypeIds>> cachedTypeIds;
bool withinResourceLimits();
public:
TypeArena* arena;
NotNull<SingletonTypes> singletonTypes;
NotNull<UnifierSharedState> sharedState;
Normalizer(TypeArena* arena, NotNull<SingletonTypes> singletonTypes, NotNull<UnifierSharedState> sharedState);
Normalizer(const Normalizer&) = delete;
Normalizer(Normalizer&&) = delete;
Normalizer() = delete;
~Normalizer() = default;
Normalizer& operator=(Normalizer&&) = delete;
Normalizer& operator=(Normalizer&) = delete;
// If this returns null, the typechecker should emit a "too complex" error
const NormalizedType* normalize(TypeId ty);
void clearNormal(NormalizedType& norm);
// ------- Cached TypeIds
TypeId unionType(TypeId here, TypeId there);
TypeId intersectionType(TypeId here, TypeId there);
const TypeIds* cacheTypeIds(TypeIds tys);
void clearCaches();
// ------- Normalizing unions
void unionTysWithTy(TypeIds& here, TypeId there);
TypeId unionOfTops(TypeId here, TypeId there);
TypeId unionOfBools(TypeId here, TypeId there);
void unionClassesWithClass(TypeIds& heres, TypeId there);
void unionClasses(TypeIds& heres, const TypeIds& theres);
void unionStrings(NormalizedStringType& here, const NormalizedStringType& there);
std::optional<TypePackId> unionOfTypePacks(TypePackId here, TypePackId there);
std::optional<TypeId> unionOfFunctions(TypeId here, TypeId there);
std::optional<TypeId> unionSaturatedFunctions(TypeId here, TypeId there);
void unionFunctionsWithFunction(NormalizedFunctionType& heress, TypeId there);
void unionFunctions(NormalizedFunctionType& heress, const NormalizedFunctionType& theress);
void unionTablesWithTable(TypeIds& heres, TypeId there);
void unionTables(TypeIds& heres, const TypeIds& theres);
bool unionNormals(NormalizedType& here, const NormalizedType& there, int ignoreSmallerTyvars = -1);
bool unionNormalWithTy(NormalizedType& here, TypeId there, int ignoreSmallerTyvars = -1);
// ------- Normalizing intersections
void intersectTysWithTy(TypeIds& here, TypeId there);
TypeId intersectionOfTops(TypeId here, TypeId there);
TypeId intersectionOfBools(TypeId here, TypeId there);
void intersectClasses(TypeIds& heres, const TypeIds& theres);
void intersectClassesWithClass(TypeIds& heres, TypeId there);
void intersectStrings(NormalizedStringType& here, const NormalizedStringType& there);
std::optional<TypePackId> intersectionOfTypePacks(TypePackId here, TypePackId there);
std::optional<TypeId> intersectionOfTables(TypeId here, TypeId there);
void intersectTablesWithTable(TypeIds& heres, TypeId there);
void intersectTables(TypeIds& heres, const TypeIds& theres);
std::optional<TypeId> intersectionOfFunctions(TypeId here, TypeId there);
void intersectFunctionsWithFunction(NormalizedFunctionType& heress, TypeId there);
void intersectFunctions(NormalizedFunctionType& heress, const NormalizedFunctionType& theress);
bool intersectTyvarsWithTy(NormalizedTyvars& here, TypeId there);
bool intersectNormals(NormalizedType& here, const NormalizedType& there, int ignoreSmallerTyvars = -1);
bool intersectNormalWithTy(NormalizedType& here, TypeId there);
// -------- Convert back from a normalized type to a type
TypeId typeFromNormal(const NormalizedType& norm);
};
} // namespace Luau } // namespace Luau

View file

@ -234,6 +234,8 @@ public:
TypeId anyify(const ScopePtr& scope, TypeId ty, Location location); TypeId anyify(const ScopePtr& scope, TypeId ty, Location location);
TypePackId anyify(const ScopePtr& scope, TypePackId ty, Location location); TypePackId anyify(const ScopePtr& scope, TypePackId ty, Location location);
TypePackId anyifyModuleReturnTypePackGenerics(TypePackId ty);
void reportError(const TypeError& error); void reportError(const TypeError& error);
void reportError(const Location& location, TypeErrorData error); void reportError(const Location& location, TypeErrorData error);
void reportErrors(const ErrorVec& errors); void reportErrors(const ErrorVec& errors);
@ -359,6 +361,7 @@ public:
InternalErrorReporter* iceHandler; InternalErrorReporter* iceHandler;
UnifierSharedState unifierState; UnifierSharedState unifierState;
Normalizer normalizer;
std::vector<RequireCycle> requireCycles; std::vector<RequireCycle> requireCycles;

View file

@ -96,7 +96,7 @@ struct Free
bool forwardedTypeAlias = false; bool forwardedTypeAlias = false;
private: private:
static int nextIndex; static int DEPRECATED_nextIndex;
}; };
template<typename Id> template<typename Id>
@ -127,7 +127,7 @@ struct Generic
bool explicitName = false; bool explicitName = false;
private: private:
static int nextIndex; static int DEPRECATED_nextIndex;
}; };
struct Error struct Error

View file

@ -9,6 +9,7 @@
#include "Luau/TxnLog.h" #include "Luau/TxnLog.h"
#include "Luau/TypeArena.h" #include "Luau/TypeArena.h"
#include "Luau/UnifierSharedState.h" #include "Luau/UnifierSharedState.h"
#include "Normalize.h"
#include <unordered_set> #include <unordered_set>
@ -52,6 +53,7 @@ struct Unifier
{ {
TypeArena* const types; TypeArena* const types;
NotNull<SingletonTypes> singletonTypes; NotNull<SingletonTypes> singletonTypes;
NotNull<Normalizer> normalizer;
Mode mode; Mode mode;
NotNull<Scope> scope; // const Scope maybe NotNull<Scope> scope; // const Scope maybe
@ -60,13 +62,14 @@ struct Unifier
Location location; Location location;
Variance variance = Covariant; Variance variance = Covariant;
bool anyIsTop = false; // If true, we consider any to be a top type. If false, it is a familiar but weird mix of top and bottom all at once. bool anyIsTop = false; // If true, we consider any to be a top type. If false, it is a familiar but weird mix of top and bottom all at once.
bool normalize; // Normalize unions and intersections if necessary
bool useScopes = false; // If true, we use the scope hierarchy rather than TypeLevels bool useScopes = false; // If true, we use the scope hierarchy rather than TypeLevels
CountMismatch::Context ctx = CountMismatch::Arg; CountMismatch::Context ctx = CountMismatch::Arg;
UnifierSharedState& sharedState; UnifierSharedState& sharedState;
Unifier(TypeArena* types, NotNull<SingletonTypes> singletonTypes, Mode mode, NotNull<Scope> scope, const Location& location, Variance variance, Unifier(NotNull<Normalizer> normalizer, Mode mode, NotNull<Scope> scope, const Location& location, Variance variance,
UnifierSharedState& sharedState, TxnLog* parentLog = nullptr); TxnLog* parentLog = nullptr);
// Test whether the two type vars unify. Never commits the result. // Test whether the two type vars unify. Never commits the result.
ErrorVec canUnify(TypeId subTy, TypeId superTy); ErrorVec canUnify(TypeId subTy, TypeId superTy);
@ -84,6 +87,7 @@ private:
void tryUnifyTypeWithUnion(TypeId subTy, TypeId superTy, const UnionTypeVar* uv, bool cacheEnabled, bool isFunctionCall); void tryUnifyTypeWithUnion(TypeId subTy, TypeId superTy, const UnionTypeVar* uv, bool cacheEnabled, bool isFunctionCall);
void tryUnifyTypeWithIntersection(TypeId subTy, TypeId superTy, const IntersectionTypeVar* uv); void tryUnifyTypeWithIntersection(TypeId subTy, TypeId superTy, const IntersectionTypeVar* uv);
void tryUnifyIntersectionWithType(TypeId subTy, const IntersectionTypeVar* uv, TypeId superTy, bool cacheEnabled, bool isFunctionCall); void tryUnifyIntersectionWithType(TypeId subTy, const IntersectionTypeVar* uv, TypeId superTy, bool cacheEnabled, bool isFunctionCall);
void tryUnifyNormalizedTypes(TypeId subTy, TypeId superTy, const NormalizedType& subNorm, const NormalizedType& superNorm, std::string reason, std::optional<TypeError> error = std::nullopt);
void tryUnifyPrimitives(TypeId subTy, TypeId superTy); void tryUnifyPrimitives(TypeId subTy, TypeId superTy);
void tryUnifySingletons(TypeId subTy, TypeId superTy); void tryUnifySingletons(TypeId subTy, TypeId superTy);
void tryUnifyFunctions(TypeId subTy, TypeId superTy, bool isFunctionCall = false); void tryUnifyFunctions(TypeId subTy, TypeId superTy, bool isFunctionCall = false);
@ -92,6 +96,8 @@ private:
void tryUnifyWithMetatable(TypeId subTy, TypeId superTy, bool reversed); void tryUnifyWithMetatable(TypeId subTy, TypeId superTy, bool reversed);
void tryUnifyWithClass(TypeId subTy, TypeId superTy, bool reversed); void tryUnifyWithClass(TypeId subTy, TypeId superTy, bool reversed);
TypePackId tryApplyOverloadedFunction(TypeId function, const NormalizedFunctionType& overloads, TypePackId args);
TypeId widen(TypeId ty); TypeId widen(TypeId ty);
TypePackId widen(TypePackId tp); TypePackId widen(TypePackId tp);

View file

@ -12,8 +12,6 @@
#include <unordered_set> #include <unordered_set>
#include <utility> #include <utility>
LUAU_FASTFLAG(LuauSelfCallAutocompleteFix3)
static const std::unordered_set<std::string> kStatementStartingKeywords = { static const std::unordered_set<std::string> kStatementStartingKeywords = {
"while", "if", "local", "repeat", "function", "do", "for", "return", "break", "continue", "type", "export"}; "while", "if", "local", "repeat", "function", "do", "for", "return", "break", "continue", "type", "export"};
@ -139,7 +137,8 @@ static bool checkTypeMatch(TypeId subTy, TypeId superTy, NotNull<Scope> scope, T
{ {
InternalErrorReporter iceReporter; InternalErrorReporter iceReporter;
UnifierSharedState unifierState(&iceReporter); UnifierSharedState unifierState(&iceReporter);
Unifier unifier(typeArena, singletonTypes, Mode::Strict, scope, Location(), Variance::Covariant, unifierState); Normalizer normalizer{typeArena, singletonTypes, NotNull{&unifierState}};
Unifier unifier(NotNull<Normalizer>{&normalizer}, Mode::Strict, scope, Location(), Variance::Covariant);
return unifier.canUnify(subTy, superTy).empty(); return unifier.canUnify(subTy, superTy).empty();
} }
@ -151,18 +150,6 @@ static TypeCorrectKind checkTypeCorrectKind(
NotNull<Scope> moduleScope{module.getModuleScope().get()}; NotNull<Scope> moduleScope{module.getModuleScope().get()};
auto canUnify = [&typeArena, singletonTypes, moduleScope](TypeId subTy, TypeId superTy) {
LUAU_ASSERT(!FFlag::LuauSelfCallAutocompleteFix3);
InternalErrorReporter iceReporter;
UnifierSharedState unifierState(&iceReporter);
Unifier unifier(typeArena, singletonTypes, Mode::Strict, moduleScope, Location(), Variance::Covariant, unifierState);
unifier.tryUnify(subTy, superTy);
bool ok = unifier.errors.empty();
return ok;
};
auto typeAtPosition = findExpectedTypeAt(module, node, position); auto typeAtPosition = findExpectedTypeAt(module, node, position);
if (!typeAtPosition) if (!typeAtPosition)
@ -170,30 +157,11 @@ static TypeCorrectKind checkTypeCorrectKind(
TypeId expectedType = follow(*typeAtPosition); TypeId expectedType = follow(*typeAtPosition);
auto checkFunctionType = [typeArena, singletonTypes, moduleScope, &canUnify, &expectedType](const FunctionTypeVar* ftv) { auto checkFunctionType = [typeArena, singletonTypes, moduleScope, &expectedType](const FunctionTypeVar* ftv) {
if (FFlag::LuauSelfCallAutocompleteFix3) if (std::optional<TypeId> firstRetTy = first(ftv->retTypes))
{ return checkTypeMatch(*firstRetTy, expectedType, moduleScope, typeArena, singletonTypes);
if (std::optional<TypeId> firstRetTy = first(ftv->retTypes))
return checkTypeMatch(*firstRetTy, expectedType, moduleScope, typeArena, singletonTypes);
return false; return false;
}
else
{
auto [retHead, retTail] = flatten(ftv->retTypes);
if (!retHead.empty() && canUnify(retHead.front(), expectedType))
return true;
// We might only have a variadic tail pack, check if the element is compatible
if (retTail)
{
if (const VariadicTypePack* vtp = get<VariadicTypePack>(follow(*retTail)); vtp && canUnify(vtp->ty, expectedType))
return true;
}
return false;
}
}; };
// We also want to suggest functions that return compatible result // We also want to suggest functions that return compatible result
@ -212,11 +180,8 @@ static TypeCorrectKind checkTypeCorrectKind(
} }
} }
if (FFlag::LuauSelfCallAutocompleteFix3) return checkTypeMatch(ty, expectedType, NotNull{module.getModuleScope().get()}, typeArena, singletonTypes) ? TypeCorrectKind::Correct
return checkTypeMatch(ty, expectedType, NotNull{module.getModuleScope().get()}, typeArena, singletonTypes) ? TypeCorrectKind::Correct : TypeCorrectKind::None;
: TypeCorrectKind::None;
else
return canUnify(ty, expectedType) ? TypeCorrectKind::Correct : TypeCorrectKind::None;
} }
enum class PropIndexType enum class PropIndexType
@ -230,51 +195,14 @@ static void autocompleteProps(const Module& module, TypeArena* typeArena, NotNul
PropIndexType indexType, const std::vector<AstNode*>& nodes, AutocompleteEntryMap& result, std::unordered_set<TypeId>& seen, PropIndexType indexType, const std::vector<AstNode*>& nodes, AutocompleteEntryMap& result, std::unordered_set<TypeId>& seen,
std::optional<const ClassTypeVar*> containingClass = std::nullopt) std::optional<const ClassTypeVar*> containingClass = std::nullopt)
{ {
if (FFlag::LuauSelfCallAutocompleteFix3) rootTy = follow(rootTy);
rootTy = follow(rootTy);
ty = follow(ty); ty = follow(ty);
if (seen.count(ty)) if (seen.count(ty))
return; return;
seen.insert(ty); seen.insert(ty);
auto isWrongIndexer_DEPRECATED = [indexType, useStrictFunctionIndexers = !!get<ClassTypeVar>(ty)](Luau::TypeId type) {
LUAU_ASSERT(!FFlag::LuauSelfCallAutocompleteFix3);
if (indexType == PropIndexType::Key)
return false;
bool colonIndex = indexType == PropIndexType::Colon;
if (const FunctionTypeVar* ftv = get<FunctionTypeVar>(type))
{
return useStrictFunctionIndexers ? colonIndex != ftv->hasSelf : false;
}
else if (const IntersectionTypeVar* itv = get<IntersectionTypeVar>(type))
{
bool allHaveSelf = true;
for (auto subType : itv->parts)
{
if (const FunctionTypeVar* ftv = get<FunctionTypeVar>(Luau::follow(subType)))
{
allHaveSelf &= ftv->hasSelf;
}
else
{
return colonIndex;
}
}
return useStrictFunctionIndexers ? colonIndex != allHaveSelf : false;
}
else
{
return colonIndex;
}
};
auto isWrongIndexer = [typeArena, singletonTypes, &module, rootTy, indexType](Luau::TypeId type) { auto isWrongIndexer = [typeArena, singletonTypes, &module, rootTy, indexType](Luau::TypeId type) {
LUAU_ASSERT(FFlag::LuauSelfCallAutocompleteFix3);
if (indexType == PropIndexType::Key) if (indexType == PropIndexType::Key)
return false; return false;
@ -337,7 +265,7 @@ static void autocompleteProps(const Module& module, TypeArena* typeArena, NotNul
AutocompleteEntryKind::Property, AutocompleteEntryKind::Property,
type, type,
prop.deprecated, prop.deprecated,
FFlag::LuauSelfCallAutocompleteFix3 ? isWrongIndexer(type) : isWrongIndexer_DEPRECATED(type), isWrongIndexer(type),
typeCorrect, typeCorrect,
containingClass, containingClass,
&prop, &prop,
@ -380,31 +308,8 @@ static void autocompleteProps(const Module& module, TypeArena* typeArena, NotNul
{ {
autocompleteProps(module, typeArena, singletonTypes, rootTy, mt->table, indexType, nodes, result, seen); autocompleteProps(module, typeArena, singletonTypes, rootTy, mt->table, indexType, nodes, result, seen);
if (FFlag::LuauSelfCallAutocompleteFix3) if (auto mtable = get<TableTypeVar>(mt->metatable))
{ fillMetatableProps(mtable);
if (auto mtable = get<TableTypeVar>(mt->metatable))
fillMetatableProps(mtable);
}
else
{
auto mtable = get<TableTypeVar>(mt->metatable);
if (!mtable)
return;
auto indexIt = mtable->props.find("__index");
if (indexIt != mtable->props.end())
{
TypeId followed = follow(indexIt->second.type);
if (get<TableTypeVar>(followed) || get<MetatableTypeVar>(followed))
autocompleteProps(module, typeArena, singletonTypes, rootTy, followed, indexType, nodes, result, seen);
else if (auto indexFunction = get<FunctionTypeVar>(followed))
{
std::optional<TypeId> indexFunctionResult = first(indexFunction->retTypes);
if (indexFunctionResult)
autocompleteProps(module, typeArena, singletonTypes, rootTy, *indexFunctionResult, indexType, nodes, result, seen);
}
}
}
} }
else if (auto i = get<IntersectionTypeVar>(ty)) else if (auto i = get<IntersectionTypeVar>(ty))
{ {
@ -446,9 +351,6 @@ static void autocompleteProps(const Module& module, TypeArena* typeArena, NotNul
AutocompleteEntryMap inner; AutocompleteEntryMap inner;
std::unordered_set<TypeId> innerSeen; std::unordered_set<TypeId> innerSeen;
if (!FFlag::LuauSelfCallAutocompleteFix3)
innerSeen = seen;
if (isNil(*iter)) if (isNil(*iter))
{ {
++iter; ++iter;
@ -472,7 +374,7 @@ static void autocompleteProps(const Module& module, TypeArena* typeArena, NotNul
++iter; ++iter;
} }
} }
else if (auto pt = get<PrimitiveTypeVar>(ty); pt && FFlag::LuauSelfCallAutocompleteFix3) else if (auto pt = get<PrimitiveTypeVar>(ty))
{ {
if (pt->metatable) if (pt->metatable)
{ {
@ -480,7 +382,7 @@ static void autocompleteProps(const Module& module, TypeArena* typeArena, NotNul
fillMetatableProps(mtable); fillMetatableProps(mtable);
} }
} }
else if (FFlag::LuauSelfCallAutocompleteFix3 && get<StringSingleton>(get<SingletonTypeVar>(ty))) else if (get<StringSingleton>(get<SingletonTypeVar>(ty)))
{ {
autocompleteProps(module, typeArena, singletonTypes, rootTy, singletonTypes->stringType, indexType, nodes, result, seen); autocompleteProps(module, typeArena, singletonTypes, rootTy, singletonTypes->stringType, indexType, nodes, result, seen);
} }
@ -1416,11 +1318,7 @@ static AutocompleteResult autocomplete(const SourceModule& sourceModule, const M
TypeId ty = follow(*it); TypeId ty = follow(*it);
PropIndexType indexType = indexName->op == ':' ? PropIndexType::Colon : PropIndexType::Point; PropIndexType indexType = indexName->op == ':' ? PropIndexType::Colon : PropIndexType::Point;
if (!FFlag::LuauSelfCallAutocompleteFix3 && isString(ty)) return {autocompleteProps(*module, &typeArena, singletonTypes, ty, indexType, ancestry), ancestry, AutocompleteContext::Property};
return {autocompleteProps(*module, &typeArena, singletonTypes, globalScope->bindings[AstName{"string"}].typeId, indexType, ancestry),
ancestry, AutocompleteContext::Property};
else
return {autocompleteProps(*module, &typeArena, singletonTypes, ty, indexType, ancestry), ancestry, AutocompleteContext::Property};
} }
else if (auto typeReference = node->as<AstTypeReference>()) else if (auto typeReference = node->as<AstTypeReference>())
{ {

View file

@ -14,6 +14,8 @@
#include "Luau/VisitTypeVar.h" #include "Luau/VisitTypeVar.h"
#include "Luau/TypeUtils.h" #include "Luau/TypeUtils.h"
#include <random>
LUAU_FASTFLAGVARIABLE(DebugLuauLogSolver, false); LUAU_FASTFLAGVARIABLE(DebugLuauLogSolver, false);
LUAU_FASTFLAGVARIABLE(DebugLuauLogSolverToJson, false); LUAU_FASTFLAGVARIABLE(DebugLuauLogSolverToJson, false);
LUAU_FASTFLAG(LuauFixNameMaps) LUAU_FASTFLAG(LuauFixNameMaps)
@ -251,10 +253,11 @@ void dump(ConstraintSolver* cs, ToStringOptions& opts)
} }
} }
ConstraintSolver::ConstraintSolver(TypeArena* arena, NotNull<SingletonTypes> singletonTypes, NotNull<Scope> rootScope, ModuleName moduleName, ConstraintSolver::ConstraintSolver(NotNull<Normalizer> normalizer, NotNull<Scope> rootScope, ModuleName moduleName,
NotNull<ModuleResolver> moduleResolver, std::vector<RequireCycle> requireCycles, DcrLogger* logger) NotNull<ModuleResolver> moduleResolver, std::vector<RequireCycle> requireCycles, DcrLogger* logger)
: arena(arena) : arena(normalizer->arena)
, singletonTypes(singletonTypes) , singletonTypes(normalizer->singletonTypes)
, normalizer(normalizer)
, constraints(collectConstraints(rootScope)) , constraints(collectConstraints(rootScope))
, rootScope(rootScope) , rootScope(rootScope)
, currentModuleName(std::move(moduleName)) , currentModuleName(std::move(moduleName))
@ -278,6 +281,12 @@ ConstraintSolver::ConstraintSolver(TypeArena* arena, NotNull<SingletonTypes> sin
LUAU_ASSERT(logger); LUAU_ASSERT(logger);
} }
void ConstraintSolver::randomize(unsigned seed)
{
std::mt19937 g(seed);
std::shuffle(begin(unsolvedConstraints), end(unsolvedConstraints), g);
}
void ConstraintSolver::run() void ConstraintSolver::run()
{ {
if (done()) if (done())
@ -1355,8 +1364,7 @@ bool ConstraintSolver::isBlocked(NotNull<const Constraint> constraint)
void ConstraintSolver::unify(TypeId subType, TypeId superType, NotNull<Scope> scope) void ConstraintSolver::unify(TypeId subType, TypeId superType, NotNull<Scope> scope)
{ {
UnifierSharedState sharedState{&iceReporter}; Unifier u{normalizer, Mode::Strict, scope, Location{}, Covariant};
Unifier u{arena, singletonTypes, Mode::Strict, scope, Location{}, Covariant, sharedState};
u.useScopes = true; u.useScopes = true;
u.tryUnify(subType, superType); u.tryUnify(subType, superType);
@ -1379,7 +1387,7 @@ void ConstraintSolver::unify(TypeId subType, TypeId superType, NotNull<Scope> sc
void ConstraintSolver::unify(TypePackId subPack, TypePackId superPack, NotNull<Scope> scope) void ConstraintSolver::unify(TypePackId subPack, TypePackId superPack, NotNull<Scope> scope)
{ {
UnifierSharedState sharedState{&iceReporter}; UnifierSharedState sharedState{&iceReporter};
Unifier u{arena, singletonTypes, Mode::Strict, scope, Location{}, Covariant, sharedState}; Unifier u{normalizer, Mode::Strict, scope, Location{}, Covariant};
u.useScopes = true; u.useScopes = true;
u.tryUnify(subPack, superPack); u.tryUnify(subPack, superPack);

View file

@ -511,11 +511,11 @@ TypeMismatch::TypeMismatch(TypeId wantedType, TypeId givenType, std::string reas
{ {
} }
TypeMismatch::TypeMismatch(TypeId wantedType, TypeId givenType, std::string reason, TypeError error) TypeMismatch::TypeMismatch(TypeId wantedType, TypeId givenType, std::string reason, std::optional<TypeError> error)
: wantedType(wantedType) : wantedType(wantedType)
, givenType(givenType) , givenType(givenType)
, reason(reason) , reason(reason)
, error(std::make_shared<TypeError>(std::move(error))) , error(error ? std::make_shared<TypeError>(std::move(*error)) : nullptr)
{ {
} }

View file

@ -860,12 +860,18 @@ ModulePtr Frontend::check(
const NotNull<ModuleResolver> mr{forAutocomplete ? &moduleResolverForAutocomplete : &moduleResolver}; const NotNull<ModuleResolver> mr{forAutocomplete ? &moduleResolverForAutocomplete : &moduleResolver};
const ScopePtr& globalScope{forAutocomplete ? typeCheckerForAutocomplete.globalScope : typeChecker.globalScope}; const ScopePtr& globalScope{forAutocomplete ? typeCheckerForAutocomplete.globalScope : typeChecker.globalScope};
Normalizer normalizer{&result->internalTypes, singletonTypes, NotNull{&typeChecker.unifierState}};
ConstraintGraphBuilder cgb{ ConstraintGraphBuilder cgb{
sourceModule.name, result, &result->internalTypes, mr, singletonTypes, NotNull(&iceHandler), globalScope, logger.get()}; sourceModule.name, result, &result->internalTypes, mr, singletonTypes, NotNull(&iceHandler), globalScope, logger.get()};
cgb.visit(sourceModule.root); cgb.visit(sourceModule.root);
result->errors = std::move(cgb.errors); result->errors = std::move(cgb.errors);
ConstraintSolver cs{&result->internalTypes, singletonTypes, NotNull(cgb.rootScope), sourceModule.name, NotNull(&moduleResolver), requireCycles, logger.get()}; ConstraintSolver cs{NotNull{&normalizer}, NotNull(cgb.rootScope), sourceModule.name, NotNull(&moduleResolver), requireCycles, logger.get()};
if (options.randomizeConstraintResolutionSeed)
cs.randomize(*options.randomizeConstraintResolutionSeed);
cs.run(); cs.run();
for (TypeError& e : cs.errors) for (TypeError& e : cs.errors)

View file

@ -14,6 +14,7 @@
#include <algorithm> #include <algorithm>
LUAU_FASTFLAG(LuauAnyifyModuleReturnGenerics)
LUAU_FASTFLAG(LuauLowerBoundsCalculation); LUAU_FASTFLAG(LuauLowerBoundsCalculation);
LUAU_FASTFLAG(DebugLuauDeferredConstraintResolution); LUAU_FASTFLAG(DebugLuauDeferredConstraintResolution);
LUAU_FASTFLAGVARIABLE(LuauForceExportSurfacesToBeNormal, false); LUAU_FASTFLAGVARIABLE(LuauForceExportSurfacesToBeNormal, false);
@ -285,13 +286,16 @@ void Module::clonePublicInterface(NotNull<SingletonTypes> singletonTypes, Intern
} }
} }
for (TypeId ty : returnType) if (!FFlag::LuauAnyifyModuleReturnGenerics)
{ {
if (get<GenericTypeVar>(follow(ty))) for (TypeId ty : returnType)
{ {
auto t = asMutable(ty); if (get<GenericTypeVar>(follow(ty)))
t->ty = AnyTypeVar{}; {
t->normal = true; auto t = asMutable(ty);
t->ty = AnyTypeVar{};
t->normal = true;
}
} }
} }

File diff suppressed because it is too large Load diff

View file

@ -280,7 +280,8 @@ struct TypeChecker2
TypePackId actualRetType = reconstructPack(ret->list, arena); TypePackId actualRetType = reconstructPack(ret->list, arena);
UnifierSharedState sharedState{&ice}; UnifierSharedState sharedState{&ice};
Unifier u{&arena, singletonTypes, Mode::Strict, stack.back(), ret->location, Covariant, sharedState}; Normalizer normalizer{&arena, singletonTypes, NotNull{&sharedState}};
Unifier u{NotNull{&normalizer}, Mode::Strict, stack.back(), ret->location, Covariant};
u.anyIsTop = true; u.anyIsTop = true;
u.tryUnify(actualRetType, expectedRetType); u.tryUnify(actualRetType, expectedRetType);
@ -1206,7 +1207,8 @@ struct TypeChecker2
ErrorVec tryUnify(NotNull<Scope> scope, const Location& location, TID subTy, TID superTy) ErrorVec tryUnify(NotNull<Scope> scope, const Location& location, TID subTy, TID superTy)
{ {
UnifierSharedState sharedState{&ice}; UnifierSharedState sharedState{&ice};
Unifier u{&module->internalTypes, singletonTypes, Mode::Strict, scope, location, Covariant, sharedState}; Normalizer normalizer{&module->internalTypes, singletonTypes, NotNull{&sharedState}};
Unifier u{NotNull{&normalizer}, Mode::Strict, scope, location, Covariant};
u.anyIsTop = true; u.anyIsTop = true;
u.tryUnify(subTy, superTy); u.tryUnify(subTy, superTy);

View file

@ -32,19 +32,21 @@ LUAU_FASTINTVARIABLE(LuauCheckRecursionLimit, 300)
LUAU_FASTINTVARIABLE(LuauVisitRecursionLimit, 500) LUAU_FASTINTVARIABLE(LuauVisitRecursionLimit, 500)
LUAU_FASTFLAG(LuauKnowsTheDataModel3) LUAU_FASTFLAG(LuauKnowsTheDataModel3)
LUAU_FASTFLAG(LuauAutocompleteDynamicLimits) LUAU_FASTFLAG(LuauAutocompleteDynamicLimits)
LUAU_FASTFLAG(LuauTypeNormalization2)
LUAU_FASTFLAGVARIABLE(LuauFunctionArgMismatchDetails, false) LUAU_FASTFLAGVARIABLE(LuauFunctionArgMismatchDetails, false)
LUAU_FASTFLAGVARIABLE(LuauInplaceDemoteSkipAllBound, false)
LUAU_FASTFLAGVARIABLE(LuauLowerBoundsCalculation, false) LUAU_FASTFLAGVARIABLE(LuauLowerBoundsCalculation, false)
LUAU_FASTFLAGVARIABLE(DebugLuauFreezeDuringUnification, false) LUAU_FASTFLAGVARIABLE(DebugLuauFreezeDuringUnification, false)
LUAU_FASTFLAGVARIABLE(LuauSelfCallAutocompleteFix3, false)
LUAU_FASTFLAGVARIABLE(LuauReturnAnyInsteadOfICE, false) // Eventually removed as false. LUAU_FASTFLAGVARIABLE(LuauReturnAnyInsteadOfICE, false) // Eventually removed as false.
LUAU_FASTFLAGVARIABLE(DebugLuauSharedSelf, false) LUAU_FASTFLAGVARIABLE(DebugLuauSharedSelf, false)
LUAU_FASTFLAGVARIABLE(LuauAnyifyModuleReturnGenerics, false)
LUAU_FASTFLAGVARIABLE(LuauUnknownAndNeverType, false) LUAU_FASTFLAGVARIABLE(LuauUnknownAndNeverType, false)
LUAU_FASTFLAGVARIABLE(LuauCallUnifyPackTails, false) LUAU_FASTFLAGVARIABLE(LuauCallUnifyPackTails, false)
LUAU_FASTFLAGVARIABLE(LuauCheckGenericHOFTypes, false) LUAU_FASTFLAGVARIABLE(LuauCheckGenericHOFTypes, false)
LUAU_FASTFLAGVARIABLE(LuauBinaryNeedsExpectedTypesToo, false) LUAU_FASTFLAGVARIABLE(LuauBinaryNeedsExpectedTypesToo, false)
LUAU_FASTFLAGVARIABLE(LuauFixVarargExprHeadType, false)
LUAU_FASTFLAGVARIABLE(LuauNeverTypesAndOperatorsInference, false) LUAU_FASTFLAGVARIABLE(LuauNeverTypesAndOperatorsInference, false)
LUAU_FASTFLAGVARIABLE(LuauReturnsFromCallsitesAreNotWidened, false) LUAU_FASTFLAGVARIABLE(LuauReturnsFromCallsitesAreNotWidened, false)
LUAU_FASTFLAG(LuauInstantiateInSubtyping)
LUAU_FASTFLAGVARIABLE(LuauCompleteVisitor, false) LUAU_FASTFLAGVARIABLE(LuauCompleteVisitor, false)
LUAU_FASTFLAGVARIABLE(LuauUnionOfTypesFollow, false) LUAU_FASTFLAGVARIABLE(LuauUnionOfTypesFollow, false)
LUAU_FASTFLAGVARIABLE(LuauReportShadowedTypeAlias, false) LUAU_FASTFLAGVARIABLE(LuauReportShadowedTypeAlias, false)
@ -255,6 +257,7 @@ TypeChecker::TypeChecker(ModuleResolver* resolver, NotNull<SingletonTypes> singl
, singletonTypes(singletonTypes) , singletonTypes(singletonTypes)
, iceHandler(iceHandler) , iceHandler(iceHandler)
, unifierState(iceHandler) , unifierState(iceHandler)
, normalizer(nullptr, singletonTypes, NotNull{&unifierState})
, nilType(singletonTypes->nilType) , nilType(singletonTypes->nilType)
, numberType(singletonTypes->numberType) , numberType(singletonTypes->numberType)
, stringType(singletonTypes->stringType) , stringType(singletonTypes->stringType)
@ -301,12 +304,13 @@ ModulePtr TypeChecker::checkWithoutRecursionCheck(const SourceModule& module, Mo
LUAU_TIMETRACE_SCOPE("TypeChecker::check", "TypeChecker"); LUAU_TIMETRACE_SCOPE("TypeChecker::check", "TypeChecker");
LUAU_TIMETRACE_ARGUMENT("module", module.name.c_str()); LUAU_TIMETRACE_ARGUMENT("module", module.name.c_str());
currentModule.reset(new Module()); currentModule.reset(new Module);
currentModule->type = module.type; currentModule->type = module.type;
currentModule->allocator = module.allocator; currentModule->allocator = module.allocator;
currentModule->names = module.names; currentModule->names = module.names;
iceHandler->moduleName = module.name; iceHandler->moduleName = module.name;
normalizer.arena = &currentModule->internalTypes;
if (FFlag::LuauAutocompleteDynamicLimits) if (FFlag::LuauAutocompleteDynamicLimits)
{ {
@ -351,15 +355,23 @@ ModulePtr TypeChecker::checkWithoutRecursionCheck(const SourceModule& module, Mo
if (get<FreeTypePack>(follow(moduleScope->returnType))) if (get<FreeTypePack>(follow(moduleScope->returnType)))
moduleScope->returnType = addTypePack(TypePack{{}, std::nullopt}); moduleScope->returnType = addTypePack(TypePack{{}, std::nullopt});
else else
{
moduleScope->returnType = anyify(moduleScope, moduleScope->returnType, Location{}); moduleScope->returnType = anyify(moduleScope, moduleScope->returnType, Location{});
}
if (FFlag::LuauAnyifyModuleReturnGenerics)
moduleScope->returnType = anyifyModuleReturnTypePackGenerics(moduleScope->returnType);
for (auto& [_, typeFun] : moduleScope->exportedTypeBindings) for (auto& [_, typeFun] : moduleScope->exportedTypeBindings)
typeFun.type = anyify(moduleScope, typeFun.type, Location{}); typeFun.type = anyify(moduleScope, typeFun.type, Location{});
prepareErrorsForDisplay(currentModule->errors); prepareErrorsForDisplay(currentModule->errors);
if (FFlag::LuauTypeNormalization2)
{
// Clear the normalizer caches, since they contain types from the internal type surface
normalizer.clearCaches();
normalizer.arena = nullptr;
}
currentModule->clonePublicInterface(singletonTypes, *iceHandler); currentModule->clonePublicInterface(singletonTypes, *iceHandler);
// Clear unifier cache since it's keyed off internal types that get deallocated // Clear unifier cache since it's keyed off internal types that get deallocated
@ -474,7 +486,7 @@ struct InplaceDemoter : TypeVarOnceVisitor
TypeArena* arena; TypeArena* arena;
InplaceDemoter(TypeLevel level, TypeArena* arena) InplaceDemoter(TypeLevel level, TypeArena* arena)
: TypeVarOnceVisitor(/* skipBoundTypes= */ FFlag::LuauInplaceDemoteSkipAllBound) : TypeVarOnceVisitor(/* skipBoundTypes= */ true)
, newLevel(level) , newLevel(level)
, arena(arena) , arena(arena)
{ {
@ -494,12 +506,6 @@ struct InplaceDemoter : TypeVarOnceVisitor
return false; return false;
} }
bool visit(TypeId ty, const BoundTypeVar& btyRef) override
{
LUAU_ASSERT(!FFlag::LuauInplaceDemoteSkipAllBound);
return true;
}
bool visit(TypeId ty) override bool visit(TypeId ty) override
{ {
if (ty->owningArena != arena) if (ty->owningArena != arena)
@ -1029,8 +1035,11 @@ void TypeChecker::check(const ScopePtr& scope, const AstStatAssign& assign)
if (right) if (right)
{ {
if (!maybeGeneric(left) && isGeneric(right)) if (!FFlag::LuauInstantiateInSubtyping)
right = instantiate(scope, right, loc); {
if (!maybeGeneric(left) && isGeneric(right))
right = instantiate(scope, right, loc);
}
// Setting a table entry to nil doesn't mean nil is the type of the indexer, it is just deleting the entry // Setting a table entry to nil doesn't mean nil is the type of the indexer, it is just deleting the entry
const TableTypeVar* destTableTypeReceivingNil = nullptr; const TableTypeVar* destTableTypeReceivingNil = nullptr;
@ -1104,7 +1113,9 @@ void TypeChecker::check(const ScopePtr& scope, const AstStatLocal& local)
variableTypes.push_back(ty); variableTypes.push_back(ty);
expectedTypes.push_back(ty); expectedTypes.push_back(ty);
instantiateGenerics.push_back(annotation != nullptr && !maybeGeneric(ty)); // with FFlag::LuauInstantiateInSubtyping enabled, we shouldn't need to produce instantiateGenerics at all.
if (!FFlag::LuauInstantiateInSubtyping)
instantiateGenerics.push_back(annotation != nullptr && !maybeGeneric(ty));
} }
if (local.values.size > 0) if (local.values.size > 0)
@ -1729,9 +1740,7 @@ void TypeChecker::check(const ScopePtr& scope, const AstStatDeclareClass& declar
{ {
ftv->argNames.insert(ftv->argNames.begin(), FunctionArgument{"self", {}}); ftv->argNames.insert(ftv->argNames.begin(), FunctionArgument{"self", {}});
ftv->argTypes = addTypePack(TypePack{{classTy}, ftv->argTypes}); ftv->argTypes = addTypePack(TypePack{{classTy}, ftv->argTypes});
ftv->hasSelf = true;
if (FFlag::LuauSelfCallAutocompleteFix3)
ftv->hasSelf = true;
} }
} }
@ -1905,8 +1914,18 @@ WithPredicate<TypeId> TypeChecker::checkExpr(const ScopePtr& scope, const AstExp
if (get<TypePack>(varargPack)) if (get<TypePack>(varargPack))
{ {
std::vector<TypeId> types = flatten(varargPack).first; if (FFlag::LuauFixVarargExprHeadType)
return {!types.empty() ? types[0] : nilType}; {
if (std::optional<TypeId> ty = first(varargPack))
return {*ty};
return {nilType};
}
else
{
std::vector<TypeId> types = flatten(varargPack).first;
return {!types.empty() ? types[0] : nilType};
}
} }
else if (get<FreeTypePack>(varargPack)) else if (get<FreeTypePack>(varargPack))
{ {
@ -3967,7 +3986,10 @@ void TypeChecker::checkArgumentList(const ScopePtr& scope, const AstExpr& funNam
} }
else else
{ {
unifyWithInstantiationIfNeeded(*argIter, *paramIter, scope, state); if (FFlag::LuauInstantiateInSubtyping)
state.tryUnify(*argIter, *paramIter, /*isFunctionCall*/ false);
else
unifyWithInstantiationIfNeeded(*argIter, *paramIter, scope, state);
++argIter; ++argIter;
++paramIter; ++paramIter;
} }
@ -4523,8 +4545,11 @@ WithPredicate<TypePackId> TypeChecker::checkExprList(const ScopePtr& scope, cons
TypeId actualType = substituteFreeForNil && expr->is<AstExprConstantNil>() ? freshType(scope) : type; TypeId actualType = substituteFreeForNil && expr->is<AstExprConstantNil>() ? freshType(scope) : type;
if (instantiateGenerics.size() > i && instantiateGenerics[i]) if (!FFlag::LuauInstantiateInSubtyping)
actualType = instantiate(scope, actualType, expr->location); {
if (instantiateGenerics.size() > i && instantiateGenerics[i])
actualType = instantiate(scope, actualType, expr->location);
}
if (expectedType) if (expectedType)
{ {
@ -4686,6 +4711,8 @@ bool TypeChecker::unifyWithInstantiationIfNeeded(TypeId subTy, TypeId superTy, c
void TypeChecker::unifyWithInstantiationIfNeeded(TypeId subTy, TypeId superTy, const ScopePtr& scope, Unifier& state) void TypeChecker::unifyWithInstantiationIfNeeded(TypeId subTy, TypeId superTy, const ScopePtr& scope, Unifier& state)
{ {
LUAU_ASSERT(!FFlag::LuauInstantiateInSubtyping);
if (!maybeGeneric(subTy)) if (!maybeGeneric(subTy))
// Quick check to see if we definitely can't instantiate // Quick check to see if we definitely can't instantiate
state.tryUnify(subTy, superTy, /*isFunctionCall*/ false); state.tryUnify(subTy, superTy, /*isFunctionCall*/ false);
@ -4828,6 +4855,33 @@ TypePackId TypeChecker::anyify(const ScopePtr& scope, TypePackId ty, Location lo
} }
} }
TypePackId TypeChecker::anyifyModuleReturnTypePackGenerics(TypePackId tp)
{
tp = follow(tp);
if (const VariadicTypePack* vtp = get<VariadicTypePack>(tp))
return get<GenericTypeVar>(vtp->ty) ? anyTypePack : tp;
if (!get<TypePack>(follow(tp)))
return tp;
std::vector<TypeId> resultTypes;
std::optional<TypePackId> resultTail;
TypePackIterator it = begin(tp);
for (TypePackIterator e = end(tp); it != e; ++it)
{
TypeId ty = follow(*it);
resultTypes.push_back(get<GenericTypeVar>(ty) ? anyType : ty);
}
if (std::optional<TypePackId> tail = it.tail())
resultTail = anyifyModuleReturnTypePackGenerics(*tail);
return addTypePack(resultTypes, resultTail);
}
void TypeChecker::reportError(const TypeError& error) void TypeChecker::reportError(const TypeError& error)
{ {
if (currentModule->mode == Mode::NoCheck) if (currentModule->mode == Mode::NoCheck)
@ -4955,8 +5009,7 @@ void TypeChecker::merge(RefinementMap& l, const RefinementMap& r)
Unifier TypeChecker::mkUnifier(const ScopePtr& scope, const Location& location) Unifier TypeChecker::mkUnifier(const ScopePtr& scope, const Location& location)
{ {
return Unifier{ return Unifier{NotNull{&normalizer}, currentModule->mode, NotNull{scope.get()}, location, Variance::Covariant};
&currentModule->internalTypes, singletonTypes, currentModule->mode, NotNull{scope.get()}, location, Variance::Covariant, unifierState};
} }
TypeId TypeChecker::freshType(const ScopePtr& scope) TypeId TypeChecker::freshType(const ScopePtr& scope)

View file

@ -27,6 +27,7 @@ LUAU_FASTFLAG(LuauUnknownAndNeverType)
LUAU_FASTFLAGVARIABLE(LuauMaybeGenericIntersectionTypes, false) LUAU_FASTFLAGVARIABLE(LuauMaybeGenericIntersectionTypes, false)
LUAU_FASTFLAGVARIABLE(LuauStringFormatArgumentErrorFix, false) LUAU_FASTFLAGVARIABLE(LuauStringFormatArgumentErrorFix, false)
LUAU_FASTFLAGVARIABLE(LuauNoMoreGlobalSingletonTypes, false) LUAU_FASTFLAGVARIABLE(LuauNoMoreGlobalSingletonTypes, false)
LUAU_FASTFLAG(LuauInstantiateInSubtyping)
namespace Luau namespace Luau
{ {
@ -339,6 +340,8 @@ bool isSubset(const UnionTypeVar& super, const UnionTypeVar& sub)
// then instantiate U if `isGeneric(U)` is true, and `maybeGeneric(T)` is false. // then instantiate U if `isGeneric(U)` is true, and `maybeGeneric(T)` is false.
bool isGeneric(TypeId ty) bool isGeneric(TypeId ty)
{ {
LUAU_ASSERT(!FFlag::LuauInstantiateInSubtyping);
ty = follow(ty); ty = follow(ty);
if (auto ftv = get<FunctionTypeVar>(ty)) if (auto ftv = get<FunctionTypeVar>(ty))
return ftv->generics.size() > 0 || ftv->genericPacks.size() > 0; return ftv->generics.size() > 0 || ftv->genericPacks.size() > 0;
@ -350,6 +353,8 @@ bool isGeneric(TypeId ty)
bool maybeGeneric(TypeId ty) bool maybeGeneric(TypeId ty)
{ {
LUAU_ASSERT(!FFlag::LuauInstantiateInSubtyping);
if (FFlag::LuauMaybeGenericIntersectionTypes) if (FFlag::LuauMaybeGenericIntersectionTypes)
{ {
ty = follow(ty); ty = follow(ty);

View file

@ -1,60 +1,64 @@
// 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/Unifiable.h" #include "Luau/Unifiable.h"
LUAU_FASTFLAG(LuauTypeNormalization2);
namespace Luau namespace Luau
{ {
namespace Unifiable namespace Unifiable
{ {
static int nextIndex = 0;
Free::Free(TypeLevel level) Free::Free(TypeLevel level)
: index(++nextIndex) : index(FFlag::LuauTypeNormalization2 ? ++nextIndex : ++DEPRECATED_nextIndex)
, level(level) , level(level)
{ {
} }
Free::Free(Scope* scope) Free::Free(Scope* scope)
: index(++nextIndex) : index(FFlag::LuauTypeNormalization2 ? ++nextIndex : ++DEPRECATED_nextIndex)
, scope(scope) , scope(scope)
{ {
} }
Free::Free(Scope* scope, TypeLevel level) Free::Free(Scope* scope, TypeLevel level)
: index(++nextIndex) : index(FFlag::LuauTypeNormalization2 ? ++nextIndex : ++DEPRECATED_nextIndex)
, level(level) , level(level)
, scope(scope) , scope(scope)
{ {
} }
int Free::nextIndex = 0; int Free::DEPRECATED_nextIndex = 0;
Generic::Generic() Generic::Generic()
: index(++nextIndex) : index(FFlag::LuauTypeNormalization2 ? ++nextIndex : ++DEPRECATED_nextIndex)
, name("g" + std::to_string(index)) , name("g" + std::to_string(index))
{ {
} }
Generic::Generic(TypeLevel level) Generic::Generic(TypeLevel level)
: index(++nextIndex) : index(FFlag::LuauTypeNormalization2 ? ++nextIndex : ++DEPRECATED_nextIndex)
, level(level) , level(level)
, name("g" + std::to_string(index)) , name("g" + std::to_string(index))
{ {
} }
Generic::Generic(const Name& name) Generic::Generic(const Name& name)
: index(++nextIndex) : index(FFlag::LuauTypeNormalization2 ? ++nextIndex : ++DEPRECATED_nextIndex)
, name(name) , name(name)
, explicitName(true) , explicitName(true)
{ {
} }
Generic::Generic(Scope* scope) Generic::Generic(Scope* scope)
: index(++nextIndex) : index(FFlag::LuauTypeNormalization2 ? ++nextIndex : ++DEPRECATED_nextIndex)
, scope(scope) , scope(scope)
{ {
} }
Generic::Generic(TypeLevel level, const Name& name) Generic::Generic(TypeLevel level, const Name& name)
: index(++nextIndex) : index(FFlag::LuauTypeNormalization2 ? ++nextIndex : ++DEPRECATED_nextIndex)
, level(level) , level(level)
, name(name) , name(name)
, explicitName(true) , explicitName(true)
@ -62,14 +66,14 @@ Generic::Generic(TypeLevel level, const Name& name)
} }
Generic::Generic(Scope* scope, const Name& name) Generic::Generic(Scope* scope, const Name& name)
: index(++nextIndex) : index(FFlag::LuauTypeNormalization2 ? ++nextIndex : ++DEPRECATED_nextIndex)
, scope(scope) , scope(scope)
, name(name) , name(name)
, explicitName(true) , explicitName(true)
{ {
} }
int Generic::nextIndex = 0; int Generic::DEPRECATED_nextIndex = 0;
Error::Error() Error::Error()
: index(++nextIndex) : index(++nextIndex)

View file

@ -2,6 +2,7 @@
#include "Luau/Unifier.h" #include "Luau/Unifier.h"
#include "Luau/Common.h" #include "Luau/Common.h"
#include "Luau/Instantiation.h"
#include "Luau/RecursionCounter.h" #include "Luau/RecursionCounter.h"
#include "Luau/Scope.h" #include "Luau/Scope.h"
#include "Luau/TypePack.h" #include "Luau/TypePack.h"
@ -20,7 +21,9 @@ LUAU_FASTINTVARIABLE(LuauTypeInferLowerBoundsIterationLimit, 2000);
LUAU_FASTFLAG(LuauLowerBoundsCalculation); LUAU_FASTFLAG(LuauLowerBoundsCalculation);
LUAU_FASTFLAG(LuauErrorRecoveryType); LUAU_FASTFLAG(LuauErrorRecoveryType);
LUAU_FASTFLAG(LuauUnknownAndNeverType) LUAU_FASTFLAG(LuauUnknownAndNeverType)
LUAU_FASTFLAGVARIABLE(LuauSubtypeNormalizer, false);
LUAU_FASTFLAGVARIABLE(LuauScalarShapeSubtyping, false) LUAU_FASTFLAGVARIABLE(LuauScalarShapeSubtyping, false)
LUAU_FASTFLAGVARIABLE(LuauInstantiateInSubtyping, false)
LUAU_FASTFLAG(LuauClassTypeVarsInSubstitution) LUAU_FASTFLAG(LuauClassTypeVarsInSubstitution)
LUAU_FASTFLAG(LuauCallUnifyPackTails) LUAU_FASTFLAG(LuauCallUnifyPackTails)
LUAU_FASTFLAG(DebugLuauDeferredConstraintResolution) LUAU_FASTFLAG(DebugLuauDeferredConstraintResolution)
@ -343,17 +346,19 @@ static bool subsumes(bool useScopes, TY_A* left, TY_B* right)
return left->level.subsumes(right->level); return left->level.subsumes(right->level);
} }
Unifier::Unifier(TypeArena* types, NotNull<SingletonTypes> singletonTypes, Mode mode, NotNull<Scope> scope, const Location& location, Unifier::Unifier(NotNull<Normalizer> normalizer, Mode mode, NotNull<Scope> scope, const Location& location,
Variance variance, UnifierSharedState& sharedState, TxnLog* parentLog) Variance variance, TxnLog* parentLog)
: types(types) : types(normalizer->arena)
, singletonTypes(singletonTypes) , singletonTypes(normalizer->singletonTypes)
, normalizer(normalizer)
, mode(mode) , mode(mode)
, scope(scope) , scope(scope)
, log(parentLog) , log(parentLog)
, location(location) , location(location)
, variance(variance) , variance(variance)
, sharedState(sharedState) , sharedState(*normalizer->sharedState)
{ {
normalize = FFlag::LuauSubtypeNormalizer;
LUAU_ASSERT(sharedState.iceHandler); LUAU_ASSERT(sharedState.iceHandler);
} }
@ -524,7 +529,7 @@ void Unifier::tryUnify_(TypeId subTy, TypeId superTy, bool isFunctionCall, bool
{ {
tryUnifyUnionWithType(subTy, subUnion, superTy); tryUnifyUnionWithType(subTy, subUnion, superTy);
} }
else if (const UnionTypeVar* uv = log.getMutable<UnionTypeVar>(superTy)) else if (const UnionTypeVar* uv = (FFlag::LuauSubtypeNormalizer? nullptr: log.getMutable<UnionTypeVar>(superTy)))
{ {
tryUnifyTypeWithUnion(subTy, superTy, uv, cacheEnabled, isFunctionCall); tryUnifyTypeWithUnion(subTy, superTy, uv, cacheEnabled, isFunctionCall);
} }
@ -532,6 +537,10 @@ void Unifier::tryUnify_(TypeId subTy, TypeId superTy, bool isFunctionCall, bool
{ {
tryUnifyTypeWithIntersection(subTy, superTy, uv); tryUnifyTypeWithIntersection(subTy, superTy, uv);
} }
else if (const UnionTypeVar* uv = log.getMutable<UnionTypeVar>(superTy))
{
tryUnifyTypeWithUnion(subTy, superTy, uv, cacheEnabled, isFunctionCall);
}
else if (const IntersectionTypeVar* uv = log.getMutable<IntersectionTypeVar>(subTy)) else if (const IntersectionTypeVar* uv = log.getMutable<IntersectionTypeVar>(subTy))
{ {
tryUnifyIntersectionWithType(subTy, uv, superTy, cacheEnabled, isFunctionCall); tryUnifyIntersectionWithType(subTy, uv, superTy, cacheEnabled, isFunctionCall);
@ -585,7 +594,7 @@ void Unifier::tryUnify_(TypeId subTy, TypeId superTy, bool isFunctionCall, bool
void Unifier::tryUnifyUnionWithType(TypeId subTy, const UnionTypeVar* subUnion, TypeId superTy) void Unifier::tryUnifyUnionWithType(TypeId subTy, const UnionTypeVar* subUnion, TypeId superTy)
{ {
// A | B <: T if A <: T and B <: T // A | B <: T if and only if A <: T and B <: T
bool failed = false; bool failed = false;
std::optional<TypeError> unificationTooComplex; std::optional<TypeError> unificationTooComplex;
std::optional<TypeError> firstFailedOption; std::optional<TypeError> firstFailedOption;
@ -715,6 +724,7 @@ void Unifier::tryUnifyTypeWithUnion(TypeId subTy, TypeId superTy, const UnionTyp
{ {
TypeId type = uv->options[(i + startIndex) % uv->options.size()]; TypeId type = uv->options[(i + startIndex) % uv->options.size()];
Unifier innerState = makeChildUnifier(); Unifier innerState = makeChildUnifier();
innerState.normalize = false;
innerState.tryUnify_(subTy, type, isFunctionCall); innerState.tryUnify_(subTy, type, isFunctionCall);
if (innerState.errors.empty()) if (innerState.errors.empty())
@ -741,6 +751,20 @@ void Unifier::tryUnifyTypeWithUnion(TypeId subTy, TypeId superTy, const UnionTyp
{ {
reportError(*unificationTooComplex); reportError(*unificationTooComplex);
} }
else if (!found && normalize)
{
// It is possible that T <: A | B even though T </: A and T </:B
// for example boolean <: true | false.
// We deal with this by type normalization.
const NormalizedType* subNorm = normalizer->normalize(subTy);
const NormalizedType* superNorm = normalizer->normalize(superTy);
if (!subNorm || !superNorm)
reportError(TypeError{location, UnificationTooComplex{}});
else if ((failedOptionCount == 1 || foundHeuristic) && failedOption)
tryUnifyNormalizedTypes(subTy, superTy, *subNorm, *superNorm, "None of the union options are compatible. For example:", *failedOption);
else
tryUnifyNormalizedTypes(subTy, superTy, *subNorm, *superNorm, "none of the union options are compatible");
}
else if (!found) else if (!found)
{ {
if ((failedOptionCount == 1 || foundHeuristic) && failedOption) if ((failedOptionCount == 1 || foundHeuristic) && failedOption)
@ -755,7 +779,7 @@ void Unifier::tryUnifyTypeWithIntersection(TypeId subTy, TypeId superTy, const I
std::optional<TypeError> unificationTooComplex; std::optional<TypeError> unificationTooComplex;
std::optional<TypeError> firstFailedOption; std::optional<TypeError> firstFailedOption;
// T <: A & B if T <: A and T <: B // T <: A & B if and only if T <: A and T <: B
for (TypeId type : uv->parts) for (TypeId type : uv->parts)
{ {
Unifier innerState = makeChildUnifier(); Unifier innerState = makeChildUnifier();
@ -806,6 +830,7 @@ void Unifier::tryUnifyIntersectionWithType(TypeId subTy, const IntersectionTypeV
{ {
TypeId type = uv->parts[(i + startIndex) % uv->parts.size()]; TypeId type = uv->parts[(i + startIndex) % uv->parts.size()];
Unifier innerState = makeChildUnifier(); Unifier innerState = makeChildUnifier();
innerState.normalize = false;
innerState.tryUnify_(type, superTy, isFunctionCall); innerState.tryUnify_(type, superTy, isFunctionCall);
if (innerState.errors.empty()) if (innerState.errors.empty())
@ -822,12 +847,207 @@ void Unifier::tryUnifyIntersectionWithType(TypeId subTy, const IntersectionTypeV
if (unificationTooComplex) if (unificationTooComplex)
reportError(*unificationTooComplex); reportError(*unificationTooComplex);
else if (!found && normalize)
{
// It is possible that A & B <: T even though A </: T and B </: T
// for example string? & number? <: nil.
// We deal with this by type normalization.
const NormalizedType* subNorm = normalizer->normalize(subTy);
const NormalizedType* superNorm = normalizer->normalize(superTy);
if (subNorm && superNorm)
tryUnifyNormalizedTypes(subTy, superTy, *subNorm, *superNorm, "none of the intersection parts are compatible");
else
reportError(TypeError{location, UnificationTooComplex{}});
}
else if (!found) else if (!found)
{ {
reportError(TypeError{location, TypeMismatch{superTy, subTy, "none of the intersection parts are compatible"}}); reportError(TypeError{location, TypeMismatch{superTy, subTy, "none of the intersection parts are compatible"}});
} }
} }
void Unifier::tryUnifyNormalizedTypes(TypeId subTy, TypeId superTy, const NormalizedType& subNorm, const NormalizedType& superNorm, std::string reason, std::optional<TypeError> error)
{
LUAU_ASSERT(FFlag::LuauSubtypeNormalizer);
if (get<UnknownTypeVar>(superNorm.tops) || get<AnyTypeVar>(superNorm.tops) || get<AnyTypeVar>(subNorm.tops))
return;
else if (get<UnknownTypeVar>(subNorm.tops))
return reportError(TypeError{location, TypeMismatch{superTy, subTy, reason, error}});
if (get<ErrorTypeVar>(subNorm.errors))
if (!get<ErrorTypeVar>(superNorm.errors))
return reportError(TypeError{location, TypeMismatch{superTy, subTy, reason, error}});
if (get<PrimitiveTypeVar>(subNorm.booleans))
{
if (!get<PrimitiveTypeVar>(superNorm.booleans))
return reportError(TypeError{location, TypeMismatch{superTy, subTy, reason, error}});
}
else if (const SingletonTypeVar* stv = get<SingletonTypeVar>(subNorm.booleans))
{
if (!get<PrimitiveTypeVar>(superNorm.booleans) && stv != get<SingletonTypeVar>(superNorm.booleans))
return reportError(TypeError{location, TypeMismatch{superTy, subTy, reason, error}});
}
if (get<PrimitiveTypeVar>(subNorm.nils))
if (!get<PrimitiveTypeVar>(superNorm.nils))
return reportError(TypeError{location, TypeMismatch{superTy, subTy, reason, error}});
if (get<PrimitiveTypeVar>(subNorm.numbers))
if (!get<PrimitiveTypeVar>(superNorm.numbers))
return reportError(TypeError{location, TypeMismatch{superTy, subTy, reason, error}});
if (subNorm.strings && superNorm.strings)
{
for (auto [name, ty] : *subNorm.strings)
if (!superNorm.strings->count(name))
return reportError(TypeError{location, TypeMismatch{superTy, subTy, reason, error}});
}
else if (!subNorm.strings && superNorm.strings)
return reportError(TypeError{location, TypeMismatch{superTy, subTy, reason, error}});
if (get<PrimitiveTypeVar>(subNorm.threads))
if (!get<PrimitiveTypeVar>(superNorm.errors))
return reportError(TypeError{location, TypeMismatch{superTy, subTy, reason, error}});
for (TypeId subClass : subNorm.classes)
{
bool found = false;
const ClassTypeVar* subCtv = get<ClassTypeVar>(subClass);
for (TypeId superClass : superNorm.classes)
{
const ClassTypeVar* superCtv = get<ClassTypeVar>(superClass);
if (isSubclass(subCtv, superCtv))
{
found = true;
break;
}
}
if (!found)
return reportError(TypeError{location, TypeMismatch{superTy, subTy, reason, error}});
}
for (TypeId subTable : subNorm.tables)
{
bool found = false;
for (TypeId superTable : superNorm.tables)
{
Unifier innerState = makeChildUnifier();
if (get<MetatableTypeVar>(superTable))
innerState.tryUnifyWithMetatable(subTable, superTable, /* reversed */ false);
else if (get<MetatableTypeVar>(subTable))
innerState.tryUnifyWithMetatable(superTable, subTable, /* reversed */ true);
else
innerState.tryUnifyTables(subTable, superTable);
if (innerState.errors.empty())
{
found = true;
log.concat(std::move(innerState.log));
break;
}
else if (auto e = hasUnificationTooComplex(innerState.errors))
return reportError(*e);
}
if (!found)
return reportError(TypeError{location, TypeMismatch{superTy, subTy, reason, error}});
}
if (subNorm.functions)
{
if (!superNorm.functions)
return reportError(TypeError{location, TypeMismatch{superTy, subTy, reason, error}});
if (superNorm.functions->empty())
return;
for (TypeId superFun : *superNorm.functions)
{
Unifier innerState = makeChildUnifier();
const FunctionTypeVar* superFtv = get<FunctionTypeVar>(superFun);
if (!superFtv)
return reportError(TypeError{location, TypeMismatch{superTy, subTy, reason, error}});
TypePackId tgt = innerState.tryApplyOverloadedFunction(subTy, subNorm.functions, superFtv->argTypes);
innerState.tryUnify_(tgt, superFtv->retTypes);
if (innerState.errors.empty())
log.concat(std::move(innerState.log));
else if (auto e = hasUnificationTooComplex(innerState.errors))
return reportError(*e);
else
return reportError(TypeError{location, TypeMismatch{superTy, subTy, reason, error}});
}
}
for (auto& [tyvar, subIntersect] : subNorm.tyvars)
{
auto found = superNorm.tyvars.find(tyvar);
if (found == superNorm.tyvars.end())
tryUnifyNormalizedTypes(subTy, superTy, *subIntersect, superNorm, reason, error);
else
tryUnifyNormalizedTypes(subTy, superTy, *subIntersect, *found->second, reason, error);
if (!errors.empty())
return;
}
}
TypePackId Unifier::tryApplyOverloadedFunction(TypeId function, const NormalizedFunctionType& overloads, TypePackId args)
{
if (!overloads || overloads->empty())
{
reportError(TypeError{location, CannotCallNonFunction{function}});
return singletonTypes->errorRecoveryTypePack();
}
std::optional<TypePackId> result;
const FunctionTypeVar* firstFun = nullptr;
for (TypeId overload : *overloads)
{
if (const FunctionTypeVar* ftv = get<FunctionTypeVar>(overload))
{
// TODO: instantiate generics?
if (ftv->generics.empty() && ftv->genericPacks.empty())
{
if (!firstFun)
firstFun = ftv;
Unifier innerState = makeChildUnifier();
innerState.tryUnify_(args, ftv->argTypes);
if (innerState.errors.empty())
{
log.concat(std::move(innerState.log));
if (result)
{
// Annoyingly, since we don't support intersection of generic type packs,
// the intersection may fail. We rather arbitrarily use the first matching overload
// in that case.
if (std::optional<TypePackId> intersect = normalizer->intersectionOfTypePacks(*result, ftv->retTypes))
result = intersect;
}
else
result = ftv->retTypes;
}
else if (auto e = hasUnificationTooComplex(innerState.errors))
{
reportError(*e);
return singletonTypes->errorRecoveryTypePack(args);
}
}
}
}
if (result)
return *result;
else if (firstFun)
{
// TODO: better error reporting?
// The logic for error reporting overload resolution
// is currently over in TypeInfer.cpp, should we move it?
reportError(TypeError{location, GenericError{"No matching overload."}});
return singletonTypes->errorRecoveryTypePack(firstFun->retTypes);
}
else
{
reportError(TypeError{location, CannotCallNonFunction{function}});
return singletonTypes->errorRecoveryTypePack();
}
}
bool Unifier::canCacheResult(TypeId subTy, TypeId superTy) bool Unifier::canCacheResult(TypeId subTy, TypeId superTy)
{ {
bool* superTyInfo = sharedState.skipCacheForType.find(superTy); bool* superTyInfo = sharedState.skipCacheForType.find(superTy);
@ -1253,14 +1473,38 @@ void Unifier::tryUnifyFunctions(TypeId subTy, TypeId superTy, bool isFunctionCal
ice("passed non-function types to unifyFunction"); ice("passed non-function types to unifyFunction");
size_t numGenerics = superFunction->generics.size(); size_t numGenerics = superFunction->generics.size();
if (numGenerics != subFunction->generics.size()) size_t numGenericPacks = superFunction->genericPacks.size();
bool shouldInstantiate = (numGenerics == 0 && subFunction->generics.size() > 0) || (numGenericPacks == 0 && subFunction->genericPacks.size() > 0);
if (FFlag::LuauInstantiateInSubtyping && variance == Covariant && shouldInstantiate)
{
Instantiation instantiation{&log, types, scope->level, scope};
std::optional<TypeId> instantiated = instantiation.substitute(subTy);
if (instantiated.has_value())
{
subFunction = log.getMutable<FunctionTypeVar>(*instantiated);
if (!subFunction)
ice("instantiation made a function type into a non-function type in unifyFunction");
numGenerics = std::min(superFunction->generics.size(), subFunction->generics.size());
numGenericPacks = std::min(superFunction->genericPacks.size(), subFunction->genericPacks.size());
}
else
{
reportError(TypeError{location, UnificationTooComplex{}});
}
}
else if (numGenerics != subFunction->generics.size())
{ {
numGenerics = std::min(superFunction->generics.size(), subFunction->generics.size()); numGenerics = std::min(superFunction->generics.size(), subFunction->generics.size());
reportError(TypeError{location, TypeMismatch{superTy, subTy, "different number of generic type parameters"}}); reportError(TypeError{location, TypeMismatch{superTy, subTy, "different number of generic type parameters"}});
} }
size_t numGenericPacks = superFunction->genericPacks.size();
if (numGenericPacks != subFunction->genericPacks.size()) if (numGenericPacks != subFunction->genericPacks.size())
{ {
numGenericPacks = std::min(superFunction->genericPacks.size(), subFunction->genericPacks.size()); numGenericPacks = std::min(superFunction->genericPacks.size(), subFunction->genericPacks.size());
@ -1376,6 +1620,27 @@ void Unifier::tryUnifyTables(TypeId subTy, TypeId superTy, bool isIntersection)
std::vector<std::string> missingProperties; std::vector<std::string> missingProperties;
std::vector<std::string> extraProperties; std::vector<std::string> extraProperties;
if (FFlag::LuauInstantiateInSubtyping)
{
if (variance == Covariant && subTable->state == TableState::Generic && superTable->state != TableState::Generic)
{
Instantiation instantiation{&log, types, subTable->level, scope};
std::optional<TypeId> instantiated = instantiation.substitute(subTy);
if (instantiated.has_value())
{
subTable = log.getMutable<TableTypeVar>(*instantiated);
if (!subTable)
ice("instantiation made a table type into a non-table type in tryUnifyTables");
}
else
{
reportError(TypeError{location, UnificationTooComplex{}});
}
}
}
// Optimization: First test that the property sets are compatible without doing any recursive unification // Optimization: First test that the property sets are compatible without doing any recursive unification
if (!subTable->indexer && subTable->state != TableState::Free) if (!subTable->indexer && subTable->state != TableState::Free)
{ {
@ -2344,8 +2609,9 @@ bool Unifier::occursCheck(DenseHashSet<TypePackId>& seen, TypePackId needle, Typ
Unifier Unifier::makeChildUnifier() Unifier Unifier::makeChildUnifier()
{ {
Unifier u = Unifier{types, singletonTypes, mode, scope, location, variance, sharedState, &log}; Unifier u = Unifier{normalizer, mode, scope, location, variance, &log};
u.anyIsTop = anyIsTop; u.anyIsTop = anyIsTop;
u.normalize = normalize;
return u; return u;
} }

View file

@ -25,6 +25,8 @@ LUAU_DYNAMIC_FASTFLAGVARIABLE(LuaReportParseIntegerIssues, false)
LUAU_FASTFLAGVARIABLE(LuauInterpolatedStringBaseSupport, false) LUAU_FASTFLAGVARIABLE(LuauInterpolatedStringBaseSupport, false)
LUAU_FASTFLAGVARIABLE(LuauTypeAnnotationLocationChange, false) LUAU_FASTFLAGVARIABLE(LuauTypeAnnotationLocationChange, false)
LUAU_FASTFLAGVARIABLE(LuauCommaParenWarnings, false)
bool lua_telemetry_parsed_out_of_range_bin_integer = false; bool lua_telemetry_parsed_out_of_range_bin_integer = false;
bool lua_telemetry_parsed_out_of_range_hex_integer = false; bool lua_telemetry_parsed_out_of_range_hex_integer = false;
bool lua_telemetry_parsed_double_prefix_hex_integer = false; bool lua_telemetry_parsed_double_prefix_hex_integer = false;
@ -1062,6 +1064,12 @@ void Parser::parseExprList(TempVector<AstExpr*>& result)
{ {
nextLexeme(); nextLexeme();
if (FFlag::LuauCommaParenWarnings && lexer.current().type == ')')
{
report(lexer.current().location, "Expected expression after ',' but got ')' instead");
break;
}
result.push_back(parseExpr()); result.push_back(parseExpr());
} }
} }
@ -1148,7 +1156,14 @@ AstTypePack* Parser::parseTypeList(TempVector<AstType*>& result, TempVector<std:
result.push_back(parseTypeAnnotation()); result.push_back(parseTypeAnnotation());
if (lexer.current().type != ',') if (lexer.current().type != ',')
break; break;
nextLexeme(); nextLexeme();
if (FFlag::LuauCommaParenWarnings && lexer.current().type == ')')
{
report(lexer.current().location, "Expected type after ',' but got ')' instead");
break;
}
} }
return nullptr; return nullptr;
@ -2514,7 +2529,15 @@ std::pair<AstArray<AstGenericType>, AstArray<AstGenericTypePack>> Parser::parseG
} }
if (lexer.current().type == ',') if (lexer.current().type == ',')
{
nextLexeme(); nextLexeme();
if (FFlag::LuauCommaParenWarnings && lexer.current().type == '>')
{
report(lexer.current().location, "Expected type after ',' but got '>' instead");
break;
}
}
else else
break; break;
} }

View file

@ -82,11 +82,13 @@ static void profilerLoop()
if (now - last >= 1.0 / double(gProfiler.frequency)) if (now - last >= 1.0 / double(gProfiler.frequency))
{ {
gProfiler.ticks += uint64_t((now - last) * 1e6); int64_t ticks = int64_t((now - last) * 1e6);
gProfiler.ticks += ticks;
gProfiler.samples++; gProfiler.samples++;
gProfiler.callbacks->interrupt = profilerTrigger; gProfiler.callbacks->interrupt = profilerTrigger;
last = now; last += ticks * 1e-6;
} }
else else
{ {

View file

@ -152,6 +152,7 @@ private:
void placeModRegMem(OperandX64 rhs, uint8_t regop); void placeModRegMem(OperandX64 rhs, uint8_t regop);
void placeRex(RegisterX64 op); void placeRex(RegisterX64 op);
void placeRex(OperandX64 op); void placeRex(OperandX64 op);
void placeRexNoW(OperandX64 op);
void placeRex(RegisterX64 lhs, OperandX64 rhs); void placeRex(RegisterX64 lhs, OperandX64 rhs);
void placeVex(OperandX64 dst, OperandX64 src1, OperandX64 src2, bool setW, uint8_t mode, uint8_t prefix); void placeVex(OperandX64 dst, OperandX64 src1, OperandX64 src2, bool setW, uint8_t mode, uint8_t prefix);
void placeImm8Or32(int32_t imm); void placeImm8Or32(int32_t imm);

View file

@ -24,13 +24,15 @@ struct CodeAllocator
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
// If data is placed inside the block itself (some platforms require this), we also return 'unwindDataSizeInBlock' // 'startOffset' reserves space for data at the beginning of the page
void* (*createBlockUnwindInfo)(void* context, uint8_t* block, size_t blockSize, size_t& unwindDataSizeInBlock) = nullptr; void* (*createBlockUnwindInfo)(void* context, uint8_t* block, size_t blockSize, size_t& startOffset) = nullptr;
// 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;
static const size_t kMaxUnwindDataSize = 128; // 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
static const size_t kMaxReservedDataSize = 256;
bool allocateNewBlock(size_t& unwindInfoSize); bool allocateNewBlock(size_t& unwindInfoSize);

View file

@ -10,7 +10,7 @@ namespace CodeGen
{ {
// context must be an UnwindBuilder // context must be an UnwindBuilder
void* createBlockUnwindInfo(void* context, uint8_t* block, size_t blockSize, size_t& unwindDataSizeInBlock); void* createBlockUnwindInfo(void* context, uint8_t* block, size_t blockSize, size_t& startOffset);
void destroyBlockUnwindInfo(void* context, void* unwindData); void destroyBlockUnwindInfo(void* context, void* unwindData);
} // namespace CodeGen } // namespace CodeGen

View file

@ -14,7 +14,10 @@ namespace CodeGen
class UnwindBuilder class UnwindBuilder
{ {
public: public:
virtual ~UnwindBuilder() {} virtual ~UnwindBuilder() = default;
virtual void setBeginOffset(size_t beginOffset) = 0;
virtual size_t getBeginOffset() const = 0;
virtual void start() = 0; virtual void start() = 0;

View file

@ -12,6 +12,9 @@ namespace CodeGen
class UnwindBuilderDwarf2 : public UnwindBuilder class UnwindBuilderDwarf2 : public UnwindBuilder
{ {
public: public:
void setBeginOffset(size_t beginOffset) override;
size_t getBeginOffset() const override;
void start() override; void start() override;
void spill(int espOffset, RegisterX64 reg) override; void spill(int espOffset, RegisterX64 reg) override;
@ -26,6 +29,8 @@ public:
void finalize(char* target, void* funcAddress, size_t funcSize) const override; void finalize(char* target, void* funcAddress, size_t funcSize) const override;
private: private:
size_t beginOffset = 0;
static const unsigned kRawDataLimit = 128; static const unsigned kRawDataLimit = 128;
uint8_t rawData[kRawDataLimit]; uint8_t rawData[kRawDataLimit];
uint8_t* pos = rawData; uint8_t* pos = rawData;

View file

@ -22,6 +22,9 @@ struct UnwindCodeWin
class UnwindBuilderWin : public UnwindBuilder class UnwindBuilderWin : public UnwindBuilder
{ {
public: public:
void setBeginOffset(size_t beginOffset) override;
size_t getBeginOffset() const override;
void start() override; void start() override;
void spill(int espOffset, RegisterX64 reg) override; void spill(int espOffset, RegisterX64 reg) override;
@ -36,6 +39,8 @@ public:
void finalize(char* target, void* funcAddress, size_t funcSize) const override; void finalize(char* target, void* funcAddress, size_t funcSize) const override;
private: private:
size_t beginOffset = 0;
// Windows unwind codes are written in reverse, so we have to collect them all first // Windows unwind codes are written in reverse, so we have to collect them all first
std::vector<UnwindCodeWin> unwindCodes; std::vector<UnwindCodeWin> unwindCodes;

View file

@ -354,10 +354,15 @@ void AssemblyBuilderX64::jmp(Label& label)
void AssemblyBuilderX64::jmp(OperandX64 op) void AssemblyBuilderX64::jmp(OperandX64 op)
{ {
LUAU_ASSERT((op.cat == CategoryX64::reg ? op.base.size : op.memSize) == SizeX64::qword);
if (logText) if (logText)
log("jmp", op); log("jmp", op);
placeRex(op); // Indirect absolute calls always work in 64 bit width mode, so REX.W is optional
// While we could keep an optional prefix, in Windows x64 ABI it signals a tail call return statement to the unwinder
placeRexNoW(op);
place(0xff); place(0xff);
placeModRegMem(op, 4); placeModRegMem(op, 4);
commit(); commit();
@ -376,10 +381,14 @@ void AssemblyBuilderX64::call(Label& label)
void AssemblyBuilderX64::call(OperandX64 op) void AssemblyBuilderX64::call(OperandX64 op)
{ {
LUAU_ASSERT((op.cat == CategoryX64::reg ? op.base.size : op.memSize) == SizeX64::qword);
if (logText) if (logText)
log("call", op); log("call", op);
placeRex(op); // Indirect absolute calls always work in 64 bit width mode, so REX.W is optional
placeRexNoW(op);
place(0xff); place(0xff);
placeModRegMem(op, 2); placeModRegMem(op, 2);
commit(); commit();
@ -838,6 +847,21 @@ void AssemblyBuilderX64::placeRex(OperandX64 op)
place(code | 0x40); place(code | 0x40);
} }
void AssemblyBuilderX64::placeRexNoW(OperandX64 op)
{
uint8_t code = 0;
if (op.cat == CategoryX64::reg)
code = REX_B(op.base);
else if (op.cat == CategoryX64::mem)
code = REX_X(op.index) | REX_B(op.base);
else
LUAU_ASSERT(!"No encoding for left operand of this category");
if (code != 0)
place(code | 0x40);
}
void AssemblyBuilderX64::placeRex(RegisterX64 lhs, OperandX64 rhs) void AssemblyBuilderX64::placeRex(RegisterX64 lhs, OperandX64 rhs)
{ {
uint8_t code = REX_W(lhs.size == SizeX64::qword); uint8_t code = REX_W(lhs.size == SizeX64::qword);

View file

@ -91,7 +91,7 @@ CodeAllocator::CodeAllocator(size_t blockSize, size_t maxTotalSize)
: blockSize(blockSize) : blockSize(blockSize)
, maxTotalSize(maxTotalSize) , maxTotalSize(maxTotalSize)
{ {
LUAU_ASSERT(blockSize > kMaxUnwindDataSize); LUAU_ASSERT(blockSize > kMaxReservedDataSize);
LUAU_ASSERT(maxTotalSize >= blockSize); LUAU_ASSERT(maxTotalSize >= blockSize);
} }
@ -116,15 +116,15 @@ bool CodeAllocator::allocate(
size_t totalSize = alignedDataSize + codeSize; size_t totalSize = alignedDataSize + codeSize;
// Function has to fit into a single block with unwinding information // Function has to fit into a single block with unwinding information
if (totalSize > blockSize - kMaxUnwindDataSize) if (totalSize > blockSize - kMaxReservedDataSize)
return false; return false;
size_t unwindInfoSize = 0; size_t startOffset = 0;
// We might need a new block // We might need a new block
if (totalSize > size_t(blockEnd - blockPos)) if (totalSize > size_t(blockEnd - blockPos))
{ {
if (!allocateNewBlock(unwindInfoSize)) if (!allocateNewBlock(startOffset))
return false; return false;
LUAU_ASSERT(totalSize <= size_t(blockEnd - blockPos)); LUAU_ASSERT(totalSize <= size_t(blockEnd - blockPos));
@ -132,20 +132,20 @@ bool CodeAllocator::allocate(
LUAU_ASSERT((uintptr_t(blockPos) & (kPageSize - 1)) == 0); // Allocation starts on page boundary LUAU_ASSERT((uintptr_t(blockPos) & (kPageSize - 1)) == 0); // Allocation starts on page boundary
size_t dataOffset = unwindInfoSize + alignedDataSize - dataSize; size_t dataOffset = startOffset + alignedDataSize - dataSize;
size_t codeOffset = unwindInfoSize + alignedDataSize; size_t codeOffset = startOffset + alignedDataSize;
if (dataSize) if (dataSize)
memcpy(blockPos + dataOffset, data, dataSize); memcpy(blockPos + dataOffset, data, dataSize);
if (codeSize) if (codeSize)
memcpy(blockPos + codeOffset, code, codeSize); memcpy(blockPos + codeOffset, code, codeSize);
size_t pageAlignedSize = alignToPageSize(unwindInfoSize + totalSize); size_t pageAlignedSize = alignToPageSize(startOffset + totalSize);
makePagesExecutable(blockPos, pageAlignedSize); makePagesExecutable(blockPos, pageAlignedSize);
flushInstructionCache(blockPos + codeOffset, codeSize); flushInstructionCache(blockPos + codeOffset, codeSize);
result = blockPos + unwindInfoSize; result = blockPos + startOffset;
resultSize = totalSize; resultSize = totalSize;
resultCodeStart = blockPos + codeOffset; resultCodeStart = blockPos + codeOffset;
@ -190,7 +190,7 @@ bool CodeAllocator::allocateNewBlock(size_t& unwindInfoSize)
// 'Round up' to preserve 16 byte alignment of the following data and code // 'Round up' to preserve 16 byte alignment of the following data and code
unwindInfoSize = (unwindInfoSize + 15) & ~15; unwindInfoSize = (unwindInfoSize + 15) & ~15;
LUAU_ASSERT(unwindInfoSize <= kMaxUnwindDataSize); LUAU_ASSERT(unwindInfoSize <= kMaxReservedDataSize);
if (!unwindInfo) if (!unwindInfo)
return false; return false;

View file

@ -51,7 +51,7 @@ namespace Luau
namespace CodeGen namespace CodeGen
{ {
void* createBlockUnwindInfo(void* context, uint8_t* block, size_t blockSize, size_t& unwindDataSizeInBlock) void* createBlockUnwindInfo(void* context, uint8_t* block, size_t blockSize, size_t& beginOffset)
{ {
#if defined(_WIN32) && defined(_M_X64) #if defined(_WIN32) && defined(_M_X64)
UnwindBuilder* unwind = (UnwindBuilder*)context; UnwindBuilder* unwind = (UnwindBuilder*)context;
@ -75,7 +75,7 @@ void* createBlockUnwindInfo(void* context, uint8_t* block, size_t blockSize, siz
return nullptr; return nullptr;
} }
unwindDataSizeInBlock = unwindSize; beginOffset = unwindSize + unwind->getBeginOffset();
return block; return block;
#elif !defined(_WIN32) #elif !defined(_WIN32)
UnwindBuilder* unwind = (UnwindBuilder*)context; UnwindBuilder* unwind = (UnwindBuilder*)context;
@ -94,7 +94,7 @@ void* createBlockUnwindInfo(void* context, uint8_t* block, size_t blockSize, siz
__register_frame(unwindData); __register_frame(unwindData);
#endif #endif
unwindDataSizeInBlock = unwindSize; beginOffset = unwindSize + unwind->getBeginOffset();
return block; return block;
#endif #endif

View file

@ -129,6 +129,16 @@ namespace Luau
namespace CodeGen namespace CodeGen
{ {
void UnwindBuilderDwarf2::setBeginOffset(size_t beginOffset)
{
this->beginOffset = beginOffset;
}
size_t UnwindBuilderDwarf2::getBeginOffset() const
{
return beginOffset;
}
void UnwindBuilderDwarf2::start() void UnwindBuilderDwarf2::start()
{ {
uint8_t* cieLength = pos; uint8_t* cieLength = pos;

View file

@ -32,6 +32,16 @@ struct UnwindInfoWin
uint8_t frameregoff : 4; uint8_t frameregoff : 4;
}; };
void UnwindBuilderWin::setBeginOffset(size_t beginOffset)
{
this->beginOffset = beginOffset;
}
size_t UnwindBuilderWin::getBeginOffset() const
{
return beginOffset;
}
void UnwindBuilderWin::start() void UnwindBuilderWin::start()
{ {
stackOffset = 8; // Return address was pushed by calling the function stackOffset = 8; // Return address was pushed by calling the function

View file

@ -13,6 +13,7 @@ inline bool isFlagExperimental(const char* flag)
static const char* kList[] = { static const char* kList[] = {
"LuauLowerBoundsCalculation", "LuauLowerBoundsCalculation",
"LuauInterpolatedStringBaseSupport", "LuauInterpolatedStringBaseSupport",
"LuauInstantiateInSubtyping", // requires some fixes to lua-apps code
// makes sure we always have at least one entry // makes sure we always have at least one entry
nullptr, nullptr,
}; };

View file

@ -4,6 +4,7 @@ MAKEFLAGS+=-r -j8
COMMA=, COMMA=,
config=debug config=debug
protobuf=system
BUILD=build/$(config) BUILD=build/$(config)
@ -95,12 +96,22 @@ ifeq ($(config),fuzz)
CXX=clang++ # our fuzzing infra relies on llvm fuzzer CXX=clang++ # our fuzzing infra relies on llvm fuzzer
CXXFLAGS+=-fsanitize=address,fuzzer -Ibuild/libprotobuf-mutator -O2 CXXFLAGS+=-fsanitize=address,fuzzer -Ibuild/libprotobuf-mutator -O2
LDFLAGS+=-fsanitize=address,fuzzer LDFLAGS+=-fsanitize=address,fuzzer
LPROTOBUF=-lprotobuf
DPROTOBUF=-D CMAKE_BUILD_TYPE=Release -D LIB_PROTO_MUTATOR_TESTING=OFF
EPROTOC=protoc
endif endif
ifeq ($(config),profile) ifeq ($(config),profile)
CXXFLAGS+=-O2 -DNDEBUG -gdwarf-4 -DCALLGRIND=1 CXXFLAGS+=-O2 -DNDEBUG -gdwarf-4 -DCALLGRIND=1
endif endif
ifeq ($(protobuf),download)
CXXFLAGS+=-Ibuild/libprotobuf-mutator/external.protobuf/include
LPROTOBUF=build/libprotobuf-mutator/external.protobuf/lib/libprotobuf.a
DPROTOBUF+=-D LIB_PROTO_MUTATOR_DOWNLOAD_PROTOBUF=ON
EPROTOC=../build/libprotobuf-mutator/external.protobuf/bin/protoc
endif
# target-specific flags # target-specific flags
$(AST_OBJECTS): CXXFLAGS+=-std=c++17 -ICommon/include -IAst/include $(AST_OBJECTS): CXXFLAGS+=-std=c++17 -ICommon/include -IAst/include
$(COMPILER_OBJECTS): CXXFLAGS+=-std=c++17 -ICompiler/include -ICommon/include -IAst/include $(COMPILER_OBJECTS): CXXFLAGS+=-std=c++17 -ICompiler/include -ICommon/include -IAst/include
@ -115,7 +126,7 @@ $(FUZZ_OBJECTS): CXXFLAGS+=-std=c++17 -ICommon/include -IAst/include -ICompiler/
$(TESTS_TARGET): LDFLAGS+=-lpthread $(TESTS_TARGET): LDFLAGS+=-lpthread
$(REPL_CLI_TARGET): LDFLAGS+=-lpthread $(REPL_CLI_TARGET): LDFLAGS+=-lpthread
fuzz-proto fuzz-prototest: LDFLAGS+=build/libprotobuf-mutator/src/libfuzzer/libprotobuf-mutator-libfuzzer.a build/libprotobuf-mutator/src/libprotobuf-mutator.a -lprotobuf fuzz-proto fuzz-prototest: LDFLAGS+=build/libprotobuf-mutator/src/libfuzzer/libprotobuf-mutator-libfuzzer.a build/libprotobuf-mutator/src/libprotobuf-mutator.a $(LPROTOBUF)
# pseudo targets # pseudo targets
.PHONY: all test clean coverage format luau-size aliases .PHONY: all test clean coverage format luau-size aliases
@ -199,7 +210,7 @@ $(BUILD)/%.c.o: %.c
# protobuf fuzzer setup # protobuf fuzzer setup
fuzz/luau.pb.cpp: fuzz/luau.proto build/libprotobuf-mutator fuzz/luau.pb.cpp: fuzz/luau.proto build/libprotobuf-mutator
cd fuzz && protoc luau.proto --cpp_out=. cd fuzz && $(EPROTOC) luau.proto --cpp_out=.
mv fuzz/luau.pb.cc fuzz/luau.pb.cpp mv fuzz/luau.pb.cc fuzz/luau.pb.cpp
$(BUILD)/fuzz/proto.cpp.o: fuzz/luau.pb.cpp $(BUILD)/fuzz/proto.cpp.o: fuzz/luau.pb.cpp
@ -207,7 +218,7 @@ $(BUILD)/fuzz/protoprint.cpp.o: fuzz/luau.pb.cpp
build/libprotobuf-mutator: build/libprotobuf-mutator:
git clone https://github.com/google/libprotobuf-mutator build/libprotobuf-mutator git clone https://github.com/google/libprotobuf-mutator build/libprotobuf-mutator
CXX= cmake -S build/libprotobuf-mutator -B build/libprotobuf-mutator -D CMAKE_BUILD_TYPE=Release -D LIB_PROTO_MUTATOR_TESTING=OFF CXX= cmake -S build/libprotobuf-mutator -B build/libprotobuf-mutator $(DPROTOBUF)
make -C build/libprotobuf-mutator -j8 make -C build/libprotobuf-mutator -j8
# picks up include dependencies for all object files # picks up include dependencies for all object files

View file

@ -401,13 +401,15 @@ struct lua_Debug
const char* name; // (n) const char* name; // (n)
const char* what; // (s) `Lua', `C', `main', `tail' const char* what; // (s) `Lua', `C', `main', `tail'
const char* source; // (s) const char* source; // (s)
const char* short_src; // (s)
int linedefined; // (s) int linedefined; // (s)
int currentline; // (l) int currentline; // (l)
unsigned char nupvals; // (u) number of upvalues unsigned char nupvals; // (u) number of upvalues
unsigned char nparams; // (a) number of parameters unsigned char nparams; // (a) number of parameters
char isvararg; // (a) char isvararg; // (a)
char short_src[LUA_IDSIZE]; // (s)
void* userdata; // only valid in luau_callhook void* userdata; // only valid in luau_callhook
char ssbuf[LUA_IDSIZE];
}; };
// }====================================================================== // }======================================================================

View file

@ -12,6 +12,8 @@
#include <string.h> #include <string.h>
#include <stdio.h> #include <stdio.h>
LUAU_FASTFLAGVARIABLE(LuauFasterGetInfo, false)
static const char* getfuncname(Closure* f); static const char* getfuncname(Closure* f);
static int currentpc(lua_State* L, CallInfo* ci) static int currentpc(lua_State* L, CallInfo* ci)
@ -89,9 +91,9 @@ const char* lua_setlocal(lua_State* L, int level, int n)
return name; return name;
} }
static int auxgetinfo(lua_State* L, const char* what, lua_Debug* ar, Closure* f, CallInfo* ci) static Closure* auxgetinfo(lua_State* L, const char* what, lua_Debug* ar, Closure* f, CallInfo* ci)
{ {
int status = 1; Closure* cl = NULL;
for (; *what; what++) for (; *what; what++)
{ {
switch (*what) switch (*what)
@ -103,14 +105,23 @@ static int auxgetinfo(lua_State* L, const char* what, lua_Debug* ar, Closure* f,
ar->source = "=[C]"; ar->source = "=[C]";
ar->what = "C"; ar->what = "C";
ar->linedefined = -1; ar->linedefined = -1;
if (FFlag::LuauFasterGetInfo)
ar->short_src = "[C]";
} }
else else
{ {
ar->source = getstr(f->l.p->source); TString* source = f->l.p->source;
ar->source = getstr(source);
ar->what = "Lua"; ar->what = "Lua";
ar->linedefined = f->l.p->linedefined; ar->linedefined = f->l.p->linedefined;
if (FFlag::LuauFasterGetInfo)
ar->short_src = luaO_chunkid(ar->ssbuf, sizeof(ar->ssbuf), getstr(source), source->len);
}
if (!FFlag::LuauFasterGetInfo)
{
luaO_chunkid(ar->ssbuf, LUA_IDSIZE, ar->source, 0);
ar->short_src = ar->ssbuf;
} }
luaO_chunkid(ar->short_src, ar->source, LUA_IDSIZE);
break; break;
} }
case 'l': case 'l':
@ -150,10 +161,15 @@ static int auxgetinfo(lua_State* L, const char* what, lua_Debug* ar, Closure* f,
ar->name = ci ? getfuncname(ci_func(ci)) : getfuncname(f); ar->name = ci ? getfuncname(ci_func(ci)) : getfuncname(f);
break; break;
} }
case 'f':
{
cl = f;
break;
}
default:; default:;
} }
} }
return status; return cl;
} }
int lua_stackdepth(lua_State* L) int lua_stackdepth(lua_State* L)
@ -163,7 +179,6 @@ int lua_stackdepth(lua_State* L)
int lua_getinfo(lua_State* L, int level, const char* what, lua_Debug* ar) int lua_getinfo(lua_State* L, int level, const char* what, lua_Debug* ar)
{ {
int status = 0;
Closure* f = NULL; Closure* f = NULL;
CallInfo* ci = NULL; CallInfo* ci = NULL;
if (level < 0) if (level < 0)
@ -180,15 +195,28 @@ int lua_getinfo(lua_State* L, int level, const char* what, lua_Debug* ar)
} }
if (f) if (f)
{ {
status = auxgetinfo(L, what, ar, f, ci); if (FFlag::LuauFasterGetInfo)
if (strchr(what, 'f'))
{ {
luaC_threadbarrier(L); // auxgetinfo fills ar and optionally requests to put closure on stack
setclvalue(L, L->top, f); if (Closure* fcl = auxgetinfo(L, what, ar, f, ci))
incr_top(L); {
luaC_threadbarrier(L);
setclvalue(L, L->top, fcl);
incr_top(L);
}
}
else
{
auxgetinfo(L, what, ar, f, ci);
if (strchr(what, 'f'))
{
luaC_threadbarrier(L);
setclvalue(L, L->top, f);
incr_top(L);
}
} }
} }
return status; return f ? 1 : 0;
} }
static const char* getfuncname(Closure* cl) static const char* getfuncname(Closure* cl)
@ -284,10 +312,11 @@ static void pusherror(lua_State* L, const char* msg)
CallInfo* ci = L->ci; CallInfo* ci = L->ci;
if (isLua(ci)) if (isLua(ci))
{ {
char buff[LUA_IDSIZE]; // add file:line information TString* source = getluaproto(ci)->source;
luaO_chunkid(buff, getstr(getluaproto(ci)->source), LUA_IDSIZE); char chunkbuf[LUA_IDSIZE]; // add file:line information
const char* chunkid = luaO_chunkid(chunkbuf, sizeof(chunkbuf), getstr(source), source->len);
int line = currentline(L, ci); int line = currentline(L, ci);
luaO_pushfstring(L, "%s:%d: %s", buff, line, msg); luaO_pushfstring(L, "%s:%d: %s", chunkid, line, msg);
} }
else else
{ {

View file

@ -13,8 +13,6 @@
#include <string.h> #include <string.h>
LUAU_FASTFLAGVARIABLE(LuauBetterThreadMark, false)
/* /*
* Luau uses an incremental non-generational non-moving mark&sweep garbage collector. * Luau uses an incremental non-generational non-moving mark&sweep garbage collector.
* *
@ -473,54 +471,25 @@ static size_t propagatemark(global_State* g)
bool active = th->isactive || th == th->global->mainthread; bool active = th->isactive || th == th->global->mainthread;
if (FFlag::LuauBetterThreadMark) traversestack(g, th);
// active threads will need to be rescanned later to mark new stack writes so we mark them gray again
if (active)
{ {
traversestack(g, th); th->gclist = g->grayagain;
g->grayagain = o;
// active threads will need to be rescanned later to mark new stack writes so we mark them gray again black2gray(o);
if (active)
{
th->gclist = g->grayagain;
g->grayagain = o;
black2gray(o);
}
// the stack needs to be cleared after the last modification of the thread state before sweep begins
// if the thread is inactive, we might not see the thread in this cycle so we must clear it now
if (!active || g->gcstate == GCSatomic)
clearstack(th);
// we could shrink stack at any time but we opt to do it during initial mark to do that just once per cycle
if (g->gcstate == GCSpropagate)
shrinkstack(th);
} }
else
{
// TODO: Refactor this logic!
if (!active && g->gcstate == GCSpropagate)
{
traversestack(g, th);
clearstack(th);
}
else
{
th->gclist = g->grayagain;
g->grayagain = o;
black2gray(o); // the stack needs to be cleared after the last modification of the thread state before sweep begins
// if the thread is inactive, we might not see the thread in this cycle so we must clear it now
if (!active || g->gcstate == GCSatomic)
clearstack(th);
traversestack(g, th); // we could shrink stack at any time but we opt to do it during initial mark to do that just once per cycle
if (g->gcstate == GCSpropagate)
// final traversal? shrinkstack(th);
if (g->gcstate == GCSatomic)
clearstack(th);
}
// we could shrink stack at any time but we opt to skip it during atomic since it's redundant to do that more than once per cycle
if (g->gcstate != GCSatomic)
shrinkstack(th);
}
return sizeof(lua_State) + sizeof(TValue) * th->stacksize + sizeof(CallInfo) * th->size_ci; return sizeof(lua_State) + sizeof(TValue) * th->stacksize + sizeof(CallInfo) * th->size_ci;
} }

View file

@ -15,6 +15,8 @@
LUAU_FASTFLAG(LuauFasterGetInfo)
const TValue luaO_nilobject_ = {{NULL}, {0}, LUA_TNIL}; const TValue luaO_nilobject_ = {{NULL}, {0}, LUA_TNIL};
int luaO_log2(unsigned int x) int luaO_log2(unsigned int x)
@ -117,44 +119,68 @@ const char* luaO_pushfstring(lua_State* L, const char* fmt, ...)
return msg; return msg;
} }
void luaO_chunkid(char* out, const char* source, size_t bufflen) const char* luaO_chunkid(char* buf, size_t buflen, const char* source, size_t srclen)
{ {
if (*source == '=') if (*source == '=')
{ {
source++; // skip the `=' if (FFlag::LuauFasterGetInfo)
size_t srclen = strlen(source); {
size_t dstlen = srclen < bufflen ? srclen : bufflen - 1; if (srclen <= buflen)
memcpy(out, source, dstlen); return source + 1;
out[dstlen] = '\0'; // truncate the part after =
memcpy(buf, source + 1, buflen - 1);
buf[buflen - 1] = '\0';
}
else
{
source++; // skip the `='
size_t len = strlen(source);
size_t dstlen = len < buflen ? len : buflen - 1;
memcpy(buf, source, dstlen);
buf[dstlen] = '\0';
}
} }
else if (*source == '@') else if (*source == '@')
{ {
size_t l; if (FFlag::LuauFasterGetInfo)
source++; // skip the `@'
bufflen -= sizeof("...");
l = strlen(source);
strcpy(out, "");
if (l > bufflen)
{ {
source += (l - bufflen); // get last part of file name if (srclen <= buflen)
strcat(out, "..."); return source + 1;
} // truncate the part after @
strcat(out, source); memcpy(buf, "...", 3);
} memcpy(buf + 3, source + srclen - (buflen - 4), buflen - 4);
else buf[buflen - 1] = '\0';
{ // out = [string "string"]
size_t len = strcspn(source, "\n\r"); // stop at first newline
bufflen -= sizeof("[string \"...\"]");
if (len > bufflen)
len = bufflen;
strcpy(out, "[string \"");
if (source[len] != '\0')
{ // must truncate?
strncat(out, source, len);
strcat(out, "...");
} }
else else
strcat(out, source); {
strcat(out, "\"]"); size_t l;
source++; // skip the `@'
buflen -= sizeof("...");
l = strlen(source);
strcpy(buf, "");
if (l > buflen)
{
source += (l - buflen); // get last part of file name
strcat(buf, "...");
}
strcat(buf, source);
}
} }
else
{ // buf = [string "string"]
size_t len = strcspn(source, "\n\r"); // stop at first newline
buflen -= sizeof("[string \"...\"]");
if (len > buflen)
len = buflen;
strcpy(buf, "[string \"");
if (source[len] != '\0')
{ // must truncate?
strncat(buf, source, len);
strcat(buf, "...");
}
else
strcat(buf, source);
strcat(buf, "\"]");
}
return buf;
} }

View file

@ -460,4 +460,4 @@ LUAI_FUNC int luaO_rawequalKey(const TKey* t1, const TValue* t2);
LUAI_FUNC int luaO_str2d(const char* s, double* result); LUAI_FUNC int luaO_str2d(const char* s, double* result);
LUAI_FUNC const char* luaO_pushvfstring(lua_State* L, const char* fmt, va_list argp); LUAI_FUNC const char* luaO_pushvfstring(lua_State* L, const char* fmt, va_list argp);
LUAI_FUNC const char* luaO_pushfstring(lua_State* L, const char* fmt, ...); LUAI_FUNC const char* luaO_pushfstring(lua_State* L, const char* fmt, ...);
LUAI_FUNC void luaO_chunkid(char* out, const char* source, size_t len); LUAI_FUNC const char* luaO_chunkid(char* buf, size_t buflen, const char* source, size_t srclen);

View file

@ -781,6 +781,7 @@ reentry:
default: default:
LUAU_ASSERT(!"Unknown upvalue capture type"); LUAU_ASSERT(!"Unknown upvalue capture type");
LUAU_UNREACHABLE(); // improves switch() codegen by eliding opcode bounds checks
} }
} }
@ -1184,7 +1185,9 @@ reentry:
// slow path after switch() // slow path after switch()
break; break;
default:; default:
LUAU_ASSERT(!"Unknown value type");
LUAU_UNREACHABLE(); // improves switch() codegen by eliding opcode bounds checks
} }
// slow-path: tables with metatables and userdata values // slow-path: tables with metatables and userdata values
@ -1296,7 +1299,9 @@ reentry:
// slow path after switch() // slow path after switch()
break; break;
default:; default:
LUAU_ASSERT(!"Unknown value type");
LUAU_UNREACHABLE(); // improves switch() codegen by eliding opcode bounds checks
} }
// slow-path: tables with metatables and userdata values // slow-path: tables with metatables and userdata values

View file

@ -148,16 +148,16 @@ int luau_load(lua_State* L, const char* chunkname, const char* data, size_t size
// 0 means the rest of the bytecode is the error message // 0 means the rest of the bytecode is the error message
if (version == 0) if (version == 0)
{ {
char chunkid[LUA_IDSIZE]; char chunkbuf[LUA_IDSIZE];
luaO_chunkid(chunkid, chunkname, LUA_IDSIZE); const char* chunkid = luaO_chunkid(chunkbuf, sizeof(chunkbuf), chunkname, strlen(chunkname));
lua_pushfstring(L, "%s%.*s", chunkid, int(size - offset), data + offset); lua_pushfstring(L, "%s%.*s", chunkid, int(size - offset), data + offset);
return 1; return 1;
} }
if (version < LBC_VERSION_MIN || version > LBC_VERSION_MAX) if (version < LBC_VERSION_MIN || version > LBC_VERSION_MAX)
{ {
char chunkid[LUA_IDSIZE]; char chunkbuf[LUA_IDSIZE];
luaO_chunkid(chunkid, chunkname, LUA_IDSIZE); const char* chunkid = luaO_chunkid(chunkbuf, sizeof(chunkbuf), chunkname, strlen(chunkname));
lua_pushfstring(L, "%s: bytecode version mismatch (expected [%d..%d], got %d)", chunkid, LBC_VERSION_MIN, LBC_VERSION_MAX, version); lua_pushfstring(L, "%s: bytecode version mismatch (expected [%d..%d], got %d)", chunkid, LBC_VERSION_MIN, LBC_VERSION_MAX, version);
return 1; return 1;
} }

View file

@ -220,8 +220,13 @@ int luaV_strcmp(const TString* ls, const TString* rs)
return 0; return 0;
const char* l = getstr(ls); const char* l = getstr(ls);
size_t ll = ls->len;
const char* r = getstr(rs); const char* r = getstr(rs);
// always safe to read one character because even empty strings are nul terminated
if (*l != *r)
return uint8_t(*l) - uint8_t(*r);
size_t ll = ls->len;
size_t lr = rs->len; size_t lr = rs->len;
size_t lmin = ll < lr ? ll : lr; size_t lmin = ll < lr ? ll : lr;

View file

@ -240,12 +240,12 @@ TEST_CASE_FIXTURE(AssemblyBuilderX64Fixture, "FormsOfLea")
TEST_CASE_FIXTURE(AssemblyBuilderX64Fixture, "FormsOfAbsoluteJumps") TEST_CASE_FIXTURE(AssemblyBuilderX64Fixture, "FormsOfAbsoluteJumps")
{ {
SINGLE_COMPARE(jmp(rax), 0x48, 0xff, 0xe0); SINGLE_COMPARE(jmp(rax), 0xff, 0xe0);
SINGLE_COMPARE(jmp(r14), 0x49, 0xff, 0xe6); SINGLE_COMPARE(jmp(r14), 0x41, 0xff, 0xe6);
SINGLE_COMPARE(jmp(qword[r14 + rdx * 4]), 0x49, 0xff, 0x24, 0x96); SINGLE_COMPARE(jmp(qword[r14 + rdx * 4]), 0x41, 0xff, 0x24, 0x96);
SINGLE_COMPARE(call(rax), 0x48, 0xff, 0xd0); SINGLE_COMPARE(call(rax), 0xff, 0xd0);
SINGLE_COMPARE(call(r14), 0x49, 0xff, 0xd6); SINGLE_COMPARE(call(r14), 0x41, 0xff, 0xd6);
SINGLE_COMPARE(call(qword[r14 + rdx * 4]), 0x49, 0xff, 0x14, 0x96); SINGLE_COMPARE(call(qword[r14 + rdx * 4]), 0x41, 0xff, 0x14, 0x96);
} }
TEST_CASE_FIXTURE(AssemblyBuilderX64Fixture, "FormsOfImul") TEST_CASE_FIXTURE(AssemblyBuilderX64Fixture, "FormsOfImul")

View file

@ -2955,8 +2955,6 @@ local abc = b@1
TEST_CASE_FIXTURE(ACFixture, "no_incompatible_self_calls_on_class") TEST_CASE_FIXTURE(ACFixture, "no_incompatible_self_calls_on_class")
{ {
ScopedFastFlag selfCallAutocompleteFix3{"LuauSelfCallAutocompleteFix3", true};
loadDefinition(R"( loadDefinition(R"(
declare class Foo declare class Foo
function one(self): number function one(self): number
@ -2995,8 +2993,6 @@ t.@1
TEST_CASE_FIXTURE(ACFixture, "do_compatible_self_calls") TEST_CASE_FIXTURE(ACFixture, "do_compatible_self_calls")
{ {
ScopedFastFlag selfCallAutocompleteFix3{"LuauSelfCallAutocompleteFix3", true};
check(R"( check(R"(
local t = {} local t = {}
function t:m() end function t:m() end
@ -3011,8 +3007,6 @@ t:@1
TEST_CASE_FIXTURE(ACFixture, "no_incompatible_self_calls") TEST_CASE_FIXTURE(ACFixture, "no_incompatible_self_calls")
{ {
ScopedFastFlag selfCallAutocompleteFix3{"LuauSelfCallAutocompleteFix3", true};
check(R"( check(R"(
local t = {} local t = {}
function t.m() end function t.m() end
@ -3027,8 +3021,6 @@ t:@1
TEST_CASE_FIXTURE(ACFixture, "no_incompatible_self_calls_2") TEST_CASE_FIXTURE(ACFixture, "no_incompatible_self_calls_2")
{ {
ScopedFastFlag selfCallAutocompleteFix3{"LuauSelfCallAutocompleteFix3", true};
check(R"( check(R"(
local f: (() -> number) & ((number) -> number) = function(x: number?) return 2 end local f: (() -> number) & ((number) -> number) = function(x: number?) return 2 end
local t = {} local t = {}
@ -3059,8 +3051,6 @@ t:@1
TEST_CASE_FIXTURE(ACFixture, "no_wrong_compatible_self_calls_with_generics") TEST_CASE_FIXTURE(ACFixture, "no_wrong_compatible_self_calls_with_generics")
{ {
ScopedFastFlag selfCallAutocompleteFix3{"LuauSelfCallAutocompleteFix3", true};
check(R"( check(R"(
local t = {} local t = {}
function t.m<T>(a: T) end function t.m<T>(a: T) end
@ -3076,8 +3066,6 @@ t:@1
TEST_CASE_FIXTURE(ACFixture, "string_prim_self_calls_are_fine") TEST_CASE_FIXTURE(ACFixture, "string_prim_self_calls_are_fine")
{ {
ScopedFastFlag selfCallAutocompleteFix3{"LuauSelfCallAutocompleteFix3", true};
check(R"( check(R"(
local s = "hello" local s = "hello"
s:@1 s:@1
@ -3095,8 +3083,6 @@ s:@1
TEST_CASE_FIXTURE(ACFixture, "string_prim_non_self_calls_are_avoided") TEST_CASE_FIXTURE(ACFixture, "string_prim_non_self_calls_are_avoided")
{ {
ScopedFastFlag selfCallAutocompleteFix3{"LuauSelfCallAutocompleteFix3", true};
check(R"( check(R"(
local s = "hello" local s = "hello"
s.@1 s.@1
@ -3112,8 +3098,6 @@ s.@1
TEST_CASE_FIXTURE(ACBuiltinsFixture, "library_non_self_calls_are_fine") TEST_CASE_FIXTURE(ACBuiltinsFixture, "library_non_self_calls_are_fine")
{ {
ScopedFastFlag selfCallAutocompleteFix3{"LuauSelfCallAutocompleteFix3", true};
check(R"( check(R"(
string.@1 string.@1
)"); )");
@ -3143,8 +3127,6 @@ table.@1
TEST_CASE_FIXTURE(ACBuiltinsFixture, "library_self_calls_are_invalid") TEST_CASE_FIXTURE(ACBuiltinsFixture, "library_self_calls_are_invalid")
{ {
ScopedFastFlag selfCallAutocompleteFix3{"LuauSelfCallAutocompleteFix3", true};
check(R"( check(R"(
string:@1 string:@1
)"); )");

View file

@ -96,12 +96,12 @@ TEST_CASE("CodeAllocationWithUnwindCallbacks")
data.resize(8); data.resize(8);
allocator.context = &info; allocator.context = &info;
allocator.createBlockUnwindInfo = [](void* context, uint8_t* block, size_t blockSize, size_t& unwindDataSizeInBlock) -> void* { allocator.createBlockUnwindInfo = [](void* context, uint8_t* block, size_t blockSize, size_t& beginOffset) -> void* {
Info& info = *(Info*)context; Info& info = *(Info*)context;
CHECK(info.unwind.size() == 8); CHECK(info.unwind.size() == 8);
memcpy(block, info.unwind.data(), info.unwind.size()); memcpy(block, info.unwind.data(), info.unwind.size());
unwindDataSizeInBlock = 8; beginOffset = 8;
info.block = block; info.block = block;
@ -194,10 +194,12 @@ TEST_CASE("Dwarf2UnwindCodesX64")
// Windows x64 ABI // Windows x64 ABI
constexpr RegisterX64 rArg1 = rcx; constexpr RegisterX64 rArg1 = rcx;
constexpr RegisterX64 rArg2 = rdx; constexpr RegisterX64 rArg2 = rdx;
constexpr RegisterX64 rArg3 = r8;
#else #else
// System V AMD64 ABI // System V AMD64 ABI
constexpr RegisterX64 rArg1 = rdi; constexpr RegisterX64 rArg1 = rdi;
constexpr RegisterX64 rArg2 = rsi; constexpr RegisterX64 rArg2 = rsi;
constexpr RegisterX64 rArg3 = rdx;
#endif #endif
constexpr RegisterX64 rNonVol1 = r12; constexpr RegisterX64 rNonVol1 = r12;
@ -313,6 +315,119 @@ TEST_CASE("GeneratedCodeExecutionWithThrow")
} }
} }
TEST_CASE("GeneratedCodeExecutionWithThrowOutsideTheGate")
{
AssemblyBuilderX64 build(/* logText= */ false);
#if defined(_WIN32)
std::unique_ptr<UnwindBuilder> unwind = std::make_unique<UnwindBuilderWin>();
#else
std::unique_ptr<UnwindBuilder> unwind = std::make_unique<UnwindBuilderDwarf2>();
#endif
unwind->start();
// Prologue (some of these registers don't have to be saved, but we want to have a big prologue)
build.push(r10);
unwind->save(r10);
build.push(r11);
unwind->save(r11);
build.push(r12);
unwind->save(r12);
build.push(r13);
unwind->save(r13);
build.push(r14);
unwind->save(r14);
build.push(r15);
unwind->save(r15);
build.push(rbp);
unwind->save(rbp);
int stackSize = 64;
int localsSize = 16;
build.sub(rsp, stackSize + localsSize);
unwind->allocStack(stackSize + localsSize);
build.lea(rbp, qword[rsp + stackSize]);
unwind->setupFrameReg(rbp, stackSize);
unwind->finish();
size_t prologueSize = build.setLabel().location;
// Body
build.mov(rax, rArg1);
build.mov(rArg1, 25);
build.jmp(rax);
Label returnOffset = build.setLabel();
// Epilogue
build.lea(rsp, qword[rbp + localsSize]);
build.pop(rbp);
build.pop(r15);
build.pop(r14);
build.pop(r13);
build.pop(r12);
build.pop(r11);
build.pop(r10);
build.ret();
build.finalize();
size_t blockSize = 4096; // Force allocate to create a new block each time
size_t maxTotalSize = 1024 * 1024;
CodeAllocator allocator(blockSize, maxTotalSize);
allocator.context = unwind.get();
allocator.createBlockUnwindInfo = createBlockUnwindInfo;
allocator.destroyBlockUnwindInfo = destroyBlockUnwindInfo;
uint8_t* nativeData1;
size_t sizeNativeData1;
uint8_t* nativeEntry1;
REQUIRE(
allocator.allocate(build.data.data(), build.data.size(), build.code.data(), build.code.size(), nativeData1, sizeNativeData1, nativeEntry1));
REQUIRE(nativeEntry1);
// Now we set the offset at the begining so that functions in new blocks will not overlay the locations
// specified by the unwind information of the entry function
unwind->setBeginOffset(prologueSize);
using FunctionType = int64_t(void*, void (*)(int64_t), void*);
FunctionType* f = (FunctionType*)nativeEntry1;
uint8_t* nativeExit = nativeEntry1 + returnOffset.location;
AssemblyBuilderX64 build2(/* logText= */ false);
build2.mov(r12, rArg3);
build2.call(rArg2);
build2.jmp(r12);
build2.finalize();
uint8_t* nativeData2;
size_t sizeNativeData2;
uint8_t* nativeEntry2;
REQUIRE(allocator.allocate(
build2.data.data(), build2.data.size(), build2.code.data(), build2.code.size(), nativeData2, sizeNativeData2, nativeEntry2));
REQUIRE(nativeEntry2);
// To simplify debugging, CHECK_THROWS_WITH_AS is not used here
try
{
f(nativeEntry2, throwing, nativeExit);
}
catch (const std::runtime_error& error)
{
CHECK(strcmp(error.what(), "testing") == 0);
}
REQUIRE(nativeEntry2);
}
#endif #endif
TEST_SUITE_END(); TEST_SUITE_END();

View file

@ -28,8 +28,11 @@ TEST_CASE_FIXTURE(ConstraintGraphBuilderFixture, "hello")
cgb.visit(block); cgb.visit(block);
NotNull<Scope> rootScope{cgb.rootScope}; NotNull<Scope> rootScope{cgb.rootScope};
InternalErrorReporter iceHandler;
UnifierSharedState sharedState{&iceHandler};
Normalizer normalizer{&arena, singletonTypes, NotNull{&sharedState}};
NullModuleResolver resolver; NullModuleResolver resolver;
ConstraintSolver cs{&arena, singletonTypes, rootScope, "MainModule", NotNull(&resolver), {}, &logger}; ConstraintSolver cs{NotNull{&normalizer}, rootScope, "MainModule", NotNull(&resolver), {}, &logger};
cs.run(); cs.run();
@ -49,9 +52,11 @@ TEST_CASE_FIXTURE(ConstraintGraphBuilderFixture, "generic_function")
cgb.visit(block); cgb.visit(block);
NotNull<Scope> rootScope{cgb.rootScope}; NotNull<Scope> rootScope{cgb.rootScope};
InternalErrorReporter iceHandler;
UnifierSharedState sharedState{&iceHandler};
Normalizer normalizer{&arena, singletonTypes, NotNull{&sharedState}};
NullModuleResolver resolver; NullModuleResolver resolver;
ConstraintSolver cs{&arena, singletonTypes, rootScope, "MainModule", NotNull(&resolver), {}, &logger}; ConstraintSolver cs{NotNull{&normalizer}, rootScope, "MainModule", NotNull(&resolver), {}, &logger};
cs.run(); cs.run();
TypeId idType = requireBinding(rootScope, "id"); TypeId idType = requireBinding(rootScope, "id");
@ -79,7 +84,10 @@ TEST_CASE_FIXTURE(ConstraintGraphBuilderFixture, "proper_let_generalization")
ToStringOptions opts; ToStringOptions opts;
NullModuleResolver resolver; NullModuleResolver resolver;
ConstraintSolver cs{&arena, singletonTypes, rootScope, "MainModule", NotNull(&resolver), {}, &logger}; InternalErrorReporter iceHandler;
UnifierSharedState sharedState{&iceHandler};
Normalizer normalizer{&arena, singletonTypes, NotNull{&sharedState}};
ConstraintSolver cs{NotNull{&normalizer}, rootScope, "MainModule", NotNull(&resolver), {}, &logger};
cs.run(); cs.run();

View file

@ -22,6 +22,8 @@ LUAU_FASTFLAG(DebugLuauDeferredConstraintResolution);
LUAU_FASTFLAG(LuauUnknownAndNeverType) LUAU_FASTFLAG(LuauUnknownAndNeverType)
LUAU_FASTFLAG(LuauReportShadowedTypeAlias) LUAU_FASTFLAG(LuauReportShadowedTypeAlias)
extern std::optional<unsigned> randomSeed; // tests/main.cpp
namespace Luau namespace Luau
{ {
@ -90,7 +92,7 @@ std::optional<std::string> TestFileResolver::getEnvironmentForModule(const Modul
Fixture::Fixture(bool freeze, bool prepareAutocomplete) Fixture::Fixture(bool freeze, bool prepareAutocomplete)
: sff_DebugLuauFreezeArena("DebugLuauFreezeArena", freeze) : sff_DebugLuauFreezeArena("DebugLuauFreezeArena", freeze)
, frontend(&fileResolver, &configResolver, {/* retainFullTypeGraphs= */ true}) , frontend(&fileResolver, &configResolver, {/* retainFullTypeGraphs= */ true, /* forAutocomplete */ false, /* randomConstraintResolutionSeed */ randomSeed})
, typeChecker(frontend.typeChecker) , typeChecker(frontend.typeChecker)
, singletonTypes(frontend.singletonTypes) , singletonTypes(frontend.singletonTypes)
{ {

View file

@ -21,14 +21,14 @@ end
return math.max(fib(5), 1) return math.max(fib(5), 1)
)"); )");
CHECK_EQ(result.warnings.size(), 0); REQUIRE(0 == result.warnings.size());
} }
TEST_CASE_FIXTURE(Fixture, "UnknownGlobal") TEST_CASE_FIXTURE(Fixture, "UnknownGlobal")
{ {
LintResult result = lint("--!nocheck\nreturn foo"); LintResult result = lint("--!nocheck\nreturn foo");
REQUIRE_EQ(result.warnings.size(), 1); REQUIRE(1 == result.warnings.size());
CHECK_EQ(result.warnings[0].text, "Unknown global 'foo'"); CHECK_EQ(result.warnings[0].text, "Unknown global 'foo'");
} }
@ -39,7 +39,7 @@ TEST_CASE_FIXTURE(Fixture, "DeprecatedGlobal")
LintResult result = lintTyped("Wait(5)"); LintResult result = lintTyped("Wait(5)");
REQUIRE_EQ(result.warnings.size(), 1); REQUIRE(1 == result.warnings.size());
CHECK_EQ(result.warnings[0].text, "Global 'Wait' is deprecated, use 'wait' instead"); CHECK_EQ(result.warnings[0].text, "Global 'Wait' is deprecated, use 'wait' instead");
} }
@ -53,7 +53,7 @@ TEST_CASE_FIXTURE(Fixture, "DeprecatedGlobalNoReplacement")
LintResult result = lintTyped("Version()"); LintResult result = lintTyped("Version()");
REQUIRE_EQ(result.warnings.size(), 1); REQUIRE(1 == result.warnings.size());
CHECK_EQ(result.warnings[0].text, "Global 'Version' is deprecated"); CHECK_EQ(result.warnings[0].text, "Global 'Version' is deprecated");
} }
@ -64,7 +64,7 @@ local _ = 5
return _ return _
)"); )");
CHECK_EQ(result.warnings.size(), 1); REQUIRE(1 == result.warnings.size());
CHECK_EQ(result.warnings[0].text, "Placeholder value '_' is read here; consider using a named variable"); CHECK_EQ(result.warnings[0].text, "Placeholder value '_' is read here; consider using a named variable");
} }
@ -75,7 +75,7 @@ _ = 5
print(_) print(_)
)"); )");
CHECK_EQ(result.warnings.size(), 1); REQUIRE(1 == result.warnings.size());
CHECK_EQ(result.warnings[0].text, "Placeholder value '_' is read here; consider using a named variable"); CHECK_EQ(result.warnings[0].text, "Placeholder value '_' is read here; consider using a named variable");
} }
@ -86,7 +86,7 @@ local _ = 5
_ = 6 _ = 6
)"); )");
CHECK_EQ(result.warnings.size(), 0); REQUIRE(0 == result.warnings.size());
} }
TEST_CASE_FIXTURE(BuiltinsFixture, "BuiltinGlobalWrite") TEST_CASE_FIXTURE(BuiltinsFixture, "BuiltinGlobalWrite")
@ -100,7 +100,7 @@ end
assert(5) assert(5)
)"); )");
CHECK_EQ(result.warnings.size(), 2); REQUIRE(2 == result.warnings.size());
CHECK_EQ(result.warnings[0].text, "Built-in global 'math' is overwritten here; consider using a local or changing the name"); CHECK_EQ(result.warnings[0].text, "Built-in global 'math' is overwritten here; consider using a local or changing the name");
CHECK_EQ(result.warnings[1].text, "Built-in global 'assert' is overwritten here; consider using a local or changing the name"); CHECK_EQ(result.warnings[1].text, "Built-in global 'assert' is overwritten here; consider using a local or changing the name");
} }
@ -111,7 +111,7 @@ TEST_CASE_FIXTURE(Fixture, "MultilineBlock")
if true then print(1) print(2) print(3) end if true then print(1) print(2) print(3) end
)"); )");
CHECK_EQ(result.warnings.size(), 1); REQUIRE(1 == result.warnings.size());
CHECK_EQ(result.warnings[0].text, "A new statement is on the same line; add semi-colon on previous statement to silence"); CHECK_EQ(result.warnings[0].text, "A new statement is on the same line; add semi-colon on previous statement to silence");
} }
@ -121,7 +121,7 @@ TEST_CASE_FIXTURE(Fixture, "MultilineBlockSemicolonsWhitelisted")
print(1); print(2); print(3) print(1); print(2); print(3)
)"); )");
CHECK(result.warnings.empty()); REQUIRE(0 == result.warnings.size());
} }
TEST_CASE_FIXTURE(Fixture, "MultilineBlockMissedSemicolon") TEST_CASE_FIXTURE(Fixture, "MultilineBlockMissedSemicolon")
@ -130,7 +130,7 @@ TEST_CASE_FIXTURE(Fixture, "MultilineBlockMissedSemicolon")
print(1); print(2) print(3) print(1); print(2) print(3)
)"); )");
CHECK_EQ(result.warnings.size(), 1); REQUIRE(1 == result.warnings.size());
CHECK_EQ(result.warnings[0].text, "A new statement is on the same line; add semi-colon on previous statement to silence"); CHECK_EQ(result.warnings[0].text, "A new statement is on the same line; add semi-colon on previous statement to silence");
} }
@ -142,7 +142,7 @@ local _x do
end end
)"); )");
CHECK_EQ(result.warnings.size(), 0); REQUIRE(0 == result.warnings.size());
} }
TEST_CASE_FIXTURE(Fixture, "ConfusingIndentation") TEST_CASE_FIXTURE(Fixture, "ConfusingIndentation")
@ -152,7 +152,7 @@ print(math.max(1,
2)) 2))
)"); )");
CHECK_EQ(result.warnings.size(), 1); REQUIRE(1 == result.warnings.size());
CHECK_EQ(result.warnings[0].text, "Statement spans multiple lines; use indentation to silence"); CHECK_EQ(result.warnings[0].text, "Statement spans multiple lines; use indentation to silence");
} }
@ -167,7 +167,7 @@ end
return bar() return bar()
)"); )");
CHECK_EQ(result.warnings.size(), 1); REQUIRE(1 == result.warnings.size());
CHECK_EQ(result.warnings[0].text, "Global 'foo' is only used in the enclosing function 'bar'; consider changing it to local"); CHECK_EQ(result.warnings[0].text, "Global 'foo' is only used in the enclosing function 'bar'; consider changing it to local");
} }
@ -188,7 +188,7 @@ end
return bar() + baz() return bar() + baz()
)"); )");
REQUIRE_EQ(result.warnings.size(), 1); REQUIRE(1 == result.warnings.size());
CHECK_EQ(result.warnings[0].text, "Global 'foo' is never read before being written. Consider changing it to local"); CHECK_EQ(result.warnings[0].text, "Global 'foo' is never read before being written. Consider changing it to local");
} }
@ -213,7 +213,7 @@ end
return bar() + baz() + read() return bar() + baz() + read()
)"); )");
CHECK_EQ(result.warnings.size(), 0); REQUIRE(0 == result.warnings.size());
} }
TEST_CASE_FIXTURE(Fixture, "GlobalAsLocalWithConditional") TEST_CASE_FIXTURE(Fixture, "GlobalAsLocalWithConditional")
@ -233,7 +233,7 @@ end
return bar() + baz() return bar() + baz()
)"); )");
CHECK_EQ(result.warnings.size(), 0); REQUIRE(0 == result.warnings.size());
} }
TEST_CASE_FIXTURE(Fixture, "GlobalAsLocal3WithConditionalRead") TEST_CASE_FIXTURE(Fixture, "GlobalAsLocal3WithConditionalRead")
@ -257,7 +257,7 @@ end
return bar() + baz() + read() return bar() + baz() + read()
)"); )");
CHECK_EQ(result.warnings.size(), 0); REQUIRE(0 == result.warnings.size());
} }
TEST_CASE_FIXTURE(Fixture, "GlobalAsLocalInnerRead") TEST_CASE_FIXTURE(Fixture, "GlobalAsLocalInnerRead")
@ -275,7 +275,7 @@ function baz() bar = 0 end
return foo() + baz() return foo() + baz()
)"); )");
CHECK_EQ(result.warnings.size(), 0); REQUIRE(0 == result.warnings.size());
} }
TEST_CASE_FIXTURE(Fixture, "GlobalAsLocalMulti") TEST_CASE_FIXTURE(Fixture, "GlobalAsLocalMulti")
@ -304,7 +304,7 @@ fnA() -- prints "true", "nil"
fnB() -- prints "false", "nil" fnB() -- prints "false", "nil"
)"); )");
CHECK_EQ(result.warnings.size(), 1); REQUIRE(1 == result.warnings.size());
CHECK_EQ(result.warnings[0].text, CHECK_EQ(result.warnings[0].text,
"Global 'moreInternalLogic' is only used in the enclosing function defined at line 2; consider changing it to local"); "Global 'moreInternalLogic' is only used in the enclosing function defined at line 2; consider changing it to local");
} }
@ -319,7 +319,7 @@ local arg = 5
print(arg) print(arg)
)"); )");
CHECK_EQ(result.warnings.size(), 1); REQUIRE(1 == result.warnings.size());
CHECK_EQ(result.warnings[0].text, "Variable 'arg' shadows previous declaration at line 2"); CHECK_EQ(result.warnings[0].text, "Variable 'arg' shadows previous declaration at line 2");
} }
@ -337,7 +337,7 @@ end
return bar() return bar()
)"); )");
CHECK_EQ(result.warnings.size(), 1); REQUIRE(1 == result.warnings.size());
CHECK_EQ(result.warnings[0].text, "Variable 'global' shadows a global variable used at line 3"); CHECK_EQ(result.warnings[0].text, "Variable 'global' shadows a global variable used at line 3");
} }
@ -352,7 +352,7 @@ end
return bar() return bar()
)"); )");
CHECK_EQ(result.warnings.size(), 1); REQUIRE(1 == result.warnings.size());
CHECK_EQ(result.warnings[0].text, "Variable 'a' shadows previous declaration at line 2"); CHECK_EQ(result.warnings[0].text, "Variable 'a' shadows previous declaration at line 2");
} }
@ -372,7 +372,7 @@ end
return bar() return bar()
)"); )");
CHECK_EQ(result.warnings.size(), 2); REQUIRE(2 == result.warnings.size());
CHECK_EQ(result.warnings[0].text, "Variable 'arg' is never used; prefix with '_' to silence"); CHECK_EQ(result.warnings[0].text, "Variable 'arg' is never used; prefix with '_' to silence");
CHECK_EQ(result.warnings[1].text, "Variable 'blarg' is never used; prefix with '_' to silence"); CHECK_EQ(result.warnings[1].text, "Variable 'blarg' is never used; prefix with '_' to silence");
} }
@ -387,7 +387,7 @@ local Roact = require(game.Packages.Roact)
local _Roact = require(game.Packages.Roact) local _Roact = require(game.Packages.Roact)
)"); )");
CHECK_EQ(result.warnings.size(), 1); REQUIRE(1 == result.warnings.size());
CHECK_EQ(result.warnings[0].text, "Import 'Roact' is never used; prefix with '_' to silence"); CHECK_EQ(result.warnings[0].text, "Import 'Roact' is never used; prefix with '_' to silence");
} }
@ -412,7 +412,7 @@ end
return foo() return foo()
)"); )");
CHECK_EQ(result.warnings.size(), 2); REQUIRE(2 == result.warnings.size());
CHECK_EQ(result.warnings[0].text, "Function 'bar' is never used; prefix with '_' to silence"); CHECK_EQ(result.warnings[0].text, "Function 'bar' is never used; prefix with '_' to silence");
CHECK_EQ(result.warnings[1].text, "Function 'qux' is never used; prefix with '_' to silence"); CHECK_EQ(result.warnings[1].text, "Function 'qux' is never used; prefix with '_' to silence");
} }
@ -427,7 +427,7 @@ end
print("hi!") print("hi!")
)"); )");
CHECK_EQ(result.warnings.size(), 1); REQUIRE(1 == result.warnings.size());
CHECK_EQ(result.warnings[0].location.begin.line, 5); CHECK_EQ(result.warnings[0].location.begin.line, 5);
CHECK_EQ(result.warnings[0].text, "Unreachable code (previous statement always returns)"); CHECK_EQ(result.warnings[0].text, "Unreachable code (previous statement always returns)");
} }
@ -443,7 +443,7 @@ end
print("hi!") print("hi!")
)"); )");
CHECK_EQ(result.warnings.size(), 1); REQUIRE(1 == result.warnings.size());
CHECK_EQ(result.warnings[0].location.begin.line, 3); CHECK_EQ(result.warnings[0].location.begin.line, 3);
CHECK_EQ(result.warnings[0].text, "Unreachable code (previous statement always breaks)"); CHECK_EQ(result.warnings[0].text, "Unreachable code (previous statement always breaks)");
} }
@ -459,7 +459,7 @@ end
print("hi!") print("hi!")
)"); )");
CHECK_EQ(result.warnings.size(), 1); REQUIRE(1 == result.warnings.size());
CHECK_EQ(result.warnings[0].location.begin.line, 3); CHECK_EQ(result.warnings[0].location.begin.line, 3);
CHECK_EQ(result.warnings[0].text, "Unreachable code (previous statement always continues)"); CHECK_EQ(result.warnings[0].text, "Unreachable code (previous statement always continues)");
} }
@ -495,7 +495,7 @@ end
return { foo1, foo2, foo3 } return { foo1, foo2, foo3 }
)"); )");
CHECK_EQ(result.warnings.size(), 1); REQUIRE(1 == result.warnings.size());
CHECK_EQ(result.warnings[0].location.begin.line, 7); CHECK_EQ(result.warnings[0].location.begin.line, 7);
CHECK_EQ(result.warnings[0].text, "Unreachable code (previous statement always returns)"); CHECK_EQ(result.warnings[0].text, "Unreachable code (previous statement always returns)");
} }
@ -515,7 +515,7 @@ end
return foo1 return foo1
)"); )");
CHECK_EQ(result.warnings.size(), 0); REQUIRE(0 == result.warnings.size());
} }
TEST_CASE_FIXTURE(Fixture, "UnreachableCodeAssertFalseReturnSilent") TEST_CASE_FIXTURE(Fixture, "UnreachableCodeAssertFalseReturnSilent")
@ -532,7 +532,7 @@ end
return foo1 return foo1
)"); )");
CHECK_EQ(result.warnings.size(), 0); REQUIRE(0 == result.warnings.size());
} }
TEST_CASE_FIXTURE(Fixture, "UnreachableCodeErrorReturnNonSilentBranchy") TEST_CASE_FIXTURE(Fixture, "UnreachableCodeErrorReturnNonSilentBranchy")
@ -550,7 +550,7 @@ end
return foo1 return foo1
)"); )");
CHECK_EQ(result.warnings.size(), 1); REQUIRE(1 == result.warnings.size());
CHECK_EQ(result.warnings[0].location.begin.line, 7); CHECK_EQ(result.warnings[0].location.begin.line, 7);
CHECK_EQ(result.warnings[0].text, "Unreachable code (previous statement always errors)"); CHECK_EQ(result.warnings[0].text, "Unreachable code (previous statement always errors)");
} }
@ -571,7 +571,7 @@ end
return foo1 return foo1
)"); )");
CHECK_EQ(result.warnings.size(), 1); REQUIRE(1 == result.warnings.size());
CHECK_EQ(result.warnings[0].location.begin.line, 8); CHECK_EQ(result.warnings[0].location.begin.line, 8);
CHECK_EQ(result.warnings[0].text, "Unreachable code (previous statement always errors)"); CHECK_EQ(result.warnings[0].text, "Unreachable code (previous statement always errors)");
} }
@ -589,7 +589,7 @@ end
return foo1 return foo1
)"); )");
CHECK_EQ(result.warnings.size(), 0); REQUIRE(0 == result.warnings.size());
} }
TEST_CASE_FIXTURE(Fixture, "UnreachableCodeLoopRepeat") TEST_CASE_FIXTURE(Fixture, "UnreachableCodeLoopRepeat")
@ -605,8 +605,8 @@ end
return foo1 return foo1
)"); )");
CHECK_EQ(result.warnings.size(), // this is technically a bug, since the repeat body always returns; fixing this bug is a bit more involved than I'd like
0); // this is technically a bug, since the repeat body always returns; fixing this bug is a bit more involved than I'd like REQUIRE(0 == result.warnings.size());
} }
TEST_CASE_FIXTURE(Fixture, "UnknownType") TEST_CASE_FIXTURE(Fixture, "UnknownType")
@ -633,7 +633,7 @@ local _o02 = type(game) == "vector"
local _o03 = typeof(game) == "Part" local _o03 = typeof(game) == "Part"
)"); )");
REQUIRE_EQ(result.warnings.size(), 3); REQUIRE(3 == result.warnings.size());
CHECK_EQ(result.warnings[0].location.begin.line, 2); CHECK_EQ(result.warnings[0].location.begin.line, 2);
CHECK_EQ(result.warnings[0].text, "Unknown type 'Part' (expected primitive type)"); CHECK_EQ(result.warnings[0].text, "Unknown type 'Part' (expected primitive type)");
CHECK_EQ(result.warnings[1].location.begin.line, 3); CHECK_EQ(result.warnings[1].location.begin.line, 3);
@ -654,7 +654,7 @@ for i=#t,1,-1 do
end end
)"); )");
CHECK_EQ(result.warnings.size(), 1); REQUIRE(1 == result.warnings.size());
CHECK_EQ(result.warnings[0].location.begin.line, 3); CHECK_EQ(result.warnings[0].location.begin.line, 3);
CHECK_EQ(result.warnings[0].text, "For loop should iterate backwards; did you forget to specify -1 as step?"); CHECK_EQ(result.warnings[0].text, "For loop should iterate backwards; did you forget to specify -1 as step?");
} }
@ -669,7 +669,7 @@ for i=8,1,-1 do
end end
)"); )");
CHECK_EQ(result.warnings.size(), 1); REQUIRE(1 == result.warnings.size());
CHECK_EQ(result.warnings[0].location.begin.line, 1); CHECK_EQ(result.warnings[0].location.begin.line, 1);
CHECK_EQ(result.warnings[0].text, "For loop should iterate backwards; did you forget to specify -1 as step?"); CHECK_EQ(result.warnings[0].text, "For loop should iterate backwards; did you forget to specify -1 as step?");
} }
@ -684,7 +684,7 @@ for i=1.3,7.5,1 do
end end
)"); )");
CHECK_EQ(result.warnings.size(), 1); REQUIRE(1 == result.warnings.size());
CHECK_EQ(result.warnings[0].location.begin.line, 1); CHECK_EQ(result.warnings[0].location.begin.line, 1);
CHECK_EQ(result.warnings[0].text, "For loop ends at 7.3 instead of 7.5; did you forget to specify step?"); CHECK_EQ(result.warnings[0].text, "For loop ends at 7.3 instead of 7.5; did you forget to specify step?");
} }
@ -702,7 +702,7 @@ for i=#t,0 do
end end
)"); )");
CHECK_EQ(result.warnings.size(), 2); REQUIRE(2 == result.warnings.size());
CHECK_EQ(result.warnings[0].location.begin.line, 1); CHECK_EQ(result.warnings[0].location.begin.line, 1);
CHECK_EQ(result.warnings[0].text, "For loop starts at 0, but arrays start at 1"); CHECK_EQ(result.warnings[0].text, "For loop starts at 0, but arrays start at 1");
CHECK_EQ(result.warnings[1].location.begin.line, 7); CHECK_EQ(result.warnings[1].location.begin.line, 7);
@ -730,7 +730,7 @@ local _a,_b,_c = pcall(), nil
end end
)"); )");
CHECK_EQ(result.warnings.size(), 2); REQUIRE(2 == result.warnings.size());
CHECK_EQ(result.warnings[0].location.begin.line, 5); CHECK_EQ(result.warnings[0].location.begin.line, 5);
CHECK_EQ(result.warnings[0].text, "Assigning 2 values to 3 variables initializes extra variables with nil; add 'nil' to value list to silence"); CHECK_EQ(result.warnings[0].text, "Assigning 2 values to 3 variables initializes extra variables with nil; add 'nil' to value list to silence");
CHECK_EQ(result.warnings[1].location.begin.line, 11); CHECK_EQ(result.warnings[1].location.begin.line, 11);
@ -795,7 +795,7 @@ end
return f1,f2,f3,f4,f5,f6,f7 return f1,f2,f3,f4,f5,f6,f7
)"); )");
CHECK_EQ(result.warnings.size(), 3); REQUIRE(3 == result.warnings.size());
CHECK_EQ(result.warnings[0].location.begin.line, 4); CHECK_EQ(result.warnings[0].location.begin.line, 4);
CHECK_EQ(result.warnings[0].text, CHECK_EQ(result.warnings[0].text,
"Function 'f1' can implicitly return no values even though there's an explicit return at line 4; add explicit return to silence"); "Function 'f1' can implicitly return no values even though there's an explicit return at line 4; add explicit return to silence");
@ -851,7 +851,7 @@ end
return f1,f2,f3,f4 return f1,f2,f3,f4
)"); )");
CHECK_EQ(result.warnings.size(), 2); REQUIRE(2 == result.warnings.size());
CHECK_EQ(result.warnings[0].location.begin.line, 25); CHECK_EQ(result.warnings[0].location.begin.line, 25);
CHECK_EQ(result.warnings[0].text, CHECK_EQ(result.warnings[0].text,
"Function 'f3' can implicitly return no values even though there's an explicit return at line 21; add explicit return to silence"); "Function 'f3' can implicitly return no values even though there's an explicit return at line 21; add explicit return to silence");
@ -874,7 +874,7 @@ type InputData = {
} }
)"); )");
CHECK_EQ(result.warnings.size(), 0); REQUIRE(0 == result.warnings.size());
} }
TEST_CASE_FIXTURE(Fixture, "BreakFromInfiniteLoopMakesStatementReachable") TEST_CASE_FIXTURE(Fixture, "BreakFromInfiniteLoopMakesStatementReachable")
@ -893,7 +893,7 @@ until true
return 1 return 1
)"); )");
CHECK_EQ(result.warnings.size(), 0); REQUIRE(0 == result.warnings.size());
} }
TEST_CASE_FIXTURE(Fixture, "IgnoreLintAll") TEST_CASE_FIXTURE(Fixture, "IgnoreLintAll")
@ -903,7 +903,7 @@ TEST_CASE_FIXTURE(Fixture, "IgnoreLintAll")
return foo return foo
)"); )");
CHECK_EQ(result.warnings.size(), 0); REQUIRE(0 == result.warnings.size());
} }
TEST_CASE_FIXTURE(Fixture, "IgnoreLintSpecific") TEST_CASE_FIXTURE(Fixture, "IgnoreLintSpecific")
@ -914,7 +914,7 @@ local x = 1
return foo return foo
)"); )");
CHECK_EQ(result.warnings.size(), 1); REQUIRE(1 == result.warnings.size());
CHECK_EQ(result.warnings[0].text, "Variable 'x' is never used; prefix with '_' to silence"); CHECK_EQ(result.warnings[0].text, "Variable 'x' is never used; prefix with '_' to silence");
} }
@ -933,7 +933,7 @@ local _ = ("%"):format()
string.format("hello %+10d %.02f %%", 4, 5) string.format("hello %+10d %.02f %%", 4, 5)
)"); )");
CHECK_EQ(result.warnings.size(), 4); REQUIRE(4 == result.warnings.size());
CHECK_EQ(result.warnings[0].text, "Invalid format string: unfinished format specifier"); CHECK_EQ(result.warnings[0].text, "Invalid format string: unfinished format specifier");
CHECK_EQ(result.warnings[1].text, "Invalid format string: invalid format specifier: must be a string format specifier or %"); CHECK_EQ(result.warnings[1].text, "Invalid format string: invalid format specifier: must be a string format specifier or %");
CHECK_EQ(result.warnings[2].text, "Invalid format string: invalid format specifier: must be a string format specifier or %"); CHECK_EQ(result.warnings[2].text, "Invalid format string: invalid format specifier: must be a string format specifier or %");
@ -973,7 +973,7 @@ string.packsize("c99999999999999999999")
string.packsize("=!1bbbI3c42") string.packsize("=!1bbbI3c42")
)"); )");
CHECK_EQ(result.warnings.size(), 11); REQUIRE(11 == result.warnings.size());
CHECK_EQ(result.warnings[0].text, "Invalid pack format: unexpected character; must be a pack specifier or space"); CHECK_EQ(result.warnings[0].text, "Invalid pack format: unexpected character; must be a pack specifier or space");
CHECK_EQ(result.warnings[1].text, "Invalid pack format: unexpected character; must be a pack specifier or space"); CHECK_EQ(result.warnings[1].text, "Invalid pack format: unexpected character; must be a pack specifier or space");
CHECK_EQ(result.warnings[2].text, "Invalid pack format: unexpected character; must be a pack specifier or space"); CHECK_EQ(result.warnings[2].text, "Invalid pack format: unexpected character; must be a pack specifier or space");
@ -1017,7 +1017,7 @@ local _ = s:match("%q")
string.match(s, "[A-Z]+(%d)%1") string.match(s, "[A-Z]+(%d)%1")
)"); )");
CHECK_EQ(result.warnings.size(), 14); REQUIRE(14 == result.warnings.size());
CHECK_EQ(result.warnings[0].text, "Invalid match pattern: invalid character class, must refer to a defined class or its inverse"); CHECK_EQ(result.warnings[0].text, "Invalid match pattern: invalid character class, must refer to a defined class or its inverse");
CHECK_EQ(result.warnings[1].text, "Invalid match pattern: invalid character class, must refer to a defined class or its inverse"); CHECK_EQ(result.warnings[1].text, "Invalid match pattern: invalid character class, must refer to a defined class or its inverse");
CHECK_EQ(result.warnings[2].text, "Invalid match pattern: invalid character class, must refer to a defined class or its inverse"); CHECK_EQ(result.warnings[2].text, "Invalid match pattern: invalid character class, must refer to a defined class or its inverse");
@ -1049,7 +1049,7 @@ string.match(s, "((a)%1)")
string.match(s, "((a)%3)") string.match(s, "((a)%3)")
)~"); )~");
CHECK_EQ(result.warnings.size(), 2); REQUIRE(2 == result.warnings.size());
CHECK_EQ(result.warnings[0].text, "Invalid match pattern: invalid capture reference, must refer to a closed capture"); CHECK_EQ(result.warnings[0].text, "Invalid match pattern: invalid capture reference, must refer to a closed capture");
CHECK_EQ(result.warnings[0].location.begin.line, 7); CHECK_EQ(result.warnings[0].location.begin.line, 7);
CHECK_EQ(result.warnings[1].text, "Invalid match pattern: invalid capture reference, must refer to a valid capture"); CHECK_EQ(result.warnings[1].text, "Invalid match pattern: invalid capture reference, must refer to a valid capture");
@ -1087,7 +1087,7 @@ string.match(s, "[]|'[]")
string.match(s, "[^]|'[]") string.match(s, "[^]|'[]")
)~"); )~");
CHECK_EQ(result.warnings.size(), 7); REQUIRE(7 == result.warnings.size());
CHECK_EQ(result.warnings[0].text, "Invalid match pattern: expected ] at the end of the string to close a set"); CHECK_EQ(result.warnings[0].text, "Invalid match pattern: expected ] at the end of the string to close a set");
CHECK_EQ(result.warnings[1].text, "Invalid match pattern: expected ] at the end of the string to close a set"); CHECK_EQ(result.warnings[1].text, "Invalid match pattern: expected ] at the end of the string to close a set");
CHECK_EQ(result.warnings[2].text, "Invalid match pattern: character range can't include character sets"); CHECK_EQ(result.warnings[2].text, "Invalid match pattern: character range can't include character sets");
@ -1118,7 +1118,7 @@ string.find("foo");
("foo"):find() ("foo"):find()
)"); )");
CHECK_EQ(result.warnings.size(), 2); REQUIRE(2 == result.warnings.size());
CHECK_EQ(result.warnings[0].text, "Invalid match pattern: invalid character class, must refer to a defined class or its inverse"); CHECK_EQ(result.warnings[0].text, "Invalid match pattern: invalid character class, must refer to a defined class or its inverse");
CHECK_EQ(result.warnings[0].location.begin.line, 4); CHECK_EQ(result.warnings[0].location.begin.line, 4);
CHECK_EQ(result.warnings[1].text, "Invalid match pattern: invalid character class, must refer to a defined class or its inverse"); CHECK_EQ(result.warnings[1].text, "Invalid match pattern: invalid character class, must refer to a defined class or its inverse");
@ -1141,7 +1141,7 @@ string.gsub(s, '[A-Z]+(%d)', "%0%1")
string.gsub(s, 'foo', "%0") string.gsub(s, 'foo', "%0")
)"); )");
CHECK_EQ(result.warnings.size(), 4); REQUIRE(4 == result.warnings.size());
CHECK_EQ(result.warnings[0].text, "Invalid match replacement: unfinished replacement"); CHECK_EQ(result.warnings[0].text, "Invalid match replacement: unfinished replacement");
CHECK_EQ(result.warnings[1].text, "Invalid match replacement: unexpected replacement character; must be a digit or %"); CHECK_EQ(result.warnings[1].text, "Invalid match replacement: unexpected replacement character; must be a digit or %");
CHECK_EQ(result.warnings[2].text, "Invalid match replacement: invalid capture index, must refer to pattern capture"); CHECK_EQ(result.warnings[2].text, "Invalid match replacement: invalid capture index, must refer to pattern capture");
@ -1162,7 +1162,7 @@ os.date("it's %c now")
os.date("!*t") os.date("!*t")
)"); )");
CHECK_EQ(result.warnings.size(), 4); REQUIRE(4 == result.warnings.size());
CHECK_EQ(result.warnings[0].text, "Invalid date format: unfinished replacement"); CHECK_EQ(result.warnings[0].text, "Invalid date format: unfinished replacement");
CHECK_EQ(result.warnings[1].text, "Invalid date format: unexpected replacement character; must be a date format specifier or %"); CHECK_EQ(result.warnings[1].text, "Invalid date format: unexpected replacement character; must be a date format specifier or %");
CHECK_EQ(result.warnings[2].text, "Invalid date format: unexpected replacement character; must be a date format specifier or %"); CHECK_EQ(result.warnings[2].text, "Invalid date format: unexpected replacement character; must be a date format specifier or %");
@ -1181,7 +1181,7 @@ s:match("[]")
nons:match("[]") nons:match("[]")
)~"); )~");
REQUIRE_EQ(result.warnings.size(), 2); REQUIRE(2 == result.warnings.size());
CHECK_EQ(result.warnings[0].text, "Invalid match pattern: expected ] at the end of the string to close a set"); CHECK_EQ(result.warnings[0].text, "Invalid match pattern: expected ] at the end of the string to close a set");
CHECK_EQ(result.warnings[0].location.begin.line, 3); CHECK_EQ(result.warnings[0].location.begin.line, 3);
CHECK_EQ(result.warnings[1].text, "Invalid match pattern: expected ] at the end of the string to close a set"); CHECK_EQ(result.warnings[1].text, "Invalid match pattern: expected ] at the end of the string to close a set");
@ -1231,7 +1231,7 @@ _ = {
} }
)"); )");
CHECK_EQ(result.warnings.size(), 6); REQUIRE(6 == result.warnings.size());
CHECK_EQ(result.warnings[0].text, "Table field 'first' is a duplicate; previously defined at line 3"); CHECK_EQ(result.warnings[0].text, "Table field 'first' is a duplicate; previously defined at line 3");
CHECK_EQ(result.warnings[1].text, "Table field 'first' is a duplicate; previously defined at line 9"); CHECK_EQ(result.warnings[1].text, "Table field 'first' is a duplicate; previously defined at line 9");
CHECK_EQ(result.warnings[2].text, "Table index 1 is a duplicate; previously defined as a list entry"); CHECK_EQ(result.warnings[2].text, "Table index 1 is a duplicate; previously defined as a list entry");
@ -1248,7 +1248,7 @@ TEST_CASE_FIXTURE(Fixture, "ImportOnlyUsedInTypeAnnotation")
local x: Foo.Y = 1 local x: Foo.Y = 1
)"); )");
REQUIRE_EQ(result.warnings.size(), 1); REQUIRE(1 == result.warnings.size());
CHECK_EQ(result.warnings[0].text, "Variable 'x' is never used; prefix with '_' to silence"); CHECK_EQ(result.warnings[0].text, "Variable 'x' is never used; prefix with '_' to silence");
} }
@ -1259,7 +1259,7 @@ TEST_CASE_FIXTURE(Fixture, "DisableUnknownGlobalWithTypeChecking")
unknownGlobal() unknownGlobal()
)"); )");
REQUIRE_EQ(result.warnings.size(), 0); REQUIRE(0 == result.warnings.size());
} }
TEST_CASE_FIXTURE(Fixture, "no_spurious_warning_after_a_function_type_alias") TEST_CASE_FIXTURE(Fixture, "no_spurious_warning_after_a_function_type_alias")
@ -1271,7 +1271,7 @@ TEST_CASE_FIXTURE(Fixture, "no_spurious_warning_after_a_function_type_alias")
return exports return exports
)"); )");
CHECK_EQ(result.warnings.size(), 0); REQUIRE(0 == result.warnings.size());
} }
TEST_CASE_FIXTURE(Fixture, "use_all_parent_scopes_for_globals") TEST_CASE_FIXTURE(Fixture, "use_all_parent_scopes_for_globals")
@ -1294,7 +1294,7 @@ TEST_CASE_FIXTURE(Fixture, "use_all_parent_scopes_for_globals")
LintResult result = frontend.lint("A"); LintResult result = frontend.lint("A");
CHECK_EQ(result.warnings.size(), 0); REQUIRE(0 == result.warnings.size());
} }
TEST_CASE_FIXTURE(Fixture, "DeadLocalsUsed") TEST_CASE_FIXTURE(Fixture, "DeadLocalsUsed")
@ -1320,7 +1320,7 @@ do
end end
)"); )");
CHECK_EQ(result.warnings.size(), 3); REQUIRE(3 == result.warnings.size());
CHECK_EQ(result.warnings[0].text, "Variable 'x' defined at line 4 is never initialized or assigned; initialize with 'nil' to silence"); CHECK_EQ(result.warnings[0].text, "Variable 'x' defined at line 4 is never initialized or assigned; initialize with 'nil' to silence");
CHECK_EQ(result.warnings[1].text, "Assigning 2 values to 3 variables initializes extra variables with nil; add 'nil' to value list to silence"); CHECK_EQ(result.warnings[1].text, "Assigning 2 values to 3 variables initializes extra variables with nil; add 'nil' to value list to silence");
CHECK_EQ(result.warnings[2].text, "Variable 'c' defined at line 12 is never initialized or assigned; initialize with 'nil' to silence"); CHECK_EQ(result.warnings[2].text, "Variable 'c' defined at line 12 is never initialized or assigned; initialize with 'nil' to silence");
@ -1333,7 +1333,7 @@ local foo
function foo() end function foo() end
)"); )");
CHECK_EQ(result.warnings.size(), 0); REQUIRE(0 == result.warnings.size());
} }
TEST_CASE_FIXTURE(Fixture, "DuplicateGlobalFunction") TEST_CASE_FIXTURE(Fixture, "DuplicateGlobalFunction")
@ -1408,7 +1408,7 @@ TEST_CASE_FIXTURE(Fixture, "DontTriggerTheWarningIfTheFunctionsAreInDifferentSco
return c return c
)"); )");
CHECK(result.warnings.empty()); REQUIRE(0 == result.warnings.size());
} }
TEST_CASE_FIXTURE(Fixture, "LintHygieneUAF") TEST_CASE_FIXTURE(Fixture, "LintHygieneUAF")
@ -1444,7 +1444,7 @@ TEST_CASE_FIXTURE(Fixture, "LintHygieneUAF")
local h: Hooty.Pt local h: Hooty.Pt
)"); )");
CHECK_EQ(result.warnings.size(), 12); REQUIRE(12 == result.warnings.size());
} }
TEST_CASE_FIXTURE(Fixture, "DeprecatedApi") TEST_CASE_FIXTURE(Fixture, "DeprecatedApi")
@ -1478,7 +1478,7 @@ return function (i: Instance)
end end
)"); )");
REQUIRE_EQ(result.warnings.size(), 3); REQUIRE(3 == result.warnings.size());
CHECK_EQ(result.warnings[0].text, "Member 'Instance.Wait' is deprecated"); CHECK_EQ(result.warnings[0].text, "Member 'Instance.Wait' is deprecated");
CHECK_EQ(result.warnings[1].text, "Member 'toHSV' is deprecated, use 'Color3:ToHSV' instead"); CHECK_EQ(result.warnings[1].text, "Member 'toHSV' is deprecated, use 'Color3:ToHSV' instead");
CHECK_EQ(result.warnings[2].text, "Member 'Instance.DataCost' is deprecated"); CHECK_EQ(result.warnings[2].text, "Member 'Instance.DataCost' is deprecated");
@ -1511,7 +1511,7 @@ table.create(42, {})
table.create(42, {} :: {}) table.create(42, {} :: {})
)"); )");
REQUIRE_EQ(result.warnings.size(), 10); REQUIRE(10 == result.warnings.size());
CHECK_EQ(result.warnings[0].text, "table.insert will insert the value before the last element, which is likely a bug; consider removing the " CHECK_EQ(result.warnings[0].text, "table.insert will insert the value before the last element, which is likely a bug; consider removing the "
"second argument or wrap it in parentheses to silence"); "second argument or wrap it in parentheses to silence");
CHECK_EQ(result.warnings[1].text, "table.insert will append the value to the table; consider removing the second argument for efficiency"); CHECK_EQ(result.warnings[1].text, "table.insert will append the value to the table; consider removing the second argument for efficiency");
@ -1556,7 +1556,7 @@ _ = true and true or false -- no warning since this is is a common pattern used
_ = if true then 1 elseif true then 2 else 3 _ = if true then 1 elseif true then 2 else 3
)"); )");
REQUIRE_EQ(result.warnings.size(), 8); REQUIRE(8 == result.warnings.size());
CHECK_EQ(result.warnings[0].text, "Condition has already been checked on line 2"); CHECK_EQ(result.warnings[0].text, "Condition has already been checked on line 2");
CHECK_EQ(result.warnings[0].location.begin.line + 1, 4); CHECK_EQ(result.warnings[0].location.begin.line + 1, 4);
CHECK_EQ(result.warnings[1].text, "Condition has already been checked on column 5"); CHECK_EQ(result.warnings[1].text, "Condition has already been checked on column 5");
@ -1580,7 +1580,7 @@ elseif correct({a = 1, b = 2 * (-2), c = opaque.path['with']("calls", false)}) t
end end
)"); )");
REQUIRE_EQ(result.warnings.size(), 1); REQUIRE(1 == result.warnings.size());
CHECK_EQ(result.warnings[0].text, "Condition has already been checked on line 4"); CHECK_EQ(result.warnings[0].text, "Condition has already been checked on line 4");
CHECK_EQ(result.warnings[0].location.begin.line + 1, 5); CHECK_EQ(result.warnings[0].location.begin.line + 1, 5);
} }
@ -1601,7 +1601,7 @@ end
return foo, moo, a1, a2 return foo, moo, a1, a2
)"); )");
REQUIRE_EQ(result.warnings.size(), 4); REQUIRE(4 == result.warnings.size());
CHECK_EQ(result.warnings[0].text, "Function parameter 'a1' already defined on column 14"); CHECK_EQ(result.warnings[0].text, "Function parameter 'a1' already defined on column 14");
CHECK_EQ(result.warnings[1].text, "Variable 'a1' is never used; prefix with '_' to silence"); CHECK_EQ(result.warnings[1].text, "Variable 'a1' is never used; prefix with '_' to silence");
CHECK_EQ(result.warnings[2].text, "Variable 'a1' already defined on column 7"); CHECK_EQ(result.warnings[2].text, "Variable 'a1' already defined on column 7");
@ -1618,7 +1618,7 @@ _ = math.random() < 0.5 and 0 or 42
_ = (math.random() < 0.5 and false) or 42 -- currently ignored _ = (math.random() < 0.5 and false) or 42 -- currently ignored
)"); )");
REQUIRE_EQ(result.warnings.size(), 2); REQUIRE(2 == result.warnings.size());
CHECK_EQ(result.warnings[0].text, "The and-or expression always evaluates to the second alternative because the first alternative is false; " CHECK_EQ(result.warnings[0].text, "The and-or expression always evaluates to the second alternative because the first alternative is false; "
"consider using if-then-else expression instead"); "consider using if-then-else expression instead");
CHECK_EQ(result.warnings[1].text, "The and-or expression always evaluates to the second alternative because the first alternative is nil; " CHECK_EQ(result.warnings[1].text, "The and-or expression always evaluates to the second alternative because the first alternative is nil; "
@ -1640,7 +1640,7 @@ do end
--!nolint --!nolint
)"); )");
REQUIRE_EQ(result.warnings.size(), 6); REQUIRE(6 == result.warnings.size());
CHECK_EQ(result.warnings[0].text, "Unknown comment directive 'struct'; did you mean 'strict'?"); CHECK_EQ(result.warnings[0].text, "Unknown comment directive 'struct'; did you mean 'strict'?");
CHECK_EQ(result.warnings[1].text, "Unknown comment directive 'nolintGlobal'"); CHECK_EQ(result.warnings[1].text, "Unknown comment directive 'nolintGlobal'");
CHECK_EQ(result.warnings[2].text, "nolint directive refers to unknown lint rule 'Global'"); CHECK_EQ(result.warnings[2].text, "nolint directive refers to unknown lint rule 'Global'");
@ -1656,7 +1656,7 @@ TEST_CASE_FIXTURE(Fixture, "WrongCommentMuteSelf")
--!struct --!struct
)"); )");
REQUIRE_EQ(result.warnings.size(), 0); // --!nolint disables WrongComment lint :) REQUIRE(0 == result.warnings.size()); // --!nolint disables WrongComment lint :)
} }
TEST_CASE_FIXTURE(Fixture, "DuplicateConditionsIfStatAndExpr") TEST_CASE_FIXTURE(Fixture, "DuplicateConditionsIfStatAndExpr")
@ -1668,7 +1668,7 @@ elseif if 0 then 5 else 4 then
end end
)"); )");
REQUIRE_EQ(result.warnings.size(), 1); REQUIRE(1 == result.warnings.size());
CHECK_EQ(result.warnings[0].text, "Condition has already been checked on line 2"); CHECK_EQ(result.warnings[0].text, "Condition has already been checked on line 2");
} }
@ -1681,13 +1681,13 @@ TEST_CASE_FIXTURE(Fixture, "WrongCommentOptimize")
--!optimize 2 --!optimize 2
)"); )");
REQUIRE_EQ(result.warnings.size(), 3); REQUIRE(3 == result.warnings.size());
CHECK_EQ(result.warnings[0].text, "optimize directive requires an optimization level"); CHECK_EQ(result.warnings[0].text, "optimize directive requires an optimization level");
CHECK_EQ(result.warnings[1].text, "optimize directive uses unknown optimization level 'me', 0..2 expected"); CHECK_EQ(result.warnings[1].text, "optimize directive uses unknown optimization level 'me', 0..2 expected");
CHECK_EQ(result.warnings[2].text, "optimize directive uses unknown optimization level '100500', 0..2 expected"); CHECK_EQ(result.warnings[2].text, "optimize directive uses unknown optimization level '100500', 0..2 expected");
result = lint("--!optimize "); result = lint("--!optimize ");
REQUIRE_EQ(result.warnings.size(), 1); REQUIRE(1 == result.warnings.size());
CHECK_EQ(result.warnings[0].text, "optimize directive requires an optimization level"); CHECK_EQ(result.warnings[0].text, "optimize directive requires an optimization level");
} }
@ -1700,7 +1700,7 @@ TEST_CASE_FIXTURE(Fixture, "TestStringInterpolation")
local _ = `unknown {foo}` local _ = `unknown {foo}`
)"); )");
REQUIRE_EQ(result.warnings.size(), 1); REQUIRE(1 == result.warnings.size());
} }
TEST_CASE_FIXTURE(Fixture, "IntegerParsing") TEST_CASE_FIXTURE(Fixture, "IntegerParsing")
@ -1710,7 +1710,7 @@ local _ = 0b10000000000000000000000000000000000000000000000000000000000000000
local _ = 0x10000000000000000 local _ = 0x10000000000000000
)"); )");
REQUIRE_EQ(result.warnings.size(), 2); REQUIRE(2 == result.warnings.size());
CHECK_EQ(result.warnings[0].text, "Binary number literal exceeded available precision and has been truncated to 2^64"); CHECK_EQ(result.warnings[0].text, "Binary number literal exceeded available precision and has been truncated to 2^64");
CHECK_EQ(result.warnings[1].text, "Hexadecimal number literal exceeded available precision and has been truncated to 2^64"); CHECK_EQ(result.warnings[1].text, "Hexadecimal number literal exceeded available precision and has been truncated to 2^64");
} }
@ -1725,7 +1725,7 @@ local _ = 0x0x123
local _ = 0x0xffffffffffffffffffffffffffffffffff local _ = 0x0xffffffffffffffffffffffffffffffffff
)"); )");
REQUIRE_EQ(result.warnings.size(), 2); REQUIRE(2 == result.warnings.size());
CHECK_EQ(result.warnings[0].text, CHECK_EQ(result.warnings[0].text,
"Hexadecimal number literal has a double prefix, which will fail to parse in the future; remove the extra 0x to fix"); "Hexadecimal number literal has a double prefix, which will fail to parse in the future; remove the extra 0x to fix");
CHECK_EQ(result.warnings[1].text, CHECK_EQ(result.warnings[1].text,
@ -1756,7 +1756,7 @@ local _ = (a <= b) == 0
local _ = a <= (b == 0) local _ = a <= (b == 0)
)"); )");
REQUIRE_EQ(result.warnings.size(), 5); REQUIRE(5 == result.warnings.size());
CHECK_EQ(result.warnings[0].text, "not X == Y is equivalent to (not X) == Y; consider using X ~= Y, or add parentheses to silence"); CHECK_EQ(result.warnings[0].text, "not X == Y is equivalent to (not X) == Y; consider using X ~= Y, or add parentheses to silence");
CHECK_EQ(result.warnings[1].text, "not X ~= Y is equivalent to (not X) ~= Y; consider using X == Y, or add parentheses to silence"); CHECK_EQ(result.warnings[1].text, "not X ~= Y is equivalent to (not X) ~= Y; consider using X == Y, or add parentheses to silence");
CHECK_EQ(result.warnings[2].text, "not X <= Y is equivalent to (not X) <= Y; add parentheses to silence"); CHECK_EQ(result.warnings[2].text, "not X <= Y is equivalent to (not X) <= Y; add parentheses to silence");

View file

@ -356,6 +356,11 @@ TEST_CASE_FIXTURE(NormalizeFixture, "table_with_any_prop")
TEST_CASE_FIXTURE(NormalizeFixture, "intersection") TEST_CASE_FIXTURE(NormalizeFixture, "intersection")
{ {
ScopedFastFlag sffs[] {
{"LuauSubtypeNormalizer", true},
{"LuauTypeNormalization2", true},
};
check(R"( check(R"(
local a: number & string local a: number & string
local b: number local b: number
@ -374,8 +379,9 @@ TEST_CASE_FIXTURE(NormalizeFixture, "intersection")
CHECK(!isSubtype(c, a)); CHECK(!isSubtype(c, a));
CHECK(isSubtype(a, c)); CHECK(isSubtype(a, c));
CHECK(!isSubtype(d, a)); // These types are both equivalent to never
CHECK(!isSubtype(a, d)); CHECK(isSubtype(d, a));
CHECK(isSubtype(a, d));
} }
TEST_CASE_FIXTURE(NormalizeFixture, "union_and_intersection") TEST_CASE_FIXTURE(NormalizeFixture, "union_and_intersection")

View file

@ -2722,4 +2722,59 @@ TEST_CASE_FIXTURE(Fixture, "error_message_for_using_function_as_type_annotation"
result.errors[0].getMessage()); result.errors[0].getMessage());
} }
TEST_CASE_FIXTURE(Fixture, "get_a_nice_error_when_there_is_an_extra_comma_at_the_end_of_a_function_argument_list")
{
ScopedFastFlag sff{"LuauCommaParenWarnings", true};
ParseResult result = tryParse(R"(
foo(a, b, c,)
)");
REQUIRE(1 == result.errors.size());
CHECK(Location({1, 20}, {1, 21}) == result.errors[0].getLocation());
CHECK("Expected expression after ',' but got ')' instead" == result.errors[0].getMessage());
}
TEST_CASE_FIXTURE(Fixture, "get_a_nice_error_when_there_is_an_extra_comma_at_the_end_of_a_function_parameter_list")
{
ScopedFastFlag sff{"LuauCommaParenWarnings", true};
ParseResult result = tryParse(R"(
export type VisitFn = (
any,
Array<TAnyNode | Array<TAnyNode>>, -- extra comma here
) -> any
)");
REQUIRE(1 == result.errors.size());
CHECK(Location({4, 8}, {4, 9}) == result.errors[0].getLocation());
CHECK("Expected type after ',' but got ')' instead" == result.errors[0].getMessage());
}
TEST_CASE_FIXTURE(Fixture, "get_a_nice_error_when_there_is_an_extra_comma_at_the_end_of_a_generic_parameter_list")
{
ScopedFastFlag sff{"LuauCommaParenWarnings", true};
ParseResult result = tryParse(R"(
export type VisitFn = <A, B,>(a: A, b: B) -> ()
)");
REQUIRE(1 == result.errors.size());
CHECK(Location({1, 36}, {1, 37}) == result.errors[0].getLocation());
CHECK("Expected type after ',' but got '>' instead" == result.errors[0].getMessage());
REQUIRE(1 == result.root->body.size);
AstStatTypeAlias* t = result.root->body.data[0]->as<AstStatTypeAlias>();
REQUIRE(t != nullptr);
AstTypeFunction* f = t->type->as<AstTypeFunction>();
REQUIRE(f != nullptr);
CHECK(2 == f->generics.size);
}
TEST_SUITE_END(); TEST_SUITE_END();

View file

@ -480,4 +480,38 @@ local a: ChildClass = i
CHECK_EQ("Type 'ChildClass' from 'Test' could not be converted into 'ChildClass' from 'MainModule'", toString(result.errors[0])); CHECK_EQ("Type 'ChildClass' from 'Test' could not be converted into 'ChildClass' from 'MainModule'", toString(result.errors[0]));
} }
TEST_CASE_FIXTURE(ClassFixture, "intersections_of_unions_of_classes")
{
ScopedFastFlag sffs[] {
{"LuauSubtypeNormalizer", true},
{"LuauTypeNormalization2", true},
};
CheckResult result = check(R"(
local x : (BaseClass | Vector2) & (ChildClass | AnotherChild)
local y : (ChildClass | AnotherChild)
x = y
y = x
)");
LUAU_REQUIRE_NO_ERRORS(result);
}
TEST_CASE_FIXTURE(ClassFixture, "unions_of_intersections_of_classes")
{
ScopedFastFlag sffs[] {
{"LuauSubtypeNormalizer", true},
{"LuauTypeNormalization2", true},
};
CheckResult result = check(R"(
local x : (BaseClass & ChildClass) | (BaseClass & AnotherChild) | (BaseClass & Vector2)
local y : (ChildClass | AnotherChild)
x = y
y = x
)");
LUAU_REQUIRE_NO_ERRORS(result);
}
TEST_SUITE_END(); TEST_SUITE_END();

View file

@ -2,6 +2,7 @@
#include "Luau/AstQuery.h" #include "Luau/AstQuery.h"
#include "Luau/BuiltinDefinitions.h" #include "Luau/BuiltinDefinitions.h"
#include "Luau/Error.h"
#include "Luau/Scope.h" #include "Luau/Scope.h"
#include "Luau/TypeInfer.h" #include "Luau/TypeInfer.h"
#include "Luau/TypeVar.h" #include "Luau/TypeVar.h"
@ -14,6 +15,7 @@
using namespace Luau; using namespace Luau;
LUAU_FASTFLAG(LuauLowerBoundsCalculation); LUAU_FASTFLAG(LuauLowerBoundsCalculation);
LUAU_FASTFLAG(LuauInstantiateInSubtyping);
LUAU_FASTFLAG(LuauSpecialTypesAsterisked); LUAU_FASTFLAG(LuauSpecialTypesAsterisked);
TEST_SUITE_BEGIN("TypeInferFunctions"); TEST_SUITE_BEGIN("TypeInferFunctions");
@ -1087,10 +1089,20 @@ f(function(a, b, c, ...) return a + b end)
LUAU_REQUIRE_ERRORS(result); LUAU_REQUIRE_ERRORS(result);
CHECK_EQ(R"(Type '(number, number, a) -> number' could not be converted into '(number, number) -> number' if (FFlag::LuauInstantiateInSubtyping)
{
CHECK_EQ(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])); 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"(
@ -1189,10 +1201,20 @@ f(function(a, b, c, ...) return a + b end)
LUAU_REQUIRE_ERRORS(result); LUAU_REQUIRE_ERRORS(result);
CHECK_EQ(R"(Type '(number, number, a) -> number' could not be converted into '(number, number) -> number' if (FFlag::LuauInstantiateInSubtyping)
{
CHECK_EQ(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])); 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"(

View file

@ -10,6 +10,7 @@
#include "doctest.h" #include "doctest.h"
LUAU_FASTFLAG(LuauCheckGenericHOFTypes) LUAU_FASTFLAG(LuauCheckGenericHOFTypes)
LUAU_FASTFLAG(LuauInstantiateInSubtyping)
LUAU_FASTFLAG(LuauSpecialTypesAsterisked) LUAU_FASTFLAG(LuauSpecialTypesAsterisked)
using namespace Luau; using namespace Luau;
@ -960,7 +961,11 @@ TEST_CASE_FIXTURE(Fixture, "instantiate_generic_function_in_assignments")
TypeMismatch* tm = get<TypeMismatch>(result.errors[0]); TypeMismatch* tm = get<TypeMismatch>(result.errors[0]);
REQUIRE(tm); REQUIRE(tm);
CHECK_EQ("((number) -> number, string) -> number", toString(tm->wantedType)); CHECK_EQ("((number) -> number, string) -> number", toString(tm->wantedType));
CHECK_EQ("((number) -> number, number) -> number", toString(tm->givenType)); if (FFlag::LuauInstantiateInSubtyping)
CHECK_EQ("<a, b...>((a) -> (b...), a) -> (b...)", toString(tm->givenType));
else
CHECK_EQ("((number) -> number, number) -> number", toString(tm->givenType));
} }
TEST_CASE_FIXTURE(Fixture, "instantiate_generic_function_in_assignments2") TEST_CASE_FIXTURE(Fixture, "instantiate_generic_function_in_assignments2")
@ -980,7 +985,10 @@ TEST_CASE_FIXTURE(Fixture, "instantiate_generic_function_in_assignments2")
TypeMismatch* tm = get<TypeMismatch>(result.errors[0]); TypeMismatch* tm = get<TypeMismatch>(result.errors[0]);
REQUIRE(tm); REQUIRE(tm);
CHECK_EQ("(string, string) -> number", toString(tm->wantedType)); CHECK_EQ("(string, string) -> number", toString(tm->wantedType));
CHECK_EQ("((string) -> number, string) -> number", toString(*tm->givenType)); if (FFlag::LuauInstantiateInSubtyping)
CHECK_EQ("<a, b...>((a) -> (b...), a) -> (b...)", toString(tm->givenType));
else
CHECK_EQ("((string) -> number, string) -> number", toString(*tm->givenType));
} }
TEST_CASE_FIXTURE(Fixture, "self_recursive_instantiated_param") TEST_CASE_FIXTURE(Fixture, "self_recursive_instantiated_param")
@ -1110,6 +1118,15 @@ local c = sumrec(function(x, y, f) return f(x, y) end) -- type binders are not i
{ {
LUAU_REQUIRE_NO_ERRORS(result); LUAU_REQUIRE_NO_ERRORS(result);
} }
else if (FFlag::LuauInstantiateInSubtyping)
{
LUAU_REQUIRE_ERRORS(result);
CHECK_EQ(
R"(Type '<a, b, c...>(a, b, (a, b) -> (c...)) -> (c...)' could not be converted into '<a>(a, a, (a, a) -> a) -> a'
caused by:
Argument #1 type is not compatible. Generic subtype escaping scope)",
toString(result.errors[0]));
}
else else
{ {
LUAU_REQUIRE_ERRORS(result); LUAU_REQUIRE_ERRORS(result);
@ -1219,4 +1236,48 @@ TEST_CASE_FIXTURE(Fixture, "do_not_always_instantiate_generic_intersection_types
LUAU_REQUIRE_NO_ERRORS(result); LUAU_REQUIRE_NO_ERRORS(result);
} }
TEST_CASE_FIXTURE(BuiltinsFixture, "hof_subtype_instantiation_regression")
{
CheckResult result = check(R"(
--!strict
local function defaultSort<T>(a: T, b: T)
return true
end
type A = any
return function<T>(array: {T}): {T}
table.sort(array, defaultSort)
return array
end
)");
LUAU_REQUIRE_NO_ERRORS(result);
}
TEST_CASE_FIXTURE(BuiltinsFixture, "higher_rank_polymorphism_should_not_accept_instantiated_arguments")
{
ScopedFastFlag sffs[] = {
{"LuauInstantiateInSubtyping", true},
{"LuauCheckGenericHOFTypes", true}, // necessary because of interactions with the test
};
CheckResult result = check(R"(
--!strict
local function instantiate(f: <a>(a) -> a): (number) -> number
return f
end
instantiate(function(x: string) return "foo" end)
)");
LUAU_REQUIRE_ERROR_COUNT(1, result);
auto tm1 = get<TypeMismatch>(result.errors[0]);
REQUIRE(tm1);
CHECK_EQ("<a>(a) -> a", toString(tm1->wantedType));
CHECK_EQ("<a>(string) -> string", toString(tm1->givenType));
}
TEST_SUITE_END(); TEST_SUITE_END();

View file

@ -446,4 +446,459 @@ TEST_CASE_FIXTURE(Fixture, "no_stack_overflow_from_flattenintersection")
LUAU_REQUIRE_ERRORS(result); LUAU_REQUIRE_ERRORS(result);
} }
TEST_CASE_FIXTURE(Fixture, "intersect_bool_and_false")
{
CheckResult result = check(R"(
local x : (boolean & false)
local y : false = x -- OK
local z : true = x -- Not OK
)");
LUAU_REQUIRE_ERROR_COUNT(1, result);
CHECK_EQ(toString(result.errors[0]), "Type 'boolean & false' could not be converted into 'true'; none of the intersection parts are compatible");
}
TEST_CASE_FIXTURE(Fixture, "intersect_false_and_bool_and_false")
{
CheckResult result = check(R"(
local x : false & (boolean & false)
local y : false = x -- OK
local z : true = x -- Not OK
)");
LUAU_REQUIRE_ERROR_COUNT(1, result);
// TODO: odd stringification of `false & (boolean & false)`.)
CHECK_EQ(toString(result.errors[0]), "Type 'boolean & false & false' could not be converted into 'true'; none of the intersection parts are compatible");
}
TEST_CASE_FIXTURE(Fixture, "intersect_saturate_overloaded_functions")
{
ScopedFastFlag sffs[] {
{"LuauSubtypeNormalizer", true},
{"LuauTypeNormalization2", true},
};
CheckResult result = check(R"(
local x : ((number?) -> number?) & ((string?) -> string?)
local y : (nil) -> nil = x -- OK
local z : (number) -> number = x -- Not OK
)");
LUAU_REQUIRE_ERROR_COUNT(1, result);
CHECK_EQ(toString(result.errors[0]), "Type '((number?) -> number?) & ((string?) -> string?)' could not be converted into '(number) -> number'; none of the intersection parts are compatible");
}
TEST_CASE_FIXTURE(Fixture, "union_saturate_overloaded_functions")
{
ScopedFastFlag sffs[] {
{"LuauSubtypeNormalizer", true},
{"LuauTypeNormalization2", true},
};
CheckResult result = check(R"(
local x : ((number) -> number) & ((string) -> string)
local y : ((number | string) -> (number | string)) = x -- OK
local z : ((number | boolean) -> (number | boolean)) = x -- Not OK
)");
LUAU_REQUIRE_ERROR_COUNT(1, result);
CHECK_EQ(toString(result.errors[0]), "Type '((number) -> number) & ((string) -> string)' could not be converted into '(boolean | number) -> boolean | number'; none of the intersection parts are compatible");
}
TEST_CASE_FIXTURE(Fixture, "intersection_of_tables")
{
ScopedFastFlag sffs[] {
{"LuauSubtypeNormalizer", true},
{"LuauTypeNormalization2", true},
};
CheckResult result = check(R"(
local x : { p : number?, q : string? } & { p : number?, q : number?, r : number? }
local y : { p : number?, q : nil, r : number? } = x -- OK
local z : { p : nil } = x -- Not OK
)");
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 '{| p: nil |}'; none of the intersection parts are compatible");
}
TEST_CASE_FIXTURE(Fixture, "intersection_of_tables_with_top_properties")
{
CheckResult result = check(R"(
local x : { p : number?, q : any } & { p : unknown, q : string? }
local y : { p : number?, q : string? } = x -- OK
local z : { p : string?, q : number? } = x -- Not OK
)");
LUAU_REQUIRE_ERROR_COUNT(1, result);
CHECK_EQ(toString(result.errors[0]), "Type '{| p: number?, q: any |} & {| p: unknown, q: string? |}' could not be converted into '{| p: string?, q: number? |}'; none of the intersection parts are compatible");
}
TEST_CASE_FIXTURE(Fixture, "intersection_of_tables_with_never_properties")
{
ScopedFastFlag sffs[] {
{"LuauSubtypeNormalizer", true},
{"LuauTypeNormalization2", true},
};
CheckResult result = check(R"(
local x : { p : number?, q : never } & { p : never, q : string? }
local y : { p : never, q : never } = x -- OK
local z : never = x -- OK
)");
// TODO: this should not produce type errors, since never <: { p : never }
LUAU_REQUIRE_ERROR_COUNT(1, result);
CHECK_EQ(toString(result.errors[0]), "Type '{| p: never, q: string? |} & {| p: number?, q: never |}' could not be converted into 'never'; none of the intersection parts are compatible");
}
TEST_CASE_FIXTURE(Fixture, "overloaded_functions_returning_intersections")
{
ScopedFastFlag sffs[] {
{"LuauSubtypeNormalizer", true},
{"LuauTypeNormalization2", true},
};
CheckResult result = check(R"(
local x : ((number?) -> ({ p : number } & { q : number })) & ((string?) -> ({ p : number } & { r : number }))
local y : (nil) -> { p : number, q : number, r : number} = x -- OK
local z : (number?) -> { p : number, q : number, r : number} = x -- Not OK
)");
LUAU_REQUIRE_ERROR_COUNT(1, result);
CHECK_EQ(toString(result.errors[0]), "Type '((number?) -> {| p: number |} & {| q: number |}) & ((string?) -> {| p: number |} & {| r: number |})' could not be converted into '(number?) -> {| p: number, q: number, r: number |}'; none of the intersection parts are compatible");
}
TEST_CASE_FIXTURE(Fixture, "overloaded_functions_mentioning_generic")
{
ScopedFastFlag sffs[] {
{"LuauSubtypeNormalizer", true},
{"LuauTypeNormalization2", true},
};
CheckResult result = check(R"(
function f<a>()
local x : ((number?) -> (a | number)) & ((string?) -> (a | string))
local y : (nil) -> a = x -- OK
local z : (number?) -> a = x -- Not OK
end
)");
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'; none of the intersection parts are compatible");
}
TEST_CASE_FIXTURE(Fixture, "overloaded_functions_mentioning_generics")
{
ScopedFastFlag sffs[] {
{"LuauSubtypeNormalizer", true},
{"LuauTypeNormalization2", true},
};
CheckResult result = check(R"(
function f<a,b,c>()
local x : ((a?) -> (a | b)) & ((c?) -> (b | c))
local y : (nil) -> ((a & c) | b) = x -- OK
local z : (a?) -> ((a & c) | b) = x -- Not OK
end
)");
LUAU_REQUIRE_ERROR_COUNT(1, result);
CHECK_EQ(toString(result.errors[0]), "Type '((a?) -> a | b) & ((c?) -> b | c)' could not be converted into '(a?) -> (a & c) | b'; none of the intersection parts are compatible");
}
TEST_CASE_FIXTURE(Fixture, "overloaded_functions_mentioning_generic_packs")
{
ScopedFastFlag sffs[] {
{"LuauSubtypeNormalizer", true},
{"LuauTypeNormalization2", true},
};
CheckResult result = check(R"(
function f<a...,b...>()
local x : ((number?, a...) -> (number?, b...)) & ((string?, a...) -> (string?, b...))
local y : ((nil, a...) -> (nil, b...)) = x -- OK
local z : ((nil, b...) -> (nil, a...)) = x -- Not OK
end
)");
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 into '(nil, b...) -> (nil, a...)'; none of the intersection parts are compatible");
}
TEST_CASE_FIXTURE(Fixture, "overloadeded_functions_with_unknown_result")
{
ScopedFastFlag sffs[] {
{"LuauSubtypeNormalizer", true},
{"LuauTypeNormalization2", true},
};
CheckResult result = check(R"(
function f<a...,b...>()
local x : ((number) -> number) & ((nil) -> unknown)
local y : (number?) -> unknown = x -- OK
local z : (number?) -> number? = x -- Not OK
end
)");
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 of the intersection parts are compatible");
}
TEST_CASE_FIXTURE(Fixture, "overloadeded_functions_with_unknown_arguments")
{
ScopedFastFlag sffs[] {
{"LuauSubtypeNormalizer", true},
{"LuauTypeNormalization2", true},
};
CheckResult result = check(R"(
function f<a...,b...>()
local x : ((number) -> number?) & ((unknown) -> string?)
local y : (number) -> nil = x -- OK
local z : (number?) -> nil = x -- Not OK
end
)");
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 of the intersection parts are compatible");
}
TEST_CASE_FIXTURE(Fixture, "overloadeded_functions_with_never_result")
{
ScopedFastFlag sffs[] {
{"LuauSubtypeNormalizer", true},
{"LuauTypeNormalization2", true},
};
CheckResult result = check(R"(
function f<a...,b...>()
local x : ((number) -> number) & ((nil) -> never)
local y : (number?) -> number = x -- OK
local z : (number?) -> never = x -- Not OK
end
)");
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 the intersection parts are compatible");
}
TEST_CASE_FIXTURE(Fixture, "overloadeded_functions_with_never_arguments")
{
ScopedFastFlag sffs[] {
{"LuauSubtypeNormalizer", true},
{"LuauTypeNormalization2", true},
};
CheckResult result = check(R"(
function f<a...,b...>()
local x : ((number) -> number?) & ((never) -> string?)
local y : (never) -> nil = x -- OK
local z : (number?) -> nil = x -- Not OK
end
)");
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 of the intersection parts are compatible");
}
TEST_CASE_FIXTURE(Fixture, "overloadeded_functions_with_overlapping_results_and_variadics")
{
CheckResult result = check(R"(
local x : ((string?) -> (string | number)) & ((number?) -> ...number)
local y : ((nil) -> (number, number?)) = x -- OK
local z : ((string | number) -> (number, number?)) = x -- Not OK
)");
LUAU_REQUIRE_ERROR_COUNT(1, result);
CHECK_EQ(toString(result.errors[0]), "Type '((number?) -> (...number)) & ((string?) -> number | string)' could not be converted into '(number | string) -> (number, number?)'; none of the intersection parts are compatible");
}
TEST_CASE_FIXTURE(Fixture, "overloadeded_functions_with_weird_typepacks_1")
{
CheckResult result = check(R"(
function f<a...,b...>()
local x : (() -> a...) & (() -> b...)
local y : (() -> b...) & (() -> a...) = x -- OK
local z : () -> () = x -- Not OK
end
)");
LUAU_REQUIRE_ERROR_COUNT(1, result);
CHECK_EQ(toString(result.errors[0]), "Type '(() -> (a...)) & (() -> (b...))' could not be converted into '() -> ()'; none of the intersection parts are compatible");
}
TEST_CASE_FIXTURE(Fixture, "overloadeded_functions_with_weird_typepacks_2")
{
CheckResult result = check(R"(
function f<a...,b...>()
local x : ((a...) -> ()) & ((b...) -> ())
local y : ((b...) -> ()) & ((a...) -> ()) = x -- OK
local z : () -> () = x -- Not OK
end
)");
LUAU_REQUIRE_ERROR_COUNT(1, result);
CHECK_EQ(toString(result.errors[0]), "Type '((a...) -> ()) & ((b...) -> ())' could not be converted into '() -> ()'; none of the intersection parts are compatible");
}
TEST_CASE_FIXTURE(Fixture, "overloadeded_functions_with_weird_typepacks_3")
{
CheckResult result = check(R"(
function f<a...>()
local x : (() -> a...) & (() -> (number?,a...))
local y : (() -> (number?,a...)) & (() -> a...) = x -- OK
local z : () -> (number) = x -- Not OK
end
)");
LUAU_REQUIRE_ERROR_COUNT(1, result);
CHECK_EQ(toString(result.errors[0]), "Type '(() -> (a...)) & (() -> (number?, a...))' could not be converted into '() -> number'; none of the intersection parts are compatible");
}
TEST_CASE_FIXTURE(Fixture, "overloadeded_functions_with_weird_typepacks_4")
{
CheckResult result = check(R"(
function f<a...>()
local x : ((a...) -> ()) & ((number,a...) -> number)
local y : ((number,a...) -> number) & ((a...) -> ()) = x -- OK
local z : (number?) -> () = x -- Not OK
end
)");
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 the intersection parts are compatible");
}
TEST_CASE_FIXTURE(BuiltinsFixture, "intersect_metatables")
{
ScopedFastFlag sffs[] {
{"LuauSubtypeNormalizer", true},
{"LuauTypeNormalization2", true},
};
CheckResult result = check(R"(
local a : string? = nil
local b : number? = nil
local x = setmetatable({}, { p = 5, q = a });
local y = setmetatable({}, { q = b, r = "hi" });
local z = setmetatable({}, { p = 5, q = nil, r = "hi" });
type X = typeof(x)
type Y = typeof(y)
type Z = typeof(z)
local xy : X&Y = z;
local yx : Y&X = z;
z = xy;
z = yx;
)");
LUAU_REQUIRE_NO_ERRORS(result);
}
TEST_CASE_FIXTURE(BuiltinsFixture, "intersect_metatable_subtypes")
{
ScopedFastFlag sffs[] {
{"LuauSubtypeNormalizer", true},
{"LuauTypeNormalization2", true},
};
CheckResult result = check(R"(
local x = setmetatable({ a = 5 }, { p = 5 });
local y = setmetatable({ b = "hi" }, { p = 5, q = "hi" });
local z = setmetatable({ a = 5, b = "hi" }, { p = 5, q = "hi" });
type X = typeof(x)
type Y = typeof(y)
type Z = typeof(z)
local xy : X&Y = z;
local yx : Y&X = z;
z = xy;
z = yx;
)");
LUAU_REQUIRE_NO_ERRORS(result);
}
TEST_CASE_FIXTURE(BuiltinsFixture, "intersect_metatables_with_properties")
{
ScopedFastFlag sffs[] {
{"LuauSubtypeNormalizer", true},
{"LuauTypeNormalization2", true},
};
CheckResult result = check(R"(
local x = setmetatable({ a = 5 }, { p = 5 });
local y = setmetatable({ b = "hi" }, { q = "hi" });
local z = setmetatable({ a = 5, b = "hi" }, { p = 5, q = "hi" });
type X = typeof(x)
type Y = typeof(y)
type Z = typeof(z)
local xy : X&Y = z;
z = xy;
)");
LUAU_REQUIRE_NO_ERRORS(result);
}
TEST_CASE_FIXTURE(BuiltinsFixture, "intersect_metatable_with table")
{
ScopedFastFlag sffs[] {
{"LuauSubtypeNormalizer", true},
{"LuauTypeNormalization2", true},
};
CheckResult result = check(R"(
local x = setmetatable({ a = 5 }, { p = 5 });
local z = setmetatable({ a = 5, b = "hi" }, { p = 5 });
type X = typeof(x)
type Y = { b : string }
type Z = typeof(z)
-- TODO: once we have shape types, we should be able to initialize these with z
local xy : X&Y;
local yx : Y&X;
z = xy;
z = yx;
)");
LUAU_REQUIRE_NO_ERRORS(result);
}
TEST_CASE_FIXTURE(Fixture, "CLI-44817")
{
ScopedFastFlag sffs[] {
{"LuauSubtypeNormalizer", true},
{"LuauTypeNormalization2", true},
};
CheckResult result = check(R"(
type X = {x: number}
type Y = {y: number}
type Z = {z: number}
type XY = {x: number, y: number}
type XYZ = {x:number, y: number, z: number}
local xy: XY = {x = 0, y = 0}
local xyz: XYZ = {x = 0, y = 0, z = 0}
local xNy: X&Y = xy
local xNyNz: X&Y&Z = xyz
local t1: XY = xNy -- Type 'X & Y' could not be converted into 'XY'
local t2: XY = xNyNz -- Type 'X & Y & Z' could not be converted into 'XY'
local t3: XYZ = xNyNz -- Type 'X & Y & Z' could not be converted into 'XYZ'
)");
LUAU_REQUIRE_NO_ERRORS(result);
}
TEST_SUITE_END(); TEST_SUITE_END();

View file

@ -10,6 +10,8 @@
#include "doctest.h" #include "doctest.h"
LUAU_FASTFLAG(LuauInstantiateInSubtyping)
using namespace Luau; using namespace Luau;
LUAU_FASTFLAG(LuauSpecialTypesAsterisked) LUAU_FASTFLAG(LuauSpecialTypesAsterisked)
@ -248,7 +250,24 @@ end
return m return m
)"); )");
LUAU_REQUIRE_NO_ERRORS(result);
if (FFlag::LuauInstantiateInSubtyping)
{
// though this didn't error before the flag, it seems as though it should error since fields of a table are invariant.
// the user's intent would likely be that these "method" fields would be read-only, but without an annotation, accepting this should be unsound.
LUAU_REQUIRE_ERROR_COUNT(1, result);
CHECK_EQ(R"(Type 'n' could not be converted into 't1 where t1 = {- Clone: (t1) -> (a...) -}'
caused by:
Property 'Clone' is not compatible. Type '<a>(a) -> ()' could not be converted into 't1 where t1 = ({- Clone: t1 -}) -> (a...)'; different number of generic type parameters)",
toString(result.errors[0]));
}
else
{
LUAU_REQUIRE_NO_ERRORS(result);
}
} }
TEST_CASE_FIXTURE(BuiltinsFixture, "custom_require_global") TEST_CASE_FIXTURE(BuiltinsFixture, "custom_require_global")
@ -367,8 +386,6 @@ type Table = typeof(tbl)
TEST_CASE_FIXTURE(BuiltinsFixture, "do_not_modify_imported_types_5") TEST_CASE_FIXTURE(BuiltinsFixture, "do_not_modify_imported_types_5")
{ {
ScopedFastFlag luauInplaceDemoteSkipAllBound{"LuauInplaceDemoteSkipAllBound", true};
fileResolver.source["game/A"] = R"( fileResolver.source["game/A"] = R"(
export type Type = {x: number, y: number} export type Type = {x: number, y: number}
local arrayops = {} local arrayops = {}

View file

@ -271,30 +271,6 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "bail_early_if_unification_is_too_complicated
} }
} }
// Should be in TypeInfer.tables.test.cpp
// It's unsound to instantiate tables containing generic methods,
// since mutating properties means table properties should be invariant.
// We currently allow this but we shouldn't!
TEST_CASE_FIXTURE(Fixture, "invariant_table_properties_means_instantiating_tables_in_call_is_unsound")
{
CheckResult result = check(R"(
--!strict
local t = {}
function t.m(x) return x end
local a : string = t.m("hi")
local b : number = t.m(5)
function f(x : { m : (number)->number })
x.m = function(x) return 1+x end
end
f(t) -- This shouldn't typecheck
local c : string = t.m("hi")
)");
// TODO: this should error!
// This should be fixed by replacing generic tables by generics with type bounds.
LUAU_REQUIRE_NO_ERRORS(result);
}
// FIXME: Move this test to another source file when removing FFlag::LuauLowerBoundsCalculation // FIXME: Move this test to another source file when removing FFlag::LuauLowerBoundsCalculation
TEST_CASE_FIXTURE(Fixture, "do_not_ice_when_trying_to_pick_first_of_generic_type_pack") TEST_CASE_FIXTURE(Fixture, "do_not_ice_when_trying_to_pick_first_of_generic_type_pack")
{ {
@ -608,7 +584,8 @@ TEST_CASE_FIXTURE(Fixture, "free_options_cannot_be_unified_together")
InternalErrorReporter iceHandler; InternalErrorReporter iceHandler;
UnifierSharedState sharedState{&iceHandler}; UnifierSharedState sharedState{&iceHandler};
Unifier u{&arena, singletonTypes, Mode::Strict, NotNull{scope.get()}, Location{}, Variance::Covariant, sharedState}; Normalizer normalizer{&arena, singletonTypes, NotNull{&sharedState}};
Unifier u{NotNull{&normalizer}, Mode::Strict, NotNull{scope.get()}, Location{}, Variance::Covariant};
u.tryUnify(option1, option2); u.tryUnify(option1, option2);
@ -635,4 +612,87 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "for_in_loop_with_zero_iterators")
LUAU_REQUIRE_NO_ERRORS(result); LUAU_REQUIRE_NO_ERRORS(result);
} }
// Ideally, we would not try to export a function type with generic types from incorrect scope
TEST_CASE_FIXTURE(BuiltinsFixture, "generic_type_leak_to_module_interface")
{
ScopedFastFlag LuauAnyifyModuleReturnGenerics{"LuauAnyifyModuleReturnGenerics", true};
fileResolver.source["game/A"] = R"(
local wrapStrictTable
local metatable = {
__index = function(self, key)
local value = self.__tbl[key]
if type(value) == "table" then
-- unification of the free 'wrapStrictTable' with this function type causes generics of this function to leak out of scope
return wrapStrictTable(value, self.__name .. "." .. key)
end
return value
end,
}
return wrapStrictTable
)";
frontend.check("game/A");
fileResolver.source["game/B"] = R"(
local wrapStrictTable = require(game.A)
local Constants = {}
return wrapStrictTable(Constants, "Constants")
)";
frontend.check("game/B");
ModulePtr m = frontend.moduleResolver.modules["game/B"];
REQUIRE(m);
std::optional<TypeId> result = first(m->getModuleScope()->returnType);
REQUIRE(result);
CHECK(get<AnyTypeVar>(*result));
}
TEST_CASE_FIXTURE(BuiltinsFixture, "generic_type_leak_to_module_interface_variadic")
{
ScopedFastFlag LuauAnyifyModuleReturnGenerics{"LuauAnyifyModuleReturnGenerics", true};
fileResolver.source["game/A"] = R"(
local wrapStrictTable
local metatable = {
__index = function<T>(self, key, ...: T)
local value = self.__tbl[key]
if type(value) == "table" then
-- unification of the free 'wrapStrictTable' with this function type causes generics of this function to leak out of scope
return wrapStrictTable(value, self.__name .. "." .. key)
end
return ...
end,
}
return wrapStrictTable
)";
frontend.check("game/A");
fileResolver.source["game/B"] = R"(
local wrapStrictTable = require(game.A)
local Constants = {}
return wrapStrictTable(Constants, "Constants")
)";
frontend.check("game/B");
ModulePtr m = frontend.moduleResolver.modules["game/B"];
REQUIRE(m);
std::optional<TypeId> result = first(m->getModuleScope()->returnType);
REQUIRE(result);
CHECK(get<AnyTypeVar>(*result));
}
TEST_SUITE_END(); TEST_SUITE_END();

View file

@ -11,7 +11,8 @@
using namespace Luau; using namespace Luau;
LUAU_FASTFLAG(LuauLowerBoundsCalculation); LUAU_FASTFLAG(LuauLowerBoundsCalculation)
LUAU_FASTFLAG(LuauInstantiateInSubtyping)
TEST_SUITE_BEGIN("TableTests"); TEST_SUITE_BEGIN("TableTests");
@ -2038,11 +2039,22 @@ caused by:
caused by: caused by:
Property 'y' is not compatible. Type 'string' could not be converted into 'number')"); Property 'y' is not compatible. Type 'string' could not be converted into 'number')");
CHECK_EQ(toString(result.errors[1]), R"(Type 'b2' could not be converted into 'a2' if (FFlag::LuauInstantiateInSubtyping)
{
CHECK_EQ(toString(result.errors[1]), 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)");
}
else
{
CHECK_EQ(toString(result.errors[1]), R"(Type 'b2' could not be converted into 'a2'
caused by: caused by:
Type '{ __call: (a, b) -> () }' could not be converted into '{ __call: <a>(a) -> () }' Type '{ __call: (a, b) -> () }' could not be converted into '{ __call: <a>(a) -> () }'
caused by: caused by:
Property '__call' is not compatible. Type '(a, b) -> ()' could not be converted into '<a>(a) -> ()'; 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)");
}
} }
TEST_CASE_FIXTURE(Fixture, "error_detailed_indexer_key") TEST_CASE_FIXTURE(Fixture, "error_detailed_indexer_key")
@ -3173,4 +3185,53 @@ caused by:
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")));
} }
TEST_CASE_FIXTURE(Fixture, "invariant_table_properties_means_instantiating_tables_in_call_is_unsound")
{
ScopedFastFlag sff[]{
{"LuauInstantiateInSubtyping", true},
};
CheckResult result = check(R"(
--!strict
local t = {}
function t.m(x) return x end
local a : string = t.m("hi")
local b : number = t.m(5)
function f(x : { m : (number)->number })
x.m = function(x) return 1+x end
end
f(t) -- This shouldn't typecheck
local c : string = t.m("hi")
)");
LUAU_REQUIRE_ERRORS(result);
CHECK_EQ(toString(result.errors[0]), R"(Type 't' could not be converted into '{| m: (number) -> number |}'
caused by:
Property 'm' is not compatible. Type '<a>(a) -> a' could not be converted into '(number) -> number'; different number of generic type parameters)");
// this error message is not great since the underlying issue is that the context is invariant,
// and `(number) -> number` cannot be a subtype of `<a>(a) -> a`.
}
TEST_CASE_FIXTURE(BuiltinsFixture, "generic_table_instantiation_potential_regression")
{
CheckResult result = check(R"(
--!strict
function f(x)
x.p = 5
return x
end
local g : ({ p : number, q : string }) -> ({ p : number, r : boolean }) = f
)");
LUAU_REQUIRE_ERROR_COUNT(1, result);
MissingProperties* error = get<MissingProperties>(result.errors[0]);
REQUIRE(error != nullptr);
REQUIRE(error->properties.size() == 1);
CHECK_EQ("r", error->properties[0]);
}
TEST_SUITE_END(); TEST_SUITE_END();

View file

@ -17,7 +17,9 @@
LUAU_FASTFLAG(LuauLowerBoundsCalculation); LUAU_FASTFLAG(LuauLowerBoundsCalculation);
LUAU_FASTFLAG(LuauFixLocationSpanTableIndexExpr); LUAU_FASTFLAG(LuauFixLocationSpanTableIndexExpr);
LUAU_FASTFLAG(DebugLuauDeferredConstraintResolution); LUAU_FASTFLAG(DebugLuauDeferredConstraintResolution);
LUAU_FASTFLAG(LuauInstantiateInSubtyping);
LUAU_FASTFLAG(LuauSpecialTypesAsterisked); LUAU_FASTFLAG(LuauSpecialTypesAsterisked);
LUAU_FASTFLAG(LuauCheckGenericHOFTypes);
using namespace Luau; using namespace Luau;
@ -999,7 +1001,26 @@ TEST_CASE_FIXTURE(Fixture, "cli_50041_committing_txnlog_in_apollo_client_error")
end end
)"); )");
LUAU_REQUIRE_NO_ERRORS(result); if (FFlag::LuauInstantiateInSubtyping && !FFlag::LuauCheckGenericHOFTypes)
{
// though this didn't error before the flag, it seems as though it should error since fields of a table are invariant.
// the user's intent would likely be that these "method" fields would be read-only, but without an annotation, accepting this should be unsound.
LUAU_REQUIRE_ERROR_COUNT(1, result);
CHECK_EQ(R"(Type 't1 where t1 = {+ getStoreFieldName: (t1, {| fieldName: string |} & {| from: number? |}) -> (a, b...) +}' could not be converted into 'Policies'
caused by:
Property 'getStoreFieldName' is not compatible. Type 't1 where t1 = ({+ getStoreFieldName: t1 +}, {| fieldName: string |} & {| from: number? |}) -> (a, b...)' could not be converted into '(Policies, FieldSpecifier) -> string'
caused by:
Argument #2 type is not compatible. Type 'FieldSpecifier' could not be converted into 'FieldSpecifier & {| from: number? |}'
caused by:
Not all intersection parts are compatible. Table type 'FieldSpecifier' not compatible with type '{| from: number? |}' because the former has extra field 'fieldName')",
toString(result.errors[0]));
}
else
{
LUAU_REQUIRE_NO_ERRORS(result);
}
} }
TEST_CASE_FIXTURE(Fixture, "type_infer_recursion_limit_no_ice") TEST_CASE_FIXTURE(Fixture, "type_infer_recursion_limit_no_ice")
@ -1020,6 +1041,43 @@ TEST_CASE_FIXTURE(Fixture, "type_infer_recursion_limit_no_ice")
CHECK_EQ("Code is too complex to typecheck! Consider simplifying the code around this area", toString(result.errors[0])); CHECK_EQ("Code is too complex to typecheck! Consider simplifying the code around this area", toString(result.errors[0]));
} }
TEST_CASE_FIXTURE(Fixture, "type_infer_recursion_limit_normalizer")
{
ScopedFastInt sfi("LuauTypeInferRecursionLimit", 10);
ScopedFastFlag sffs[] {
{"LuauSubtypeNormalizer", true},
{"LuauTypeNormalization2", true},
{"LuauAutocompleteDynamicLimits", true},
};
CheckResult result = check(R"(
function f<a,b,c,d,e,f,g,h,i,j>()
local x : a&b&c&d&e&f&g&h&(i?)
local y : (a&b&c&d&e&f&g&h&i)? = x
end
)");
LUAU_REQUIRE_ERRORS(result);
CHECK_EQ("Internal error: Code is too complex to typecheck! Consider adding type annotations around this area", toString(result.errors[0]));
}
TEST_CASE_FIXTURE(Fixture, "type_infer_cache_limit_normalizer")
{
ScopedFastInt sfi("LuauNormalizeCacheLimit", 10);
ScopedFastFlag sffs[] {
{"LuauSubtypeNormalizer", true},
{"LuauTypeNormalization2", true},
};
CheckResult result = check(R"(
local x : ((number) -> number) & ((string) -> string) & ((nil) -> nil) & (({}) -> {})
local y : (number | string | nil | {}) -> (number | string | nil | {}) = x
)");
LUAU_REQUIRE_ERRORS(result);
CHECK_EQ("Internal error: Code is too complex to typecheck! Consider adding type annotations around this area", toString(result.errors[0]));
}
TEST_CASE_FIXTURE(Fixture, "follow_on_new_types_in_substitution") TEST_CASE_FIXTURE(Fixture, "follow_on_new_types_in_substitution")
{ {
CheckResult result = check(R"( CheckResult result = check(R"(

View file

@ -17,8 +17,8 @@ struct TryUnifyFixture : Fixture
ScopePtr globalScope{new Scope{arena.addTypePack({TypeId{}})}}; ScopePtr globalScope{new Scope{arena.addTypePack({TypeId{}})}};
InternalErrorReporter iceHandler; InternalErrorReporter iceHandler;
UnifierSharedState unifierState{&iceHandler}; UnifierSharedState unifierState{&iceHandler};
Normalizer normalizer{&arena, singletonTypes, NotNull{&unifierState}};
Unifier state{&arena, singletonTypes, Mode::Strict, NotNull{globalScope.get()}, Location{}, Variance::Covariant, unifierState}; Unifier state{NotNull{&normalizer}, Mode::Strict, NotNull{globalScope.get()}, Location{}, Variance::Covariant};
}; };
TEST_SUITE_BEGIN("TryUnifyTests"); TEST_SUITE_BEGIN("TryUnifyTests");

View file

@ -1000,4 +1000,23 @@ TEST_CASE_FIXTURE(Fixture, "unify_variadic_tails_in_arguments_free")
CHECK_EQ(toString(result.errors[0]), "Type 'number' could not be converted into 'boolean'"); CHECK_EQ(toString(result.errors[0]), "Type 'number' could not be converted into 'boolean'");
} }
TEST_CASE_FIXTURE(BuiltinsFixture, "type_packs_with_tails_in_vararg_adjustment")
{
ScopedFastFlag luauFixVarargExprHeadType{"LuauFixVarargExprHeadType", true};
CheckResult result = check(R"(
local function wrapReject<TArg, TResult>(fn: (self: any, ...TArg) -> ...TResult): (self: any, ...TArg) -> ...TResult
return function(self, ...)
local arguments = { ... }
local ok, result = pcall(function()
return fn(self, table.unpack(arguments))
end)
return result
end
end
)");
LUAU_REQUIRE_NO_ERRORS(result);
}
TEST_SUITE_END(); TEST_SUITE_END();

View file

@ -541,5 +541,182 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "table_union_write_indirect")
R"(Type '(string) -> number' could not be converted into '((number) -> string) | ((number) -> string)'; none of the union options are compatible)"); R"(Type '(string) -> number' could not be converted into '((number) -> string) | ((number) -> string)'; none of the union options are compatible)");
} }
TEST_CASE_FIXTURE(Fixture, "union_true_and_false")
{
ScopedFastFlag sffs[] {
{"LuauSubtypeNormalizer", true},
{"LuauTypeNormalization2", true},
};
CheckResult result = check(R"(
local x : boolean
local y1 : (true | false) = x -- OK
local y2 : (true | false | (string & number)) = x -- OK
local y3 : (true | (string & number) | false) = x -- OK
local y4 : (true | (boolean & true) | false) = x -- OK
)");
LUAU_REQUIRE_NO_ERRORS(result);
}
TEST_CASE_FIXTURE(Fixture, "union_of_functions")
{
ScopedFastFlag sffs[] {
{"LuauSubtypeNormalizer", true},
{"LuauTypeNormalization2", true},
};
CheckResult result = check(R"(
local x : (number) -> number?
local y : ((number?) -> number?) | ((number) -> number) = x -- OK
)");
LUAU_REQUIRE_NO_ERRORS(result);
}
TEST_CASE_FIXTURE(Fixture, "union_of_generic_functions")
{
CheckResult result = check(R"(
local x : <a>(a) -> a?
local y : (<a>(a?) -> a?) | (<b>(b) -> b) = x -- Not OK
)");
// TODO: should this example typecheck?
LUAU_REQUIRE_ERRORS(result);
}
TEST_CASE_FIXTURE(Fixture, "union_of_generic_typepack_functions")
{
CheckResult result = check(R"(
local x : <a...>(number, a...) -> (number?, a...)
local y : (<a...>(number?, a...) -> (number?, a...)) | (<b...>(number, b...) -> (number, b...)) = x -- Not OK
)");
// TODO: should this example typecheck?
LUAU_REQUIRE_ERRORS(result);
}
TEST_CASE_FIXTURE(Fixture, "union_of_functions_mentioning_generics")
{
ScopedFastFlag sffs[] {
{"LuauSubtypeNormalizer", true},
{"LuauTypeNormalization2", true},
};
CheckResult result = check(R"(
function f<a,b>()
local x : (a) -> a?
local y : ((a?) -> nil) | ((a) -> a) = x -- OK
local z : ((b?) -> nil) | ((b) -> b) = x -- Not OK
end
)");
LUAU_REQUIRE_ERROR_COUNT(1, result);
CHECK_EQ(toString(result.errors[0]), "Type '(a) -> a?' could not be converted into '((b) -> b) | ((b?) -> nil)'; none of the union options are compatible");
}
TEST_CASE_FIXTURE(Fixture, "union_of_functions_mentioning_generic_typepacks")
{
ScopedFastFlag sffs[] {
{"LuauSubtypeNormalizer", true},
{"LuauTypeNormalization2", true},
};
CheckResult result = check(R"(
function f<a...>()
local x : (number, a...) -> (number?, a...)
local y : ((number | string, a...) -> (number, a...)) | ((number?, a...) -> (nil, a...)) = x -- OK
local z : ((number) -> number) | ((number?, a...) -> (number?, a...)) = x -- Not OK
end
)");
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?, a...) -> (number?, a...))'; none of the union options are compatible");
}
TEST_CASE_FIXTURE(Fixture, "union_of_functions_with_mismatching_arg_arities")
{
ScopedFastFlag sffs[] {
{"LuauSubtypeNormalizer", true},
{"LuauTypeNormalization2", true},
};
CheckResult result = check(R"(
local x : (number) -> number?
local y : ((number?) -> number) | ((number | string) -> nil) = x -- OK
local z : ((number, string?) -> number) | ((number) -> nil) = x -- Not OK
)");
LUAU_REQUIRE_ERROR_COUNT(1, result);
CHECK_EQ(toString(result.errors[0]), "Type '(number) -> number?' could not be converted into '((number) -> nil) | ((number, string?) -> number)'; none of the union options are compatible");
}
TEST_CASE_FIXTURE(Fixture, "union_of_functions_with_mismatching_result_arities")
{
ScopedFastFlag sffs[] {
{"LuauSubtypeNormalizer", true},
{"LuauTypeNormalization2", true},
};
CheckResult result = check(R"(
local x : () -> (number | string)
local y : (() -> number) | (() -> string) = x -- OK
local z : (() -> number) | (() -> (string, string)) = x -- Not OK
)");
LUAU_REQUIRE_ERROR_COUNT(1, result);
CHECK_EQ(toString(result.errors[0]), "Type '() -> number | string' could not be converted into '(() -> (string, string)) | (() -> number)'; none of the union options are compatible");
}
TEST_CASE_FIXTURE(Fixture, "union_of_functions_with_variadics")
{
ScopedFastFlag sffs[] {
{"LuauSubtypeNormalizer", true},
{"LuauTypeNormalization2", true},
};
CheckResult result = check(R"(
local x : (...nil) -> (...number?)
local y : ((...string?) -> (...number)) | ((...number?) -> nil) = x -- OK
local z : ((...string?) -> (...number)) | ((...string?) -> nil) = x -- OK
)");
LUAU_REQUIRE_ERROR_COUNT(1, result);
CHECK_EQ(toString(result.errors[0]), "Type '(...nil) -> (...number?)' could not be converted into '((...string?) -> (...number)) | ((...string?) -> nil)'; none of the union options are compatible");
}
TEST_CASE_FIXTURE(Fixture, "union_of_functions_with_mismatching_arg_variadics")
{
ScopedFastFlag sffs[] {
{"LuauSubtypeNormalizer", true},
{"LuauTypeNormalization2", true},
};
CheckResult result = check(R"(
local x : (number) -> ()
local y : ((number?) -> ()) | ((...number) -> ()) = x -- OK
local z : ((number?) -> ()) | ((...number?) -> ()) = x -- Not OK
)");
LUAU_REQUIRE_ERROR_COUNT(1, result);
CHECK_EQ(toString(result.errors[0]), "Type '(number) -> ()' could not be converted into '((...number?) -> ()) | ((number?) -> ())'; none of the union options are compatible");
}
TEST_CASE_FIXTURE(Fixture, "union_of_functions_with_mismatching_result_variadics")
{
ScopedFastFlag sffs[] {
{"LuauSubtypeNormalizer", true},
{"LuauTypeNormalization2", true},
};
CheckResult result = check(R"(
local x : () -> (number?, ...number)
local y : (() -> (...number)) | (() -> nil) = x -- OK
local z : (() -> (...number)) | (() -> number) = x -- OK
)");
LUAU_REQUIRE_ERROR_COUNT(1, result);
CHECK_EQ(toString(result.errors[0]), "Type '() -> (number?, ...number)' could not be converted into '(() -> (...number)) | (() -> number)'; none of the union options are compatible");
}
TEST_SUITE_END(); TEST_SUITE_END();

View file

@ -508,6 +508,9 @@ assert((function() function cmp(a,b) return a<b,a<=b,a>b,a>=b end return concat(
assert((function() function cmp(a,b) return a<b,a<=b,a>b,a>=b end return concat(cmp('abc', 'abd')) end)() == "true,true,false,false") assert((function() function cmp(a,b) return a<b,a<=b,a>b,a>=b end return concat(cmp('abc', 'abd')) end)() == "true,true,false,false")
assert((function() function cmp(a,b) return a<b,a<=b,a>b,a>=b end return concat(cmp('ab\\0c', 'ab\\0d')) end)() == "true,true,false,false") assert((function() function cmp(a,b) return a<b,a<=b,a>b,a>=b end return concat(cmp('ab\\0c', 'ab\\0d')) end)() == "true,true,false,false")
assert((function() function cmp(a,b) return a<b,a<=b,a>b,a>=b end return concat(cmp('ab\\0c', 'ab\\0')) end)() == "false,false,true,true") assert((function() function cmp(a,b) return a<b,a<=b,a>b,a>=b end return concat(cmp('ab\\0c', 'ab\\0')) end)() == "false,false,true,true")
assert((function() function cmp(a,b) return a<b,a<=b,a>b,a>=b end return concat(cmp('\\0a', '\\0b')) end)() == "true,true,false,false")
assert((function() function cmp(a,b) return a<b,a<=b,a>b,a>=b end return concat(cmp('a', 'a\\0')) end)() == "true,true,false,false")
assert((function() function cmp(a,b) return a<b,a<=b,a>b,a>=b end return concat(cmp('a', '\200')) end)() == "true,true,false,false")
-- array access -- array access
assert((function() local a = {4,5,6} return a[3] end)() == 6) assert((function() local a = {4,5,6} return a[3] end)() == 6)

View file

@ -21,6 +21,7 @@
#include <sys/sysctl.h> #include <sys/sysctl.h>
#endif #endif
#include <random>
#include <optional> #include <optional>
// Indicates if verbose output is enabled; can be overridden via --verbose // Indicates if verbose output is enabled; can be overridden via --verbose
@ -30,6 +31,10 @@ bool verbose = false;
// Default optimization level for conformance test; can be overridden via -On // Default optimization level for conformance test; can be overridden via -On
int optimizationLevel = 1; int optimizationLevel = 1;
// Something to seed a pseudorandom number generator with. Defaults to
// something from std::random_device.
std::optional<unsigned> randomSeed;
static bool skipFastFlag(const char* flagName) static bool skipFastFlag(const char* flagName)
{ {
if (strncmp(flagName, "Test", 4) == 0) if (strncmp(flagName, "Test", 4) == 0)
@ -261,6 +266,16 @@ int main(int argc, char** argv)
optimizationLevel = level; optimizationLevel = level;
} }
int rseed = -1;
if (doctest::parseIntOption(argc, argv, "--random-seed=", doctest::option_int, rseed))
randomSeed = unsigned(rseed);
if (doctest::parseOption(argc, argv, "--randomize") && !randomSeed)
{
randomSeed = std::random_device()();
printf("Using RNG seed %u\n", *randomSeed);
}
if (std::vector<doctest::String> flags; doctest::parseCommaSepArgs(argc, argv, "--fflags=", flags)) if (std::vector<doctest::String> flags; doctest::parseCommaSepArgs(argc, argv, "--fflags=", flags))
setFastFlags(flags); setFastFlags(flags);
@ -295,6 +310,8 @@ int main(int argc, char** argv)
printf(" --verbose Enables verbose output (e.g. lua 'print' statements)\n"); printf(" --verbose Enables verbose output (e.g. lua 'print' statements)\n");
printf(" --fflags= Sets specified fast flags\n"); printf(" --fflags= Sets specified fast flags\n");
printf(" --list-fflags List all fast flags\n"); printf(" --list-fflags List all fast flags\n");
printf(" --randomize Use a random RNG seed\n");
printf(" --random-seed=n Use a particular RNG seed\n");
} }
return result; return result;
} }

View file

@ -5,3 +5,6 @@ type synthetic add -x "^Luau::Variant<.+>$" -l lldb_formatters.LuauVariantSynthe
type summary add -x "^Luau::Variant<.+>$" -F lldb_formatters.luau_variant_summary type summary add -x "^Luau::Variant<.+>$" -F lldb_formatters.luau_variant_summary
type synthetic add -x "^Luau::AstArray<.+>$" -l lldb_formatters.AstArraySyntheticChildrenProvider type synthetic add -x "^Luau::AstArray<.+>$" -l lldb_formatters.AstArraySyntheticChildrenProvider
type summary add --summary-string "${var.line}:${var.column}" Luau::Position
type summary add --summary-string "${var.begin}-${var.end}" Luau::Location

View file

@ -22,4 +22,25 @@
</Expand> </Expand>
</Type> </Type>
<Type Name="Luau::AstName">
<DisplayString>{value,na}</DisplayString>
</Type>
<Type Name="Luau::AstLocal">
<DisplayString>{name.value,na}</DisplayString>
</Type>
<Type Name="Luau::Symbol">
<DisplayString Condition="local">local {local->name.value,na}</DisplayString>
<DisplayString>global {global.value,na}</DisplayString>
</Type>
<Type Name="Luau::Position">
<DisplayString>{line}:{column}</DisplayString>
</Type>
<Type Name="Luau::Location">
<DisplayString>{begin}-{end}</DisplayString>
</Type>
</AutoVisualizer> </AutoVisualizer>

View file

@ -22,6 +22,10 @@ def safeParseInt(i, default=0):
return default return default
def makeDottedName(path):
return ".".join(path)
class Handler(x.ContentHandler): class Handler(x.ContentHandler):
def __init__(self, failList): def __init__(self, failList):
self.currentTest = [] self.currentTest = []
@ -41,7 +45,7 @@ class Handler(x.ContentHandler):
if self.currentTest: if self.currentTest:
passed = attrs["test_case_success"] == "true" passed = attrs["test_case_success"] == "true"
dottedName = ".".join(self.currentTest) dottedName = makeDottedName(self.currentTest)
# Sometimes we get multiple XML trees for the same test. All of # Sometimes we get multiple XML trees for the same test. All of
# them must report a pass in order for us to consider the test # them must report a pass in order for us to consider the test
@ -60,6 +64,10 @@ class Handler(x.ContentHandler):
self.currentTest.pop() self.currentTest.pop()
def print_stderr(*args, **kw):
print(*args, **kw, file=sys.stderr)
def main(): def main():
parser = argparse.ArgumentParser( parser = argparse.ArgumentParser(
description="Run Luau.UnitTest with deferred constraint resolution enabled" description="Run Luau.UnitTest with deferred constraint resolution enabled"
@ -80,6 +88,16 @@ def main():
help="Write a new faillist.txt after running tests.", help="Write a new faillist.txt after running tests.",
) )
parser.add_argument("--randomize", action="store_true", help="Pick a random seed")
parser.add_argument(
"--random-seed",
action="store",
dest="random_seed",
type=int,
help="Accept a specific RNG seed",
)
args = parser.parse_args() args = parser.parse_args()
failList = loadFailList() failList = loadFailList()
@ -90,7 +108,12 @@ def main():
"--fflags=true,DebugLuauDeferredConstraintResolution=true", "--fflags=true,DebugLuauDeferredConstraintResolution=true",
] ]
print('>', ' '.join(commandLine), file=sys.stderr) if args.random_seed:
commandLine.append("--random-seed=" + str(args.random_seed))
elif args.randomize:
commandLine.append("--randomize")
print_stderr(">", " ".join(commandLine))
p = sp.Popen( p = sp.Popen(
commandLine, commandLine,
@ -104,15 +127,21 @@ def main():
sys.stdout.buffer.write(line) sys.stdout.buffer.write(line)
return return
else: else:
x.parse(p.stdout, handler) try:
x.parse(p.stdout, handler)
except x.SAXParseException as e:
print_stderr(
f"XML parsing failed during test {makeDottedName(handler.currentTest)}. That probably means that the test crashed"
)
sys.exit(1)
p.wait() p.wait()
for testName, passed in handler.results.items(): for testName, passed in handler.results.items():
if passed and testName in failList: if passed and testName in failList:
print("UNEXPECTED: {} should have failed".format(testName)) print_stderr(f"UNEXPECTED: {testName} should have failed")
elif not passed and testName not in failList: elif not passed and testName not in failList:
print("UNEXPECTED: {} should have passed".format(testName)) print_stderr(f"UNEXPECTED: {testName} should have passed")
if args.write: if args.write:
newFailList = sorted( newFailList = sorted(
@ -126,14 +155,11 @@ def main():
with open(FAIL_LIST_PATH, "w", newline="\n") as f: with open(FAIL_LIST_PATH, "w", newline="\n") as f:
for name in newFailList: for name in newFailList:
print(name, file=f) print(name, file=f)
print("Updated faillist.txt", file=sys.stderr) print_stderr("Updated faillist.txt")
if handler.numSkippedTests > 0: if handler.numSkippedTests > 0:
print( print_stderr(
"{} test(s) were skipped! That probably means that a test segfaulted!".format( f"{handler.numSkippedTests} test(s) were skipped! That probably means that a test segfaulted!"
handler.numSkippedTests
),
file=sys.stderr,
) )
sys.exit(1) sys.exit(1)
@ -143,7 +169,7 @@ def main():
) )
if ok: if ok:
print("Everything in order!", file=sys.stderr) print_stderr("Everything in order!")
sys.exit(0 if ok else 1) sys.exit(0 if ok else 1)