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

View file

@ -16,7 +16,7 @@ struct TypeMismatch
TypeMismatch() = default;
TypeMismatch(TypeId wantedType, TypeId givenType);
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 givenType = nullptr;

View file

@ -83,8 +83,13 @@ struct FrontendOptions
// is complete.
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;
// 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

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
#pragma once
#include "Luau/Module.h"
#include "Luau/NotNull.h"
#include "Luau/TypeVar.h"
#include "Luau/UnifierSharedState.h"
#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, 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

View file

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

View file

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

View file

@ -9,6 +9,7 @@
#include "Luau/TxnLog.h"
#include "Luau/TypeArena.h"
#include "Luau/UnifierSharedState.h"
#include "Normalize.h"
#include <unordered_set>
@ -52,6 +53,7 @@ struct Unifier
{
TypeArena* const types;
NotNull<SingletonTypes> singletonTypes;
NotNull<Normalizer> normalizer;
Mode mode;
NotNull<Scope> scope; // const Scope maybe
@ -60,13 +62,14 @@ struct Unifier
Location location;
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 normalize; // Normalize unions and intersections if necessary
bool useScopes = false; // If true, we use the scope hierarchy rather than TypeLevels
CountMismatch::Context ctx = CountMismatch::Arg;
UnifierSharedState& sharedState;
Unifier(TypeArena* types, NotNull<SingletonTypes> singletonTypes, Mode mode, NotNull<Scope> scope, const Location& location, Variance variance,
UnifierSharedState& sharedState, TxnLog* parentLog = nullptr);
Unifier(NotNull<Normalizer> normalizer, Mode mode, NotNull<Scope> scope, const Location& location, Variance variance,
TxnLog* parentLog = nullptr);
// Test whether the two type vars unify. Never commits the result.
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 tryUnifyTypeWithIntersection(TypeId subTy, TypeId superTy, const IntersectionTypeVar* uv);
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 tryUnifySingletons(TypeId subTy, TypeId superTy);
void tryUnifyFunctions(TypeId subTy, TypeId superTy, bool isFunctionCall = false);
@ -92,6 +96,8 @@ private:
void tryUnifyWithMetatable(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);
TypePackId widen(TypePackId tp);

View file

@ -12,8 +12,6 @@
#include <unordered_set>
#include <utility>
LUAU_FASTFLAG(LuauSelfCallAutocompleteFix3)
static const std::unordered_set<std::string> kStatementStartingKeywords = {
"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;
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();
}
@ -151,18 +150,6 @@ static TypeCorrectKind checkTypeCorrectKind(
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);
if (!typeAtPosition)
@ -170,30 +157,11 @@ static TypeCorrectKind checkTypeCorrectKind(
TypeId expectedType = follow(*typeAtPosition);
auto checkFunctionType = [typeArena, singletonTypes, moduleScope, &canUnify, &expectedType](const FunctionTypeVar* ftv) {
if (FFlag::LuauSelfCallAutocompleteFix3)
{
auto checkFunctionType = [typeArena, singletonTypes, moduleScope, &expectedType](const FunctionTypeVar* ftv) {
if (std::optional<TypeId> firstRetTy = first(ftv->retTypes))
return checkTypeMatch(*firstRetTy, expectedType, moduleScope, typeArena, singletonTypes);
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
@ -212,11 +180,8 @@ static TypeCorrectKind checkTypeCorrectKind(
}
}
if (FFlag::LuauSelfCallAutocompleteFix3)
return checkTypeMatch(ty, expectedType, NotNull{module.getModuleScope().get()}, typeArena, singletonTypes) ? TypeCorrectKind::Correct
: TypeCorrectKind::None;
else
return canUnify(ty, expectedType) ? TypeCorrectKind::Correct : TypeCorrectKind::None;
}
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,
std::optional<const ClassTypeVar*> containingClass = std::nullopt)
{
if (FFlag::LuauSelfCallAutocompleteFix3)
rootTy = follow(rootTy);
ty = follow(ty);
if (seen.count(ty))
return;
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) {
LUAU_ASSERT(FFlag::LuauSelfCallAutocompleteFix3);
if (indexType == PropIndexType::Key)
return false;
@ -337,7 +265,7 @@ static void autocompleteProps(const Module& module, TypeArena* typeArena, NotNul
AutocompleteEntryKind::Property,
type,
prop.deprecated,
FFlag::LuauSelfCallAutocompleteFix3 ? isWrongIndexer(type) : isWrongIndexer_DEPRECATED(type),
isWrongIndexer(type),
typeCorrect,
containingClass,
&prop,
@ -380,32 +308,9 @@ static void autocompleteProps(const Module& module, TypeArena* typeArena, NotNul
{
autocompleteProps(module, typeArena, singletonTypes, rootTy, mt->table, indexType, nodes, result, seen);
if (FFlag::LuauSelfCallAutocompleteFix3)
{
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))
{
// Complete all properties in every variant
@ -446,9 +351,6 @@ static void autocompleteProps(const Module& module, TypeArena* typeArena, NotNul
AutocompleteEntryMap inner;
std::unordered_set<TypeId> innerSeen;
if (!FFlag::LuauSelfCallAutocompleteFix3)
innerSeen = seen;
if (isNil(*iter))
{
++iter;
@ -472,7 +374,7 @@ static void autocompleteProps(const Module& module, TypeArena* typeArena, NotNul
++iter;
}
}
else if (auto pt = get<PrimitiveTypeVar>(ty); pt && FFlag::LuauSelfCallAutocompleteFix3)
else if (auto pt = get<PrimitiveTypeVar>(ty))
{
if (pt->metatable)
{
@ -480,7 +382,7 @@ static void autocompleteProps(const Module& module, TypeArena* typeArena, NotNul
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);
}
@ -1416,10 +1318,6 @@ static AutocompleteResult autocomplete(const SourceModule& sourceModule, const M
TypeId ty = follow(*it);
PropIndexType indexType = indexName->op == ':' ? PropIndexType::Colon : PropIndexType::Point;
if (!FFlag::LuauSelfCallAutocompleteFix3 && isString(ty))
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>())

View file

@ -14,6 +14,8 @@
#include "Luau/VisitTypeVar.h"
#include "Luau/TypeUtils.h"
#include <random>
LUAU_FASTFLAGVARIABLE(DebugLuauLogSolver, false);
LUAU_FASTFLAGVARIABLE(DebugLuauLogSolverToJson, false);
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)
: arena(arena)
, singletonTypes(singletonTypes)
: arena(normalizer->arena)
, singletonTypes(normalizer->singletonTypes)
, normalizer(normalizer)
, constraints(collectConstraints(rootScope))
, rootScope(rootScope)
, currentModuleName(std::move(moduleName))
@ -278,6 +281,12 @@ ConstraintSolver::ConstraintSolver(TypeArena* arena, NotNull<SingletonTypes> sin
LUAU_ASSERT(logger);
}
void ConstraintSolver::randomize(unsigned seed)
{
std::mt19937 g(seed);
std::shuffle(begin(unsolvedConstraints), end(unsolvedConstraints), g);
}
void ConstraintSolver::run()
{
if (done())
@ -1355,8 +1364,7 @@ bool ConstraintSolver::isBlocked(NotNull<const Constraint> constraint)
void ConstraintSolver::unify(TypeId subType, TypeId superType, NotNull<Scope> scope)
{
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.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)
{
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.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)
, givenType(givenType)
, 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 ScopePtr& globalScope{forAutocomplete ? typeCheckerForAutocomplete.globalScope : typeChecker.globalScope};
Normalizer normalizer{&result->internalTypes, singletonTypes, NotNull{&typeChecker.unifierState}};
ConstraintGraphBuilder cgb{
sourceModule.name, result, &result->internalTypes, mr, singletonTypes, NotNull(&iceHandler), globalScope, logger.get()};
cgb.visit(sourceModule.root);
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();
for (TypeError& e : cs.errors)

View file

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

File diff suppressed because it is too large Load diff

View file

@ -280,7 +280,8 @@ struct TypeChecker2
TypePackId actualRetType = reconstructPack(ret->list, arena);
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.tryUnify(actualRetType, expectedRetType);
@ -1206,7 +1207,8 @@ struct TypeChecker2
ErrorVec tryUnify(NotNull<Scope> scope, const Location& location, TID subTy, TID superTy)
{
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.tryUnify(subTy, superTy);

View file

@ -32,19 +32,21 @@ LUAU_FASTINTVARIABLE(LuauCheckRecursionLimit, 300)
LUAU_FASTINTVARIABLE(LuauVisitRecursionLimit, 500)
LUAU_FASTFLAG(LuauKnowsTheDataModel3)
LUAU_FASTFLAG(LuauAutocompleteDynamicLimits)
LUAU_FASTFLAG(LuauTypeNormalization2)
LUAU_FASTFLAGVARIABLE(LuauFunctionArgMismatchDetails, false)
LUAU_FASTFLAGVARIABLE(LuauInplaceDemoteSkipAllBound, false)
LUAU_FASTFLAGVARIABLE(LuauLowerBoundsCalculation, false)
LUAU_FASTFLAGVARIABLE(DebugLuauFreezeDuringUnification, false)
LUAU_FASTFLAGVARIABLE(LuauSelfCallAutocompleteFix3, false)
LUAU_FASTFLAGVARIABLE(LuauReturnAnyInsteadOfICE, false) // Eventually removed as false.
LUAU_FASTFLAGVARIABLE(DebugLuauSharedSelf, false)
LUAU_FASTFLAGVARIABLE(LuauAnyifyModuleReturnGenerics, false)
LUAU_FASTFLAGVARIABLE(LuauUnknownAndNeverType, false)
LUAU_FASTFLAGVARIABLE(LuauCallUnifyPackTails, false)
LUAU_FASTFLAGVARIABLE(LuauCheckGenericHOFTypes, false)
LUAU_FASTFLAGVARIABLE(LuauBinaryNeedsExpectedTypesToo, false)
LUAU_FASTFLAGVARIABLE(LuauFixVarargExprHeadType, false)
LUAU_FASTFLAGVARIABLE(LuauNeverTypesAndOperatorsInference, false)
LUAU_FASTFLAGVARIABLE(LuauReturnsFromCallsitesAreNotWidened, false)
LUAU_FASTFLAG(LuauInstantiateInSubtyping)
LUAU_FASTFLAGVARIABLE(LuauCompleteVisitor, false)
LUAU_FASTFLAGVARIABLE(LuauUnionOfTypesFollow, false)
LUAU_FASTFLAGVARIABLE(LuauReportShadowedTypeAlias, false)
@ -255,6 +257,7 @@ TypeChecker::TypeChecker(ModuleResolver* resolver, NotNull<SingletonTypes> singl
, singletonTypes(singletonTypes)
, iceHandler(iceHandler)
, unifierState(iceHandler)
, normalizer(nullptr, singletonTypes, NotNull{&unifierState})
, nilType(singletonTypes->nilType)
, numberType(singletonTypes->numberType)
, stringType(singletonTypes->stringType)
@ -301,12 +304,13 @@ ModulePtr TypeChecker::checkWithoutRecursionCheck(const SourceModule& module, Mo
LUAU_TIMETRACE_SCOPE("TypeChecker::check", "TypeChecker");
LUAU_TIMETRACE_ARGUMENT("module", module.name.c_str());
currentModule.reset(new Module());
currentModule.reset(new Module);
currentModule->type = module.type;
currentModule->allocator = module.allocator;
currentModule->names = module.names;
iceHandler->moduleName = module.name;
normalizer.arena = &currentModule->internalTypes;
if (FFlag::LuauAutocompleteDynamicLimits)
{
@ -351,15 +355,23 @@ ModulePtr TypeChecker::checkWithoutRecursionCheck(const SourceModule& module, Mo
if (get<FreeTypePack>(follow(moduleScope->returnType)))
moduleScope->returnType = addTypePack(TypePack{{}, std::nullopt});
else
{
moduleScope->returnType = anyify(moduleScope, moduleScope->returnType, Location{});
}
if (FFlag::LuauAnyifyModuleReturnGenerics)
moduleScope->returnType = anyifyModuleReturnTypePackGenerics(moduleScope->returnType);
for (auto& [_, typeFun] : moduleScope->exportedTypeBindings)
typeFun.type = anyify(moduleScope, typeFun.type, Location{});
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);
// Clear unifier cache since it's keyed off internal types that get deallocated
@ -474,7 +486,7 @@ struct InplaceDemoter : TypeVarOnceVisitor
TypeArena* arena;
InplaceDemoter(TypeLevel level, TypeArena* arena)
: TypeVarOnceVisitor(/* skipBoundTypes= */ FFlag::LuauInplaceDemoteSkipAllBound)
: TypeVarOnceVisitor(/* skipBoundTypes= */ true)
, newLevel(level)
, arena(arena)
{
@ -494,12 +506,6 @@ struct InplaceDemoter : TypeVarOnceVisitor
return false;
}
bool visit(TypeId ty, const BoundTypeVar& btyRef) override
{
LUAU_ASSERT(!FFlag::LuauInplaceDemoteSkipAllBound);
return true;
}
bool visit(TypeId ty) override
{
if (ty->owningArena != arena)
@ -1028,9 +1034,12 @@ void TypeChecker::check(const ScopePtr& scope, const AstStatAssign& assign)
}
if (right)
{
if (!FFlag::LuauInstantiateInSubtyping)
{
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
const TableTypeVar* destTableTypeReceivingNil = nullptr;
@ -1104,6 +1113,8 @@ void TypeChecker::check(const ScopePtr& scope, const AstStatLocal& local)
variableTypes.push_back(ty);
expectedTypes.push_back(ty);
// with FFlag::LuauInstantiateInSubtyping enabled, we shouldn't need to produce instantiateGenerics at all.
if (!FFlag::LuauInstantiateInSubtyping)
instantiateGenerics.push_back(annotation != nullptr && !maybeGeneric(ty));
}
@ -1729,8 +1740,6 @@ void TypeChecker::check(const ScopePtr& scope, const AstStatDeclareClass& declar
{
ftv->argNames.insert(ftv->argNames.begin(), FunctionArgument{"self", {}});
ftv->argTypes = addTypePack(TypePack{{classTy}, ftv->argTypes});
if (FFlag::LuauSelfCallAutocompleteFix3)
ftv->hasSelf = true;
}
}
@ -1904,10 +1913,20 @@ WithPredicate<TypeId> TypeChecker::checkExpr(const ScopePtr& scope, const AstExp
TypePackId varargPack = checkExprPack(scope, expr).type;
if (get<TypePack>(varargPack))
{
if (FFlag::LuauFixVarargExprHeadType)
{
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))
{
TypeId head = freshType(scope);
@ -3967,6 +3986,9 @@ void TypeChecker::checkArgumentList(const ScopePtr& scope, const AstExpr& funNam
}
else
{
if (FFlag::LuauInstantiateInSubtyping)
state.tryUnify(*argIter, *paramIter, /*isFunctionCall*/ false);
else
unifyWithInstantiationIfNeeded(*argIter, *paramIter, scope, state);
++argIter;
++paramIter;
@ -4523,8 +4545,11 @@ WithPredicate<TypePackId> TypeChecker::checkExprList(const ScopePtr& scope, cons
TypeId actualType = substituteFreeForNil && expr->is<AstExprConstantNil>() ? freshType(scope) : type;
if (!FFlag::LuauInstantiateInSubtyping)
{
if (instantiateGenerics.size() > i && instantiateGenerics[i])
actualType = instantiate(scope, actualType, expr->location);
}
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)
{
LUAU_ASSERT(!FFlag::LuauInstantiateInSubtyping);
if (!maybeGeneric(subTy))
// Quick check to see if we definitely can't instantiate
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)
{
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)
{
return Unifier{
&currentModule->internalTypes, singletonTypes, currentModule->mode, NotNull{scope.get()}, location, Variance::Covariant, unifierState};
return Unifier{NotNull{&normalizer}, currentModule->mode, NotNull{scope.get()}, location, Variance::Covariant};
}
TypeId TypeChecker::freshType(const ScopePtr& scope)

View file

@ -27,6 +27,7 @@ LUAU_FASTFLAG(LuauUnknownAndNeverType)
LUAU_FASTFLAGVARIABLE(LuauMaybeGenericIntersectionTypes, false)
LUAU_FASTFLAGVARIABLE(LuauStringFormatArgumentErrorFix, false)
LUAU_FASTFLAGVARIABLE(LuauNoMoreGlobalSingletonTypes, false)
LUAU_FASTFLAG(LuauInstantiateInSubtyping)
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.
bool isGeneric(TypeId ty)
{
LUAU_ASSERT(!FFlag::LuauInstantiateInSubtyping);
ty = follow(ty);
if (auto ftv = get<FunctionTypeVar>(ty))
return ftv->generics.size() > 0 || ftv->genericPacks.size() > 0;
@ -350,6 +353,8 @@ bool isGeneric(TypeId ty)
bool maybeGeneric(TypeId ty)
{
LUAU_ASSERT(!FFlag::LuauInstantiateInSubtyping);
if (FFlag::LuauMaybeGenericIntersectionTypes)
{
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
#include "Luau/Unifiable.h"
LUAU_FASTFLAG(LuauTypeNormalization2);
namespace Luau
{
namespace Unifiable
{
static int nextIndex = 0;
Free::Free(TypeLevel level)
: index(++nextIndex)
: index(FFlag::LuauTypeNormalization2 ? ++nextIndex : ++DEPRECATED_nextIndex)
, level(level)
{
}
Free::Free(Scope* scope)
: index(++nextIndex)
: index(FFlag::LuauTypeNormalization2 ? ++nextIndex : ++DEPRECATED_nextIndex)
, scope(scope)
{
}
Free::Free(Scope* scope, TypeLevel level)
: index(++nextIndex)
: index(FFlag::LuauTypeNormalization2 ? ++nextIndex : ++DEPRECATED_nextIndex)
, level(level)
, scope(scope)
{
}
int Free::nextIndex = 0;
int Free::DEPRECATED_nextIndex = 0;
Generic::Generic()
: index(++nextIndex)
: index(FFlag::LuauTypeNormalization2 ? ++nextIndex : ++DEPRECATED_nextIndex)
, name("g" + std::to_string(index))
{
}
Generic::Generic(TypeLevel level)
: index(++nextIndex)
: index(FFlag::LuauTypeNormalization2 ? ++nextIndex : ++DEPRECATED_nextIndex)
, level(level)
, name("g" + std::to_string(index))
{
}
Generic::Generic(const Name& name)
: index(++nextIndex)
: index(FFlag::LuauTypeNormalization2 ? ++nextIndex : ++DEPRECATED_nextIndex)
, name(name)
, explicitName(true)
{
}
Generic::Generic(Scope* scope)
: index(++nextIndex)
: index(FFlag::LuauTypeNormalization2 ? ++nextIndex : ++DEPRECATED_nextIndex)
, scope(scope)
{
}
Generic::Generic(TypeLevel level, const Name& name)
: index(++nextIndex)
: index(FFlag::LuauTypeNormalization2 ? ++nextIndex : ++DEPRECATED_nextIndex)
, level(level)
, name(name)
, explicitName(true)
@ -62,14 +66,14 @@ Generic::Generic(TypeLevel level, const Name& name)
}
Generic::Generic(Scope* scope, const Name& name)
: index(++nextIndex)
: index(FFlag::LuauTypeNormalization2 ? ++nextIndex : ++DEPRECATED_nextIndex)
, scope(scope)
, name(name)
, explicitName(true)
{
}
int Generic::nextIndex = 0;
int Generic::DEPRECATED_nextIndex = 0;
Error::Error()
: index(++nextIndex)

View file

@ -2,6 +2,7 @@
#include "Luau/Unifier.h"
#include "Luau/Common.h"
#include "Luau/Instantiation.h"
#include "Luau/RecursionCounter.h"
#include "Luau/Scope.h"
#include "Luau/TypePack.h"
@ -20,7 +21,9 @@ LUAU_FASTINTVARIABLE(LuauTypeInferLowerBoundsIterationLimit, 2000);
LUAU_FASTFLAG(LuauLowerBoundsCalculation);
LUAU_FASTFLAG(LuauErrorRecoveryType);
LUAU_FASTFLAG(LuauUnknownAndNeverType)
LUAU_FASTFLAGVARIABLE(LuauSubtypeNormalizer, false);
LUAU_FASTFLAGVARIABLE(LuauScalarShapeSubtyping, false)
LUAU_FASTFLAGVARIABLE(LuauInstantiateInSubtyping, false)
LUAU_FASTFLAG(LuauClassTypeVarsInSubstitution)
LUAU_FASTFLAG(LuauCallUnifyPackTails)
LUAU_FASTFLAG(DebugLuauDeferredConstraintResolution)
@ -343,17 +346,19 @@ static bool subsumes(bool useScopes, TY_A* left, TY_B* right)
return left->level.subsumes(right->level);
}
Unifier::Unifier(TypeArena* types, NotNull<SingletonTypes> singletonTypes, Mode mode, NotNull<Scope> scope, const Location& location,
Variance variance, UnifierSharedState& sharedState, TxnLog* parentLog)
: types(types)
, singletonTypes(singletonTypes)
Unifier::Unifier(NotNull<Normalizer> normalizer, Mode mode, NotNull<Scope> scope, const Location& location,
Variance variance, TxnLog* parentLog)
: types(normalizer->arena)
, singletonTypes(normalizer->singletonTypes)
, normalizer(normalizer)
, mode(mode)
, scope(scope)
, log(parentLog)
, location(location)
, variance(variance)
, sharedState(sharedState)
, sharedState(*normalizer->sharedState)
{
normalize = FFlag::LuauSubtypeNormalizer;
LUAU_ASSERT(sharedState.iceHandler);
}
@ -524,7 +529,7 @@ void Unifier::tryUnify_(TypeId subTy, TypeId superTy, bool isFunctionCall, bool
{
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);
}
@ -532,6 +537,10 @@ void Unifier::tryUnify_(TypeId subTy, TypeId superTy, bool isFunctionCall, bool
{
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))
{
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)
{
// A | B <: T if A <: T and B <: T
// A | B <: T if and only if A <: T and B <: T
bool failed = false;
std::optional<TypeError> unificationTooComplex;
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()];
Unifier innerState = makeChildUnifier();
innerState.normalize = false;
innerState.tryUnify_(subTy, type, isFunctionCall);
if (innerState.errors.empty())
@ -741,6 +751,20 @@ void Unifier::tryUnifyTypeWithUnion(TypeId subTy, TypeId superTy, const UnionTyp
{
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)
{
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> 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)
{
Unifier innerState = makeChildUnifier();
@ -806,6 +830,7 @@ void Unifier::tryUnifyIntersectionWithType(TypeId subTy, const IntersectionTypeV
{
TypeId type = uv->parts[(i + startIndex) % uv->parts.size()];
Unifier innerState = makeChildUnifier();
innerState.normalize = false;
innerState.tryUnify_(type, superTy, isFunctionCall);
if (innerState.errors.empty())
@ -822,12 +847,207 @@ void Unifier::tryUnifyIntersectionWithType(TypeId subTy, const IntersectionTypeV
if (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)
{
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* 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");
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());
reportError(TypeError{location, TypeMismatch{superTy, subTy, "different number of generic type parameters"}});
}
size_t numGenericPacks = superFunction->genericPacks.size();
if (numGenericPacks != 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> 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
if (!subTable->indexer && subTable->state != TableState::Free)
{
@ -2344,8 +2609,9 @@ bool Unifier::occursCheck(DenseHashSet<TypePackId>& seen, TypePackId needle, Typ
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.normalize = normalize;
return u;
}

View file

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

View file

@ -82,11 +82,13 @@ static void profilerLoop()
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.callbacks->interrupt = profilerTrigger;
last = now;
last += ticks * 1e-6;
}
else
{

View file

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

View file

@ -24,13 +24,15 @@ struct CodeAllocator
void* context = nullptr;
// 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'
void* (*createBlockUnwindInfo)(void* context, uint8_t* block, size_t blockSize, size_t& unwindDataSizeInBlock) = nullptr;
// 'startOffset' reserves space for data at the beginning of the page
void* (*createBlockUnwindInfo)(void* context, uint8_t* block, size_t blockSize, size_t& startOffset) = nullptr;
// Called to destroy unwinding information returned by 'createBlockUnwindInfo'
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);

View file

@ -10,7 +10,7 @@ namespace CodeGen
{
// 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);
} // namespace CodeGen

View file

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

View file

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

View file

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

View file

@ -354,10 +354,15 @@ void AssemblyBuilderX64::jmp(Label& label)
void AssemblyBuilderX64::jmp(OperandX64 op)
{
LUAU_ASSERT((op.cat == CategoryX64::reg ? op.base.size : op.memSize) == SizeX64::qword);
if (logText)
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);
placeModRegMem(op, 4);
commit();
@ -376,10 +381,14 @@ void AssemblyBuilderX64::call(Label& label)
void AssemblyBuilderX64::call(OperandX64 op)
{
LUAU_ASSERT((op.cat == CategoryX64::reg ? op.base.size : op.memSize) == SizeX64::qword);
if (logText)
log("call", op);
placeRex(op);
// Indirect absolute calls always work in 64 bit width mode, so REX.W is optional
placeRexNoW(op);
place(0xff);
placeModRegMem(op, 2);
commit();
@ -838,6 +847,21 @@ void AssemblyBuilderX64::placeRex(OperandX64 op)
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)
{
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)
, maxTotalSize(maxTotalSize)
{
LUAU_ASSERT(blockSize > kMaxUnwindDataSize);
LUAU_ASSERT(blockSize > kMaxReservedDataSize);
LUAU_ASSERT(maxTotalSize >= blockSize);
}
@ -116,15 +116,15 @@ bool CodeAllocator::allocate(
size_t totalSize = alignedDataSize + codeSize;
// Function has to fit into a single block with unwinding information
if (totalSize > blockSize - kMaxUnwindDataSize)
if (totalSize > blockSize - kMaxReservedDataSize)
return false;
size_t unwindInfoSize = 0;
size_t startOffset = 0;
// We might need a new block
if (totalSize > size_t(blockEnd - blockPos))
{
if (!allocateNewBlock(unwindInfoSize))
if (!allocateNewBlock(startOffset))
return false;
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
size_t dataOffset = unwindInfoSize + alignedDataSize - dataSize;
size_t codeOffset = unwindInfoSize + alignedDataSize;
size_t dataOffset = startOffset + alignedDataSize - dataSize;
size_t codeOffset = startOffset + alignedDataSize;
if (dataSize)
memcpy(blockPos + dataOffset, data, dataSize);
if (codeSize)
memcpy(blockPos + codeOffset, code, codeSize);
size_t pageAlignedSize = alignToPageSize(unwindInfoSize + totalSize);
size_t pageAlignedSize = alignToPageSize(startOffset + totalSize);
makePagesExecutable(blockPos, pageAlignedSize);
flushInstructionCache(blockPos + codeOffset, codeSize);
result = blockPos + unwindInfoSize;
result = blockPos + startOffset;
resultSize = totalSize;
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
unwindInfoSize = (unwindInfoSize + 15) & ~15;
LUAU_ASSERT(unwindInfoSize <= kMaxUnwindDataSize);
LUAU_ASSERT(unwindInfoSize <= kMaxReservedDataSize);
if (!unwindInfo)
return false;

View file

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

View file

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

View file

@ -32,6 +32,16 @@ struct UnwindInfoWin
uint8_t frameregoff : 4;
};
void UnwindBuilderWin::setBeginOffset(size_t beginOffset)
{
this->beginOffset = beginOffset;
}
size_t UnwindBuilderWin::getBeginOffset() const
{
return beginOffset;
}
void UnwindBuilderWin::start()
{
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[] = {
"LuauLowerBoundsCalculation",
"LuauInterpolatedStringBaseSupport",
"LuauInstantiateInSubtyping", // requires some fixes to lua-apps code
// makes sure we always have at least one entry
nullptr,
};

View file

@ -4,6 +4,7 @@ MAKEFLAGS+=-r -j8
COMMA=,
config=debug
protobuf=system
BUILD=build/$(config)
@ -95,12 +96,22 @@ ifeq ($(config),fuzz)
CXX=clang++ # our fuzzing infra relies on llvm fuzzer
CXXFLAGS+=-fsanitize=address,fuzzer -Ibuild/libprotobuf-mutator -O2
LDFLAGS+=-fsanitize=address,fuzzer
LPROTOBUF=-lprotobuf
DPROTOBUF=-D CMAKE_BUILD_TYPE=Release -D LIB_PROTO_MUTATOR_TESTING=OFF
EPROTOC=protoc
endif
ifeq ($(config),profile)
CXXFLAGS+=-O2 -DNDEBUG -gdwarf-4 -DCALLGRIND=1
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
$(AST_OBJECTS): CXXFLAGS+=-std=c++17 -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
$(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
.PHONY: all test clean coverage format luau-size aliases
@ -199,7 +210,7 @@ $(BUILD)/%.c.o: %.c
# protobuf fuzzer setup
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
$(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:
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
# picks up include dependencies for all object files

View file

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

View file

@ -12,6 +12,8 @@
#include <string.h>
#include <stdio.h>
LUAU_FASTFLAGVARIABLE(LuauFasterGetInfo, false)
static const char* getfuncname(Closure* f);
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;
}
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++)
{
switch (*what)
@ -103,14 +105,23 @@ static int auxgetinfo(lua_State* L, const char* what, lua_Debug* ar, Closure* f,
ar->source = "=[C]";
ar->what = "C";
ar->linedefined = -1;
if (FFlag::LuauFasterGetInfo)
ar->short_src = "[C]";
}
else
{
ar->source = getstr(f->l.p->source);
TString* source = f->l.p->source;
ar->source = getstr(source);
ar->what = "Lua";
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;
}
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);
break;
}
case 'f':
{
cl = f;
break;
}
default:;
}
}
return status;
return cl;
}
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 status = 0;
Closure* f = NULL;
CallInfo* ci = NULL;
if (level < 0)
@ -180,7 +195,19 @@ int lua_getinfo(lua_State* L, int level, const char* what, lua_Debug* ar)
}
if (f)
{
status = auxgetinfo(L, what, ar, f, ci);
if (FFlag::LuauFasterGetInfo)
{
// auxgetinfo fills ar and optionally requests to put closure on stack
if (Closure* fcl = auxgetinfo(L, what, ar, f, ci))
{
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);
@ -188,7 +215,8 @@ int lua_getinfo(lua_State* L, int level, const char* what, lua_Debug* ar)
incr_top(L);
}
}
return status;
}
return f ? 1 : 0;
}
static const char* getfuncname(Closure* cl)
@ -284,10 +312,11 @@ static void pusherror(lua_State* L, const char* msg)
CallInfo* ci = L->ci;
if (isLua(ci))
{
char buff[LUA_IDSIZE]; // add file:line information
luaO_chunkid(buff, getstr(getluaproto(ci)->source), LUA_IDSIZE);
TString* source = getluaproto(ci)->source;
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);
luaO_pushfstring(L, "%s:%d: %s", buff, line, msg);
luaO_pushfstring(L, "%s:%d: %s", chunkid, line, msg);
}
else
{

View file

@ -13,8 +13,6 @@
#include <string.h>
LUAU_FASTFLAGVARIABLE(LuauBetterThreadMark, false)
/*
* Luau uses an incremental non-generational non-moving mark&sweep garbage collector.
*
@ -473,8 +471,6 @@ static size_t propagatemark(global_State* g)
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
@ -494,33 +490,6 @@ static size_t propagatemark(global_State* g)
// 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);
traversestack(g, th);
// final traversal?
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;
}

View file

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

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
if (version == 0)
{
char chunkid[LUA_IDSIZE];
luaO_chunkid(chunkid, chunkname, LUA_IDSIZE);
char chunkbuf[LUA_IDSIZE];
const char* chunkid = luaO_chunkid(chunkbuf, sizeof(chunkbuf), chunkname, strlen(chunkname));
lua_pushfstring(L, "%s%.*s", chunkid, int(size - offset), data + offset);
return 1;
}
if (version < LBC_VERSION_MIN || version > LBC_VERSION_MAX)
{
char chunkid[LUA_IDSIZE];
luaO_chunkid(chunkid, chunkname, LUA_IDSIZE);
char chunkbuf[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);
return 1;
}

View file

@ -220,8 +220,13 @@ int luaV_strcmp(const TString* ls, const TString* rs)
return 0;
const char* l = getstr(ls);
size_t ll = ls->len;
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 lmin = ll < lr ? ll : lr;

View file

@ -240,12 +240,12 @@ TEST_CASE_FIXTURE(AssemblyBuilderX64Fixture, "FormsOfLea")
TEST_CASE_FIXTURE(AssemblyBuilderX64Fixture, "FormsOfAbsoluteJumps")
{
SINGLE_COMPARE(jmp(rax), 0x48, 0xff, 0xe0);
SINGLE_COMPARE(jmp(r14), 0x49, 0xff, 0xe6);
SINGLE_COMPARE(jmp(qword[r14 + rdx * 4]), 0x49, 0xff, 0x24, 0x96);
SINGLE_COMPARE(call(rax), 0x48, 0xff, 0xd0);
SINGLE_COMPARE(call(r14), 0x49, 0xff, 0xd6);
SINGLE_COMPARE(call(qword[r14 + rdx * 4]), 0x49, 0xff, 0x14, 0x96);
SINGLE_COMPARE(jmp(rax), 0xff, 0xe0);
SINGLE_COMPARE(jmp(r14), 0x41, 0xff, 0xe6);
SINGLE_COMPARE(jmp(qword[r14 + rdx * 4]), 0x41, 0xff, 0x24, 0x96);
SINGLE_COMPARE(call(rax), 0xff, 0xd0);
SINGLE_COMPARE(call(r14), 0x41, 0xff, 0xd6);
SINGLE_COMPARE(call(qword[r14 + rdx * 4]), 0x41, 0xff, 0x14, 0x96);
}
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")
{
ScopedFastFlag selfCallAutocompleteFix3{"LuauSelfCallAutocompleteFix3", true};
loadDefinition(R"(
declare class Foo
function one(self): number
@ -2995,8 +2993,6 @@ t.@1
TEST_CASE_FIXTURE(ACFixture, "do_compatible_self_calls")
{
ScopedFastFlag selfCallAutocompleteFix3{"LuauSelfCallAutocompleteFix3", true};
check(R"(
local t = {}
function t:m() end
@ -3011,8 +3007,6 @@ t:@1
TEST_CASE_FIXTURE(ACFixture, "no_incompatible_self_calls")
{
ScopedFastFlag selfCallAutocompleteFix3{"LuauSelfCallAutocompleteFix3", true};
check(R"(
local t = {}
function t.m() end
@ -3027,8 +3021,6 @@ t:@1
TEST_CASE_FIXTURE(ACFixture, "no_incompatible_self_calls_2")
{
ScopedFastFlag selfCallAutocompleteFix3{"LuauSelfCallAutocompleteFix3", true};
check(R"(
local f: (() -> number) & ((number) -> number) = function(x: number?) return 2 end
local t = {}
@ -3059,8 +3051,6 @@ t:@1
TEST_CASE_FIXTURE(ACFixture, "no_wrong_compatible_self_calls_with_generics")
{
ScopedFastFlag selfCallAutocompleteFix3{"LuauSelfCallAutocompleteFix3", true};
check(R"(
local t = {}
function t.m<T>(a: T) end
@ -3076,8 +3066,6 @@ t:@1
TEST_CASE_FIXTURE(ACFixture, "string_prim_self_calls_are_fine")
{
ScopedFastFlag selfCallAutocompleteFix3{"LuauSelfCallAutocompleteFix3", true};
check(R"(
local s = "hello"
s:@1
@ -3095,8 +3083,6 @@ s:@1
TEST_CASE_FIXTURE(ACFixture, "string_prim_non_self_calls_are_avoided")
{
ScopedFastFlag selfCallAutocompleteFix3{"LuauSelfCallAutocompleteFix3", true};
check(R"(
local s = "hello"
s.@1
@ -3112,8 +3098,6 @@ s.@1
TEST_CASE_FIXTURE(ACBuiltinsFixture, "library_non_self_calls_are_fine")
{
ScopedFastFlag selfCallAutocompleteFix3{"LuauSelfCallAutocompleteFix3", true};
check(R"(
string.@1
)");
@ -3143,8 +3127,6 @@ table.@1
TEST_CASE_FIXTURE(ACBuiltinsFixture, "library_self_calls_are_invalid")
{
ScopedFastFlag selfCallAutocompleteFix3{"LuauSelfCallAutocompleteFix3", true};
check(R"(
string:@1
)");

View file

@ -96,12 +96,12 @@ TEST_CASE("CodeAllocationWithUnwindCallbacks")
data.resize(8);
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;
CHECK(info.unwind.size() == 8);
memcpy(block, info.unwind.data(), info.unwind.size());
unwindDataSizeInBlock = 8;
beginOffset = 8;
info.block = block;
@ -194,10 +194,12 @@ TEST_CASE("Dwarf2UnwindCodesX64")
// Windows x64 ABI
constexpr RegisterX64 rArg1 = rcx;
constexpr RegisterX64 rArg2 = rdx;
constexpr RegisterX64 rArg3 = r8;
#else
// System V AMD64 ABI
constexpr RegisterX64 rArg1 = rdi;
constexpr RegisterX64 rArg2 = rsi;
constexpr RegisterX64 rArg3 = rdx;
#endif
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
TEST_SUITE_END();

View file

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

View file

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

View file

@ -21,14 +21,14 @@ end
return math.max(fib(5), 1)
)");
CHECK_EQ(result.warnings.size(), 0);
REQUIRE(0 == result.warnings.size());
}
TEST_CASE_FIXTURE(Fixture, "UnknownGlobal")
{
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'");
}
@ -39,7 +39,7 @@ TEST_CASE_FIXTURE(Fixture, "DeprecatedGlobal")
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");
}
@ -53,7 +53,7 @@ TEST_CASE_FIXTURE(Fixture, "DeprecatedGlobalNoReplacement")
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");
}
@ -64,7 +64,7 @@ local _ = 5
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");
}
@ -75,7 +75,7 @@ _ = 5
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");
}
@ -86,7 +86,7 @@ local _ = 5
_ = 6
)");
CHECK_EQ(result.warnings.size(), 0);
REQUIRE(0 == result.warnings.size());
}
TEST_CASE_FIXTURE(BuiltinsFixture, "BuiltinGlobalWrite")
@ -100,7 +100,7 @@ end
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[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
)");
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");
}
@ -121,7 +121,7 @@ TEST_CASE_FIXTURE(Fixture, "MultilineBlockSemicolonsWhitelisted")
print(1); print(2); print(3)
)");
CHECK(result.warnings.empty());
REQUIRE(0 == result.warnings.size());
}
TEST_CASE_FIXTURE(Fixture, "MultilineBlockMissedSemicolon")
@ -130,7 +130,7 @@ TEST_CASE_FIXTURE(Fixture, "MultilineBlockMissedSemicolon")
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");
}
@ -142,7 +142,7 @@ local _x do
end
)");
CHECK_EQ(result.warnings.size(), 0);
REQUIRE(0 == result.warnings.size());
}
TEST_CASE_FIXTURE(Fixture, "ConfusingIndentation")
@ -152,7 +152,7 @@ print(math.max(1,
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");
}
@ -167,7 +167,7 @@ end
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");
}
@ -188,7 +188,7 @@ end
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");
}
@ -213,7 +213,7 @@ end
return bar() + baz() + read()
)");
CHECK_EQ(result.warnings.size(), 0);
REQUIRE(0 == result.warnings.size());
}
TEST_CASE_FIXTURE(Fixture, "GlobalAsLocalWithConditional")
@ -233,7 +233,7 @@ end
return bar() + baz()
)");
CHECK_EQ(result.warnings.size(), 0);
REQUIRE(0 == result.warnings.size());
}
TEST_CASE_FIXTURE(Fixture, "GlobalAsLocal3WithConditionalRead")
@ -257,7 +257,7 @@ end
return bar() + baz() + read()
)");
CHECK_EQ(result.warnings.size(), 0);
REQUIRE(0 == result.warnings.size());
}
TEST_CASE_FIXTURE(Fixture, "GlobalAsLocalInnerRead")
@ -275,7 +275,7 @@ function baz() bar = 0 end
return foo() + baz()
)");
CHECK_EQ(result.warnings.size(), 0);
REQUIRE(0 == result.warnings.size());
}
TEST_CASE_FIXTURE(Fixture, "GlobalAsLocalMulti")
@ -304,7 +304,7 @@ fnA() -- prints "true", "nil"
fnB() -- prints "false", "nil"
)");
CHECK_EQ(result.warnings.size(), 1);
REQUIRE(1 == result.warnings.size());
CHECK_EQ(result.warnings[0].text,
"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)
)");
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");
}
@ -337,7 +337,7 @@ end
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");
}
@ -352,7 +352,7 @@ end
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");
}
@ -372,7 +372,7 @@ end
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[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)
)");
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");
}
@ -412,7 +412,7 @@ end
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[1].text, "Function 'qux' is never used; prefix with '_' to silence");
}
@ -427,7 +427,7 @@ end
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].text, "Unreachable code (previous statement always returns)");
}
@ -443,7 +443,7 @@ end
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].text, "Unreachable code (previous statement always breaks)");
}
@ -459,7 +459,7 @@ end
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].text, "Unreachable code (previous statement always continues)");
}
@ -495,7 +495,7 @@ end
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].text, "Unreachable code (previous statement always returns)");
}
@ -515,7 +515,7 @@ end
return foo1
)");
CHECK_EQ(result.warnings.size(), 0);
REQUIRE(0 == result.warnings.size());
}
TEST_CASE_FIXTURE(Fixture, "UnreachableCodeAssertFalseReturnSilent")
@ -532,7 +532,7 @@ end
return foo1
)");
CHECK_EQ(result.warnings.size(), 0);
REQUIRE(0 == result.warnings.size());
}
TEST_CASE_FIXTURE(Fixture, "UnreachableCodeErrorReturnNonSilentBranchy")
@ -550,7 +550,7 @@ end
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].text, "Unreachable code (previous statement always errors)");
}
@ -571,7 +571,7 @@ end
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].text, "Unreachable code (previous statement always errors)");
}
@ -589,7 +589,7 @@ end
return foo1
)");
CHECK_EQ(result.warnings.size(), 0);
REQUIRE(0 == result.warnings.size());
}
TEST_CASE_FIXTURE(Fixture, "UnreachableCodeLoopRepeat")
@ -605,8 +605,8 @@ end
return foo1
)");
CHECK_EQ(result.warnings.size(),
0); // this is technically a bug, since the repeat body always returns; fixing this bug is a bit more involved than I'd like
// 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")
@ -633,7 +633,7 @@ local _o02 = type(game) == "vector"
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].text, "Unknown type 'Part' (expected primitive type)");
CHECK_EQ(result.warnings[1].location.begin.line, 3);
@ -654,7 +654,7 @@ for i=#t,1,-1 do
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].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
)");
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].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
)");
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].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
)");
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].text, "For loop starts at 0, but arrays start at 1");
CHECK_EQ(result.warnings[1].location.begin.line, 7);
@ -730,7 +730,7 @@ local _a,_b,_c = pcall(), nil
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].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);
@ -795,7 +795,7 @@ end
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].text,
"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
)");
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].text,
"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")
@ -893,7 +893,7 @@ until true
return 1
)");
CHECK_EQ(result.warnings.size(), 0);
REQUIRE(0 == result.warnings.size());
}
TEST_CASE_FIXTURE(Fixture, "IgnoreLintAll")
@ -903,7 +903,7 @@ TEST_CASE_FIXTURE(Fixture, "IgnoreLintAll")
return foo
)");
CHECK_EQ(result.warnings.size(), 0);
REQUIRE(0 == result.warnings.size());
}
TEST_CASE_FIXTURE(Fixture, "IgnoreLintSpecific")
@ -914,7 +914,7 @@ local x = 1
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");
}
@ -933,7 +933,7 @@ local _ = ("%"):format()
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[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 %");
@ -973,7 +973,7 @@ string.packsize("c99999999999999999999")
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[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");
@ -1017,7 +1017,7 @@ local _ = s:match("%q")
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[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");
@ -1049,7 +1049,7 @@ string.match(s, "((a)%1)")
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].location.begin.line, 7);
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, "[^]|'[]")
)~");
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[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");
@ -1118,7 +1118,7 @@ string.find("foo");
("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].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");
@ -1141,7 +1141,7 @@ string.gsub(s, '[A-Z]+(%d)', "%0%1")
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[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");
@ -1162,7 +1162,7 @@ os.date("it's %c now")
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[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 %");
@ -1181,7 +1181,7 @@ s: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].location.begin.line, 3);
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[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");
@ -1248,7 +1248,7 @@ TEST_CASE_FIXTURE(Fixture, "ImportOnlyUsedInTypeAnnotation")
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");
}
@ -1259,7 +1259,7 @@ TEST_CASE_FIXTURE(Fixture, "DisableUnknownGlobalWithTypeChecking")
unknownGlobal()
)");
REQUIRE_EQ(result.warnings.size(), 0);
REQUIRE(0 == result.warnings.size());
}
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
)");
CHECK_EQ(result.warnings.size(), 0);
REQUIRE(0 == result.warnings.size());
}
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");
CHECK_EQ(result.warnings.size(), 0);
REQUIRE(0 == result.warnings.size());
}
TEST_CASE_FIXTURE(Fixture, "DeadLocalsUsed")
@ -1320,7 +1320,7 @@ do
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[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");
@ -1333,7 +1333,7 @@ local foo
function foo() end
)");
CHECK_EQ(result.warnings.size(), 0);
REQUIRE(0 == result.warnings.size());
}
TEST_CASE_FIXTURE(Fixture, "DuplicateGlobalFunction")
@ -1408,7 +1408,7 @@ TEST_CASE_FIXTURE(Fixture, "DontTriggerTheWarningIfTheFunctionsAreInDifferentSco
return c
)");
CHECK(result.warnings.empty());
REQUIRE(0 == result.warnings.size());
}
TEST_CASE_FIXTURE(Fixture, "LintHygieneUAF")
@ -1444,7 +1444,7 @@ TEST_CASE_FIXTURE(Fixture, "LintHygieneUAF")
local h: Hooty.Pt
)");
CHECK_EQ(result.warnings.size(), 12);
REQUIRE(12 == result.warnings.size());
}
TEST_CASE_FIXTURE(Fixture, "DeprecatedApi")
@ -1478,7 +1478,7 @@ return function (i: Instance)
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[1].text, "Member 'toHSV' is deprecated, use 'Color3:ToHSV' instead");
CHECK_EQ(result.warnings[2].text, "Member 'Instance.DataCost' is deprecated");
@ -1511,7 +1511,7 @@ 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 "
"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");
@ -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
)");
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].location.begin.line + 1, 4);
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
)");
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].location.begin.line + 1, 5);
}
@ -1601,7 +1601,7 @@ end
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[1].text, "Variable 'a1' is never used; prefix with '_' to silence");
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
)");
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; "
"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; "
@ -1640,7 +1640,7 @@ do end
--!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[1].text, "Unknown comment directive 'nolintGlobal'");
CHECK_EQ(result.warnings[2].text, "nolint directive refers to unknown lint rule 'Global'");
@ -1656,7 +1656,7 @@ TEST_CASE_FIXTURE(Fixture, "WrongCommentMuteSelf")
--!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")
@ -1668,7 +1668,7 @@ elseif if 0 then 5 else 4 then
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");
}
@ -1681,13 +1681,13 @@ TEST_CASE_FIXTURE(Fixture, "WrongCommentOptimize")
--!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[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");
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");
}
@ -1700,7 +1700,7 @@ TEST_CASE_FIXTURE(Fixture, "TestStringInterpolation")
local _ = `unknown {foo}`
)");
REQUIRE_EQ(result.warnings.size(), 1);
REQUIRE(1 == result.warnings.size());
}
TEST_CASE_FIXTURE(Fixture, "IntegerParsing")
@ -1710,7 +1710,7 @@ local _ = 0b10000000000000000000000000000000000000000000000000000000000000000
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[1].text, "Hexadecimal number literal exceeded available precision and has been truncated to 2^64");
}
@ -1725,7 +1725,7 @@ local _ = 0x0x123
local _ = 0x0xffffffffffffffffffffffffffffffffff
)");
REQUIRE_EQ(result.warnings.size(), 2);
REQUIRE(2 == result.warnings.size());
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");
CHECK_EQ(result.warnings[1].text,
@ -1756,7 +1756,7 @@ 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[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");

View file

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

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]));
}
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();

View file

@ -2,6 +2,7 @@
#include "Luau/AstQuery.h"
#include "Luau/BuiltinDefinitions.h"
#include "Luau/Error.h"
#include "Luau/Scope.h"
#include "Luau/TypeInfer.h"
#include "Luau/TypeVar.h"
@ -14,6 +15,7 @@
using namespace Luau;
LUAU_FASTFLAG(LuauLowerBoundsCalculation);
LUAU_FASTFLAG(LuauInstantiateInSubtyping);
LUAU_FASTFLAG(LuauSpecialTypesAsterisked);
TEST_SUITE_BEGIN("TypeInferFunctions");
@ -1087,10 +1089,20 @@ f(function(a, b, c, ...) return a + b end)
LUAU_REQUIRE_ERRORS(result);
if (FFlag::LuauInstantiateInSubtyping)
{
CHECK_EQ(R"(Type '<a>(number, number, a) -> number' could not be converted into '(number, number) -> number'
caused by:
Argument count mismatch. Function expects 3 arguments, but only 2 are specified)",
toString(result.errors[0]));
}
else
{
CHECK_EQ(R"(Type '(number, number, a) -> number' could not be converted into '(number, number) -> number'
caused by:
Argument count mismatch. Function expects 3 arguments, but only 2 are specified)",
toString(result.errors[0]));
}
// Infer from variadic packs into elements
result = check(R"(
@ -1189,10 +1201,20 @@ f(function(a, b, c, ...) return a + b end)
LUAU_REQUIRE_ERRORS(result);
if (FFlag::LuauInstantiateInSubtyping)
{
CHECK_EQ(R"(Type '<a>(number, number, a) -> number' could not be converted into '(number, number) -> number'
caused by:
Argument count mismatch. Function expects 3 arguments, but only 2 are specified)",
toString(result.errors[0]));
}
else
{
CHECK_EQ(R"(Type '(number, number, a) -> number' could not be converted into '(number, number) -> number'
caused by:
Argument count mismatch. Function expects 3 arguments, but only 2 are specified)",
toString(result.errors[0]));
}
// Infer from variadic packs into elements
result = check(R"(

View file

@ -10,6 +10,7 @@
#include "doctest.h"
LUAU_FASTFLAG(LuauCheckGenericHOFTypes)
LUAU_FASTFLAG(LuauInstantiateInSubtyping)
LUAU_FASTFLAG(LuauSpecialTypesAsterisked)
using namespace Luau;
@ -960,7 +961,11 @@ TEST_CASE_FIXTURE(Fixture, "instantiate_generic_function_in_assignments")
TypeMismatch* tm = get<TypeMismatch>(result.errors[0]);
REQUIRE(tm);
CHECK_EQ("((number) -> number, string) -> number", toString(tm->wantedType));
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")
@ -980,6 +985,9 @@ TEST_CASE_FIXTURE(Fixture, "instantiate_generic_function_in_assignments2")
TypeMismatch* tm = get<TypeMismatch>(result.errors[0]);
REQUIRE(tm);
CHECK_EQ("(string, string) -> number", toString(tm->wantedType));
if (FFlag::LuauInstantiateInSubtyping)
CHECK_EQ("<a, b...>((a) -> (b...), a) -> (b...)", toString(tm->givenType));
else
CHECK_EQ("((string) -> number, string) -> number", toString(*tm->givenType));
}
@ -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);
}
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
{
LUAU_REQUIRE_ERRORS(result);
@ -1219,4 +1236,48 @@ TEST_CASE_FIXTURE(Fixture, "do_not_always_instantiate_generic_intersection_types
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();

View file

@ -446,4 +446,459 @@ TEST_CASE_FIXTURE(Fixture, "no_stack_overflow_from_flattenintersection")
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();

View file

@ -10,6 +10,8 @@
#include "doctest.h"
LUAU_FASTFLAG(LuauInstantiateInSubtyping)
using namespace Luau;
LUAU_FASTFLAG(LuauSpecialTypesAsterisked)
@ -248,9 +250,26 @@ end
return m
)");
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")
{
CheckResult result = check(R"(
@ -367,8 +386,6 @@ type Table = typeof(tbl)
TEST_CASE_FIXTURE(BuiltinsFixture, "do_not_modify_imported_types_5")
{
ScopedFastFlag luauInplaceDemoteSkipAllBound{"LuauInplaceDemoteSkipAllBound", true};
fileResolver.source["game/A"] = R"(
export type Type = {x: number, y: number}
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
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;
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);
@ -635,4 +612,87 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "for_in_loop_with_zero_iterators")
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();

View file

@ -11,7 +11,8 @@
using namespace Luau;
LUAU_FASTFLAG(LuauLowerBoundsCalculation);
LUAU_FASTFLAG(LuauLowerBoundsCalculation)
LUAU_FASTFLAG(LuauInstantiateInSubtyping)
TEST_SUITE_BEGIN("TableTests");
@ -2038,12 +2039,23 @@ caused by:
caused by:
Property 'y' is not compatible. Type 'string' could not be converted into 'number')");
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:
Type '{ __call: (a, b) -> () }' could not be converted into '{ __call: <a>(a) -> () }'
caused by:
Property '__call' is not compatible. Type '(a, b) -> ()' could not be converted into '<a>(a) -> ()'; different number of generic type parameters)");
}
}
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")));
}
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();

View file

@ -17,7 +17,9 @@
LUAU_FASTFLAG(LuauLowerBoundsCalculation);
LUAU_FASTFLAG(LuauFixLocationSpanTableIndexExpr);
LUAU_FASTFLAG(DebugLuauDeferredConstraintResolution);
LUAU_FASTFLAG(LuauInstantiateInSubtyping);
LUAU_FASTFLAG(LuauSpecialTypesAsterisked);
LUAU_FASTFLAG(LuauCheckGenericHOFTypes);
using namespace Luau;
@ -999,8 +1001,27 @@ TEST_CASE_FIXTURE(Fixture, "cli_50041_committing_txnlog_in_apollo_client_error")
end
)");
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")
{
@ -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]));
}
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")
{
CheckResult result = check(R"(

View file

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

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)");
}
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();

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('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('\\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
assert((function() local a = {4,5,6} return a[3] end)() == 6)

View file

@ -21,6 +21,7 @@
#include <sys/sysctl.h>
#endif
#include <random>
#include <optional>
// 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
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)
{
if (strncmp(flagName, "Test", 4) == 0)
@ -261,6 +266,16 @@ int main(int argc, char** argv)
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))
setFastFlags(flags);
@ -295,6 +310,8 @@ int main(int argc, char** argv)
printf(" --verbose Enables verbose output (e.g. lua 'print' statements)\n");
printf(" --fflags= Sets specified 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;
}

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 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>
</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>

View file

@ -22,6 +22,10 @@ def safeParseInt(i, default=0):
return default
def makeDottedName(path):
return ".".join(path)
class Handler(x.ContentHandler):
def __init__(self, failList):
self.currentTest = []
@ -41,7 +45,7 @@ class Handler(x.ContentHandler):
if self.currentTest:
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
# them must report a pass in order for us to consider the test
@ -60,6 +64,10 @@ class Handler(x.ContentHandler):
self.currentTest.pop()
def print_stderr(*args, **kw):
print(*args, **kw, file=sys.stderr)
def main():
parser = argparse.ArgumentParser(
description="Run Luau.UnitTest with deferred constraint resolution enabled"
@ -80,6 +88,16 @@ def main():
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()
failList = loadFailList()
@ -90,7 +108,12 @@ def main():
"--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(
commandLine,
@ -104,15 +127,21 @@ def main():
sys.stdout.buffer.write(line)
return
else:
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()
for testName, passed in handler.results.items():
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:
print("UNEXPECTED: {} should have passed".format(testName))
print_stderr(f"UNEXPECTED: {testName} should have passed")
if args.write:
newFailList = sorted(
@ -126,14 +155,11 @@ def main():
with open(FAIL_LIST_PATH, "w", newline="\n") as f:
for name in newFailList:
print(name, file=f)
print("Updated faillist.txt", file=sys.stderr)
print_stderr("Updated faillist.txt")
if handler.numSkippedTests > 0:
print(
"{} test(s) were skipped! That probably means that a test segfaulted!".format(
handler.numSkippedTests
),
file=sys.stderr,
print_stderr(
f"{handler.numSkippedTests} test(s) were skipped! That probably means that a test segfaulted!"
)
sys.exit(1)
@ -143,7 +169,7 @@ def main():
)
if ok:
print("Everything in order!", file=sys.stderr)
print_stderr("Everything in order!")
sys.exit(0 if ok else 1)