Merge branch 'upstream' into merge

This commit is contained in:
Vyacheslav Egorov 2023-05-12 15:30:03 +03:00
commit 59c2698970
62 changed files with 2869 additions and 276 deletions

View file

@ -198,9 +198,26 @@ struct UnpackConstraint
TypePackId sourcePack;
};
using ConstraintV = Variant<SubtypeConstraint, PackSubtypeConstraint, GeneralizationConstraint, InstantiationConstraint, UnaryConstraint,
BinaryConstraint, IterableConstraint, NameConstraint, TypeAliasExpansionConstraint, FunctionCallConstraint, PrimitiveTypeConstraint,
HasPropConstraint, SetPropConstraint, SetIndexerConstraint, SingletonOrTopTypeConstraint, UnpackConstraint>;
// ty ~ reduce ty
//
// Try to reduce ty, if it is a TypeFamilyInstanceType. Otherwise, do nothing.
struct ReduceConstraint
{
TypeId ty;
};
// tp ~ reduce tp
//
// Analogous to ReduceConstraint, but for type packs.
struct ReducePackConstraint
{
TypePackId tp;
};
using ConstraintV =
Variant<SubtypeConstraint, PackSubtypeConstraint, GeneralizationConstraint, InstantiationConstraint, UnaryConstraint, BinaryConstraint,
IterableConstraint, NameConstraint, TypeAliasExpansionConstraint, FunctionCallConstraint, PrimitiveTypeConstraint, HasPropConstraint,
SetPropConstraint, SetIndexerConstraint, SingletonOrTopTypeConstraint, UnpackConstraint, ReduceConstraint, ReducePackConstraint>;
struct Constraint
{

View file

@ -121,6 +121,8 @@ struct ConstraintSolver
bool tryDispatch(const SetIndexerConstraint& c, NotNull<const Constraint> constraint, bool force);
bool tryDispatch(const SingletonOrTopTypeConstraint& c, NotNull<const Constraint> constraint);
bool tryDispatch(const UnpackConstraint& c, NotNull<const Constraint> constraint);
bool tryDispatch(const ReduceConstraint& c, NotNull<const Constraint> constraint, bool force);
bool tryDispatch(const ReducePackConstraint& c, NotNull<const Constraint> constraint, bool force);
// for a, ... in some_table do
// also handles __iter metamethod

View file

@ -329,12 +329,27 @@ struct DynamicPropertyLookupOnClassesUnsafe
bool operator==(const DynamicPropertyLookupOnClassesUnsafe& rhs) const;
};
using TypeErrorData = Variant<TypeMismatch, UnknownSymbol, UnknownProperty, NotATable, CannotExtendTable, OnlyTablesCanHaveMethods,
DuplicateTypeDefinition, CountMismatch, FunctionDoesNotTakeSelf, FunctionRequiresSelf, OccursCheckFailed, UnknownRequire,
IncorrectGenericParameterCount, SyntaxError, CodeTooComplex, UnificationTooComplex, UnknownPropButFoundLikeProp, GenericError, InternalError,
CannotCallNonFunction, ExtraInformation, DeprecatedApiUsed, ModuleHasCyclicDependency, IllegalRequire, FunctionExitsWithoutReturning,
DuplicateGenericParameter, CannotInferBinaryOperation, MissingProperties, SwappedGenericTypeParameter, OptionalValueAccess, MissingUnionProperty,
TypesAreUnrelated, NormalizationTooComplex, TypePackMismatch, DynamicPropertyLookupOnClassesUnsafe>;
struct UninhabitedTypeFamily
{
TypeId ty;
bool operator==(const UninhabitedTypeFamily& rhs) const;
};
struct UninhabitedTypePackFamily
{
TypePackId tp;
bool operator==(const UninhabitedTypePackFamily& rhs) const;
};
using TypeErrorData =
Variant<TypeMismatch, UnknownSymbol, UnknownProperty, NotATable, CannotExtendTable, OnlyTablesCanHaveMethods, DuplicateTypeDefinition,
CountMismatch, FunctionDoesNotTakeSelf, FunctionRequiresSelf, OccursCheckFailed, UnknownRequire, IncorrectGenericParameterCount, SyntaxError,
CodeTooComplex, UnificationTooComplex, UnknownPropButFoundLikeProp, GenericError, InternalError, CannotCallNonFunction, ExtraInformation,
DeprecatedApiUsed, ModuleHasCyclicDependency, IllegalRequire, FunctionExitsWithoutReturning, DuplicateGenericParameter,
CannotInferBinaryOperation, MissingProperties, SwappedGenericTypeParameter, OptionalValueAccess, MissingUnionProperty, TypesAreUnrelated,
NormalizationTooComplex, TypePackMismatch, DynamicPropertyLookupOnClassesUnsafe, UninhabitedTypeFamily, UninhabitedTypePackFamily>;
struct TypeErrorSummary
{

View file

@ -2,6 +2,9 @@
#pragma once
#include "Luau/Type.h"
#include "Luau/DenseHash.h"
#include <vector>
namespace Luau
{
@ -10,6 +13,29 @@ struct TypeArena;
struct Scope;
void quantify(TypeId ty, TypeLevel level);
std::optional<TypeId> quantify(TypeArena* arena, TypeId ty, Scope* scope);
// TODO: This is eerily similar to the pattern that NormalizedClassType
// implements. We could, and perhaps should, merge them together.
template<typename K, typename V>
struct OrderedMap
{
std::vector<K> keys;
DenseHashMap<K, V> pairings{nullptr};
void push(K k, V v)
{
keys.push_back(k);
pairings[k] = v;
}
};
struct QuantifierResult
{
TypeId result;
OrderedMap<TypeId, TypeId> insertedGenerics;
OrderedMap<TypePackId, TypePackId> insertedGenericPacks;
};
std::optional<QuantifierResult> quantify(TypeArena* arena, TypeId ty, Scope* scope);
} // namespace Luau

View file

@ -19,6 +19,10 @@ struct PendingType
// The pending Type state.
Type pending;
// On very rare occasions, we need to delete an entry from the TxnLog.
// DenseHashMap does not afford that so we note its deadness here.
bool dead = false;
explicit PendingType(Type state)
: pending(std::move(state))
{
@ -61,10 +65,11 @@ T* getMutable(PendingTypePack* pending)
// Log of what TypeIds we are rebinding, to be committed later.
struct TxnLog
{
TxnLog()
explicit TxnLog(bool useScopes = false)
: typeVarChanges(nullptr)
, typePackChanges(nullptr)
, ownedSeen()
, useScopes(useScopes)
, sharedSeen(&ownedSeen)
{
}
@ -297,6 +302,12 @@ private:
void popSeen(TypeOrPackId lhs, TypeOrPackId rhs);
public:
// There is one spot in the code where TxnLog has to reconcile collisions
// between parallel logs. In that codepath, we have to work out which of two
// FreeTypes subsumes the other. If useScopes is false, the TypeLevel is
// used. Else we use the embedded Scope*.
bool useScopes = false;
// Used to avoid infinite recursion when types are cyclic.
// Shared with all the descendent TxnLogs.
std::vector<std::pair<TypeOrPackId, TypeOrPackId>>* sharedSeen;

View file

@ -23,6 +23,7 @@
LUAU_FASTINT(LuauTableTypeMaximumStringifierLength)
LUAU_FASTINT(LuauTypeMaximumStringifierLength)
LUAU_FASTFLAG(LuauTypecheckClassTypeIndexers)
namespace Luau
{
@ -31,6 +32,8 @@ struct TypeArena;
struct Scope;
using ScopePtr = std::shared_ptr<Scope>;
struct TypeFamily;
/**
* There are three kinds of type variables:
* - `Free` variables are metavariables, which stand for unconstrained types.
@ -489,6 +492,7 @@ struct ClassType
Tags tags;
std::shared_ptr<ClassUserData> userData;
ModuleName definitionModuleName;
std::optional<TableIndexer> indexer;
ClassType(Name name, Props props, std::optional<TypeId> parent, std::optional<TypeId> metatable, Tags tags,
std::shared_ptr<ClassUserData> userData, ModuleName definitionModuleName)
@ -501,6 +505,35 @@ struct ClassType
, definitionModuleName(definitionModuleName)
{
}
ClassType(Name name, Props props, std::optional<TypeId> parent, std::optional<TypeId> metatable, Tags tags,
std::shared_ptr<ClassUserData> userData, ModuleName definitionModuleName, std::optional<TableIndexer> indexer)
: name(name)
, props(props)
, parent(parent)
, metatable(metatable)
, tags(tags)
, userData(userData)
, definitionModuleName(definitionModuleName)
, indexer(indexer)
{
LUAU_ASSERT(FFlag::LuauTypecheckClassTypeIndexers);
}
};
/**
* An instance of a type family that has not yet been reduced to a more concrete
* type. The constraint solver receives a constraint to reduce each
* TypeFamilyInstanceType to a concrete type. A design detail is important to
* note here: the parameters for this instantiation of the type family are
* contained within this type, so that they can be substituted.
*/
struct TypeFamilyInstanceType
{
NotNull<TypeFamily> family;
std::vector<TypeId> typeArguments;
std::vector<TypePackId> packArguments;
};
struct TypeFun
@ -640,8 +673,9 @@ struct NegationType
using ErrorType = Unifiable::Error;
using TypeVariant = Unifiable::Variant<TypeId, FreeType, GenericType, PrimitiveType, BlockedType, PendingExpansionType, SingletonType, FunctionType,
TableType, MetatableType, ClassType, AnyType, UnionType, IntersectionType, LazyType, UnknownType, NeverType, NegationType>;
using TypeVariant =
Unifiable::Variant<TypeId, FreeType, GenericType, PrimitiveType, BlockedType, PendingExpansionType, SingletonType, FunctionType, TableType,
MetatableType, ClassType, AnyType, UnionType, IntersectionType, LazyType, UnknownType, NeverType, NegationType, TypeFamilyInstanceType>;
struct Type final
{

View file

@ -0,0 +1,115 @@
// This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details
#pragma once
#include "Luau/Error.h"
#include "Luau/NotNull.h"
#include "Luau/Variant.h"
#include <functional>
#include <string>
#include <optional>
namespace Luau
{
struct Type;
using TypeId = const Type*;
struct TypePackVar;
using TypePackId = const TypePackVar*;
struct TypeArena;
struct BuiltinTypes;
struct TxnLog;
/// Represents a reduction result, which may have successfully reduced the type,
/// may have concretely failed to reduce the type, or may simply be stuck
/// without more information.
template<typename Ty>
struct TypeFamilyReductionResult
{
/// The result of the reduction, if any. If this is nullopt, the family
/// could not be reduced.
std::optional<Ty> result;
/// Whether the result is uninhabited: whether we know, unambiguously and
/// permanently, whether this type family reduction results in an
/// uninhabitable type. This will trigger an error to be reported.
bool uninhabited;
/// Any types that need to be progressed or mutated before the reduction may
/// proceed.
std::vector<TypeId> blockedTypes;
/// Any type packs that need to be progressed or mutated before the
/// reduction may proceed.
std::vector<TypePackId> blockedPacks;
};
/// Represents a type function that may be applied to map a series of types and
/// type packs to a single output type.
struct TypeFamily
{
/// The human-readable name of the type family. Used to stringify instance
/// types.
std::string name;
/// The reducer function for the type family.
std::function<TypeFamilyReductionResult<TypeId>(
std::vector<TypeId>, std::vector<TypePackId>, NotNull<TypeArena>, NotNull<BuiltinTypes>, NotNull<const TxnLog> log)>
reducer;
};
/// Represents a type function that may be applied to map a series of types and
/// type packs to a single output type pack.
struct TypePackFamily
{
/// The human-readable name of the type pack family. Used to stringify
/// instance packs.
std::string name;
/// The reducer function for the type pack family.
std::function<TypeFamilyReductionResult<TypePackId>(
std::vector<TypeId>, std::vector<TypePackId>, NotNull<TypeArena>, NotNull<BuiltinTypes>, NotNull<const TxnLog> log)>
reducer;
};
struct FamilyGraphReductionResult
{
ErrorVec errors;
DenseHashSet<TypeId> blockedTypes{nullptr};
DenseHashSet<TypePackId> blockedPacks{nullptr};
DenseHashSet<TypeId> reducedTypes{nullptr};
DenseHashSet<TypePackId> reducedPacks{nullptr};
};
/**
* Attempt to reduce all instances of any type or type pack family in the type
* graph provided.
*
* @param entrypoint the entry point to the type graph.
* @param location the location the reduction is occurring at; used to populate
* type errors.
* @param arena an arena to allocate types into.
* @param builtins the built-in types.
* @param log a TxnLog to use. If one is provided, substitution will take place
* against the TxnLog, otherwise substitutions will directly mutate the type
* graph. Do not provide the empty TxnLog, as a result.
*/
FamilyGraphReductionResult reduceFamilies(
TypeId entrypoint, Location location, NotNull<TypeArena> arena, NotNull<BuiltinTypes> builtins, TxnLog* log = nullptr, bool force = false);
/**
* Attempt to reduce all instances of any type or type pack family in the type
* graph provided.
*
* @param entrypoint the entry point to the type graph.
* @param location the location the reduction is occurring at; used to populate
* type errors.
* @param arena an arena to allocate types into.
* @param builtins the built-in types.
* @param log a TxnLog to use. If one is provided, substitution will take place
* against the TxnLog, otherwise substitutions will directly mutate the type
* graph. Do not provide the empty TxnLog, as a result.
*/
FamilyGraphReductionResult reduceFamilies(
TypePackId entrypoint, Location location, NotNull<TypeArena> arena, NotNull<BuiltinTypes> builtins, TxnLog* log = nullptr, bool force = false);
} // namespace Luau

View file

@ -12,11 +12,13 @@ namespace Luau
{
struct TypeArena;
struct TypePackFamily;
struct TxnLog;
struct TypePack;
struct VariadicTypePack;
struct BlockedTypePack;
struct TypeFamilyInstanceTypePack;
struct TypePackVar;
using TypePackId = const TypePackVar*;
@ -50,10 +52,10 @@ struct GenericTypePack
};
using BoundTypePack = Unifiable::Bound<TypePackId>;
using ErrorTypePack = Unifiable::Error;
using TypePackVariant = Unifiable::Variant<TypePackId, FreeTypePack, GenericTypePack, TypePack, VariadicTypePack, BlockedTypePack>;
using TypePackVariant =
Unifiable::Variant<TypePackId, FreeTypePack, GenericTypePack, TypePack, VariadicTypePack, BlockedTypePack, TypeFamilyInstanceTypePack>;
/* A TypePack is a rope-like string of TypeIds. We use this structure to encode
* notions like packs of unknown length and packs of any length, as well as more
@ -83,6 +85,17 @@ struct BlockedTypePack
static size_t nextIndex;
};
/**
* Analogous to a TypeFamilyInstanceType.
*/
struct TypeFamilyInstanceTypePack
{
NotNull<TypePackFamily> family;
std::vector<TypeId> typeArguments;
std::vector<TypePackId> packArguments;
};
struct TypePackVar
{
explicit TypePackVar(const TypePackVariant& ty);

View file

@ -64,9 +64,11 @@ struct Unifier
Variance variance = Covariant;
bool normalize = true; // Normalize unions and intersections if necessary
bool checkInhabited = true; // Normalize types to check if they are inhabited
bool useScopes = false; // If true, we use the scope hierarchy rather than TypeLevels
CountMismatch::Context ctx = CountMismatch::Arg;
// If true, generics act as free types when unifying.
bool hideousFixMeGenericsAreActuallyFree = false;
UnifierSharedState& sharedState;
// When the Unifier is forced to unify two blocked types (or packs), they
@ -78,6 +80,10 @@ struct Unifier
Unifier(
NotNull<Normalizer> normalizer, Mode mode, NotNull<Scope> scope, const Location& location, Variance variance, TxnLog* parentLog = nullptr);
// Configure the Unifier to test for scope subsumption via embedded Scope
// pointers rather than TypeLevels.
void enableScopeTests();
// Test whether the two type vars unify. Never commits the result.
ErrorVec canUnify(TypeId subTy, TypeId superTy);
ErrorVec canUnify(TypePackId subTy, TypePackId superTy, bool isFunctionCall = false);
@ -159,6 +165,9 @@ private:
// Available after regular type pack unification errors
std::optional<int> firstPackErrorPos;
// If true, we use the scope hierarchy rather than TypeLevels
bool useScopes = false;
};
void promoteTypeLevels(TxnLog& log, const TypeArena* arena, TypeLevel minLevel, Scope* outerScope, bool useScope, TypePackId tp);

View file

@ -159,6 +159,10 @@ struct GenericTypeVisitor
{
return visit(ty);
}
virtual bool visit(TypeId ty, const TypeFamilyInstanceType& tfit)
{
return visit(ty);
}
virtual bool visit(TypePackId tp)
{
@ -192,6 +196,10 @@ struct GenericTypeVisitor
{
return visit(tp);
}
virtual bool visit(TypePackId tp, const TypeFamilyInstanceTypePack& tfitp)
{
return visit(tp);
}
void traverse(TypeId ty)
{
@ -272,6 +280,15 @@ struct GenericTypeVisitor
if (ctv->metatable)
traverse(*ctv->metatable);
if (FFlag::LuauTypecheckClassTypeIndexers)
{
if (ctv->indexer)
{
traverse(ctv->indexer->indexType);
traverse(ctv->indexer->indexResultType);
}
}
}
}
else if (auto atv = get<AnyType>(ty))
@ -327,6 +344,17 @@ struct GenericTypeVisitor
if (visit(ty, *ntv))
traverse(ntv->ty);
}
else if (auto tfit = get<TypeFamilyInstanceType>(ty))
{
if (visit(ty, *tfit))
{
for (TypeId p : tfit->typeArguments)
traverse(p);
for (TypePackId p : tfit->packArguments)
traverse(p);
}
}
else
LUAU_ASSERT(!"GenericTypeVisitor::traverse(TypeId) is not exhaustive!");
@ -376,6 +404,17 @@ struct GenericTypeVisitor
}
else if (auto btp = get<BlockedTypePack>(tp))
visit(tp, *btp);
else if (auto tfitp = get<TypeFamilyInstanceTypePack>(tp))
{
if (visit(tp, *tfitp))
{
for (TypeId t : tfitp->typeArguments)
traverse(t);
for (TypePackId t : tfitp->packArguments)
traverse(t);
}
}
else
LUAU_ASSERT(!"GenericTypeVisitor::traverse(TypePackId) is not exhaustive!");

View file

@ -52,6 +52,12 @@ Property clone(const Property& prop, TypeArena& dest, CloneState& cloneState)
}
}
static TableIndexer clone(const TableIndexer& indexer, TypeArena& dest, CloneState& cloneState)
{
LUAU_ASSERT(FFlag::LuauTypecheckClassTypeIndexers);
return TableIndexer{clone(indexer.indexType, dest, cloneState), clone(indexer.indexResultType, dest, cloneState)};
}
struct TypePackCloner;
/*
@ -98,6 +104,7 @@ struct TypeCloner
void operator()(const UnknownType& t);
void operator()(const NeverType& t);
void operator()(const NegationType& t);
void operator()(const TypeFamilyInstanceType& t);
};
struct TypePackCloner
@ -171,6 +178,22 @@ struct TypePackCloner
if (t.tail)
destTp->tail = clone(*t.tail, dest, cloneState);
}
void operator()(const TypeFamilyInstanceTypePack& t)
{
TypePackId cloned = dest.addTypePack(TypeFamilyInstanceTypePack{t.family, {}, {}});
TypeFamilyInstanceTypePack* destTp = getMutable<TypeFamilyInstanceTypePack>(cloned);
LUAU_ASSERT(destTp);
seenTypePacks[typePackId] = cloned;
destTp->typeArguments.reserve(t.typeArguments.size());
for (TypeId ty : t.typeArguments)
destTp->typeArguments.push_back(clone(ty, dest, cloneState));
destTp->packArguments.reserve(t.packArguments.size());
for (TypePackId tp : t.packArguments)
destTp->packArguments.push_back(clone(tp, dest, cloneState));
}
};
template<typename T>
@ -288,8 +311,16 @@ void TypeCloner::operator()(const TableType& t)
for (const auto& [name, prop] : t.props)
ttv->props[name] = clone(prop, dest, cloneState);
if (t.indexer)
ttv->indexer = TableIndexer{clone(t.indexer->indexType, dest, cloneState), clone(t.indexer->indexResultType, dest, cloneState)};
if (FFlag::LuauTypecheckClassTypeIndexers)
{
if (t.indexer)
ttv->indexer = clone(*t.indexer, dest, cloneState);
}
else
{
if (t.indexer)
ttv->indexer = TableIndexer{clone(t.indexer->indexType, dest, cloneState), clone(t.indexer->indexResultType, dest, cloneState)};
}
for (TypeId& arg : ttv->instantiatedTypeParams)
arg = clone(arg, dest, cloneState);
@ -327,6 +358,12 @@ void TypeCloner::operator()(const ClassType& t)
if (t.metatable)
ctv->metatable = clone(*t.metatable, dest, cloneState);
if (FFlag::LuauTypecheckClassTypeIndexers)
{
if (t.indexer)
ctv->indexer = clone(*t.indexer, dest, cloneState);
}
}
void TypeCloner::operator()(const AnyType& t)
@ -389,6 +426,28 @@ void TypeCloner::operator()(const NegationType& t)
asMutable(result)->ty = NegationType{ty};
}
void TypeCloner::operator()(const TypeFamilyInstanceType& t)
{
TypeId result = dest.addType(TypeFamilyInstanceType{
t.family,
{},
{},
});
seenTypes[typeId] = result;
TypeFamilyInstanceType* tfit = getMutable<TypeFamilyInstanceType>(result);
LUAU_ASSERT(tfit != nullptr);
tfit->typeArguments.reserve(t.typeArguments.size());
for (TypeId p : t.typeArguments)
tfit->typeArguments.push_back(clone(p, dest, cloneState));
tfit->packArguments.reserve(t.packArguments.size());
for (TypePackId p : t.packArguments)
tfit->packArguments.push_back(clone(p, dest, cloneState));
}
} // anonymous namespace
TypePackId clone(TypePackId tp, TypeArena& dest, CloneState& cloneState)

View file

@ -728,6 +728,7 @@ ControlFlow ConstraintGraphBuilder::visit(const ScopePtr& scope, AstStatLocalFun
});
addConstraint(scope, std::move(c));
module->astTypes[function->func] = functionType;
return ControlFlow::None;
}
@ -1475,7 +1476,7 @@ Inference ConstraintGraphBuilder::check(
Checkpoint endCheckpoint = checkpoint(this);
TypeId generalizedTy = arena->addType(BlockedType{});
NotNull<Constraint> gc = addConstraint(scope, expr->location, GeneralizationConstraint{generalizedTy, sig.signature});
NotNull<Constraint> gc = addConstraint(sig.signatureScope, expr->location, GeneralizationConstraint{generalizedTy, sig.signature});
forEachConstraint(startCheckpoint, endCheckpoint, this, [gc](const ConstraintPtr& constraint) {
gc->dependencies.emplace_back(constraint.get());

View file

@ -16,6 +16,7 @@
#include "Luau/Type.h"
#include "Luau/Unifier.h"
#include "Luau/VisitType.h"
#include "Luau/TypeFamily.h"
LUAU_FASTFLAGVARIABLE(DebugLuauLogSolver, false);
LUAU_FASTFLAG(LuauRequirePathTrueModuleName)
@ -226,6 +227,32 @@ void dump(ConstraintSolver* cs, ToStringOptions& opts)
}
}
struct InstantiationQueuer : TypeOnceVisitor
{
ConstraintSolver* solver;
NotNull<Scope> scope;
Location location;
explicit InstantiationQueuer(NotNull<Scope> scope, const Location& location, ConstraintSolver* solver)
: solver(solver)
, scope(scope)
, location(location)
{
}
bool visit(TypeId ty, const PendingExpansionType& petv) override
{
solver->pushConstraint(scope, location, TypeAliasExpansionConstraint{ty});
return false;
}
bool visit(TypeId ty, const TypeFamilyInstanceType& tfit) override
{
solver->pushConstraint(scope, location, ReduceConstraint{ty});
return true;
}
};
ConstraintSolver::ConstraintSolver(NotNull<Normalizer> normalizer, NotNull<Scope> rootScope, std::vector<NotNull<Constraint>> constraints,
ModuleName moduleName, NotNull<ModuleResolver> moduleResolver, std::vector<RequireCycle> requireCycles, DcrLogger* logger)
: arena(normalizer->arena)
@ -441,6 +468,10 @@ bool ConstraintSolver::tryDispatch(NotNull<const Constraint> constraint, bool fo
success = tryDispatch(*sottc, constraint);
else if (auto uc = get<UnpackConstraint>(*constraint))
success = tryDispatch(*uc, constraint);
else if (auto rc = get<ReduceConstraint>(*constraint))
success = tryDispatch(*rc, constraint, force);
else if (auto rpc = get<ReducePackConstraint>(*constraint))
success = tryDispatch(*rpc, constraint, force);
else
LUAU_ASSERT(false);
@ -479,13 +510,19 @@ bool ConstraintSolver::tryDispatch(const GeneralizationConstraint& c, NotNull<co
else if (get<PendingExpansionType>(generalizedType))
return block(generalizedType, constraint);
std::optional<TypeId> generalized = quantify(arena, c.sourceType, constraint->scope);
std::optional<QuantifierResult> generalized = quantify(arena, c.sourceType, constraint->scope);
if (generalized)
{
if (get<BlockedType>(generalizedType))
asMutable(generalizedType)->ty.emplace<BoundType>(*generalized);
asMutable(generalizedType)->ty.emplace<BoundType>(generalized->result);
else
unify(generalizedType, *generalized, constraint->scope);
unify(generalizedType, generalized->result, constraint->scope);
for (auto [free, gen] : generalized->insertedGenerics.pairings)
unify(free, gen, constraint->scope);
for (auto [free, gen] : generalized->insertedGenericPacks.pairings)
unify(free, gen, constraint->scope);
}
else
{
@ -504,6 +541,9 @@ bool ConstraintSolver::tryDispatch(const InstantiationConstraint& c, NotNull<con
if (isBlocked(c.superType))
return block(c.superType, constraint);
if (!recursiveBlock(c.superType, constraint))
return false;
Instantiation inst(TxnLog::empty(), arena, TypeLevel{}, constraint->scope);
std::optional<TypeId> instantiated = inst.substitute(c.superType);
@ -512,6 +552,9 @@ bool ConstraintSolver::tryDispatch(const InstantiationConstraint& c, NotNull<con
LUAU_ASSERT(get<BlockedType>(c.subType));
asMutable(c.subType)->ty.emplace<BoundType>(*instantiated);
InstantiationQueuer queuer{constraint->scope, constraint->location, this};
queuer.traverse(c.subType);
unblock(c.subType);
return true;
@ -953,26 +996,6 @@ struct InfiniteTypeFinder : TypeOnceVisitor
}
};
struct InstantiationQueuer : TypeOnceVisitor
{
ConstraintSolver* solver;
NotNull<Scope> scope;
Location location;
explicit InstantiationQueuer(NotNull<Scope> scope, const Location& location, ConstraintSolver* solver)
: solver(solver)
, scope(scope)
, location(location)
{
}
bool visit(TypeId ty, const PendingExpansionType& petv) override
{
solver->pushConstraint(scope, location, TypeAliasExpansionConstraint{ty});
return false;
}
};
bool ConstraintSolver::tryDispatch(const TypeAliasExpansionConstraint& c, NotNull<const Constraint> constraint)
{
const PendingExpansionType* petv = get<PendingExpansionType>(follow(c.target));
@ -1246,7 +1269,7 @@ bool ConstraintSolver::tryDispatch(const FunctionCallConstraint& c, NotNull<cons
LUAU_ASSERT(instantiated); // TODO FIXME HANDLE THIS
Unifier u{normalizer, Mode::Strict, constraint->scope, Location{}, Covariant};
u.useScopes = true;
u.enableScopeTests();
u.tryUnify(*instantiated, inferredTy, /* isFunctionCall */ true);
@ -1278,8 +1301,12 @@ bool ConstraintSolver::tryDispatch(const FunctionCallConstraint& c, NotNull<cons
u.log.commit();
unblock(changedTypes);
unblock(changedPacks);
unblock(c.result);
InstantiationQueuer queuer{constraint->scope, constraint->location, this};
queuer.traverse(fn);
queuer.traverse(inferredTy);
return true;
}
}
@ -1295,7 +1322,7 @@ bool ConstraintSolver::tryDispatch(const FunctionCallConstraint& c, NotNull<cons
// We found no matching overloads.
Unifier u{normalizer, Mode::Strict, constraint->scope, Location{}, Covariant};
u.useScopes = true;
u.enableScopeTests();
u.tryUnify(inferredTy, builtinTypes->anyType);
u.tryUnify(fn, builtinTypes->anyType);
@ -1305,8 +1332,12 @@ bool ConstraintSolver::tryDispatch(const FunctionCallConstraint& c, NotNull<cons
unblock(changedTypes);
unblock(changedPacks);
unblock(c.result);
InstantiationQueuer queuer{constraint->scope, constraint->location, this};
queuer.traverse(fn);
queuer.traverse(inferredTy);
return true;
}
@ -1567,8 +1598,11 @@ bool ConstraintSolver::tryDispatch(const SetIndexerConstraint& c, NotNull<const
}
else if (tt->state == TableState::Free || tt->state == TableState::Unsealed)
{
TypeId promotedIndexTy = arena->freshType(tt->scope);
unify(c.indexType, promotedIndexTy, constraint->scope);
auto mtt = getMutable<TableType>(subjectType);
mtt->indexer = TableIndexer{c.indexType, c.propType};
mtt->indexer = TableIndexer{promotedIndexTy, c.propType};
asMutable(c.propType)->ty.emplace<FreeType>(tt->scope);
asMutable(c.resultType)->ty.emplace<BoundType>(subjectType);
unblock(c.propType);
@ -1666,6 +1700,52 @@ bool ConstraintSolver::tryDispatch(const UnpackConstraint& c, NotNull<const Cons
return true;
}
bool ConstraintSolver::tryDispatch(const ReduceConstraint& c, NotNull<const Constraint> constraint, bool force)
{
TypeId ty = follow(c.ty);
FamilyGraphReductionResult result = reduceFamilies(ty, constraint->location, NotNull{arena}, builtinTypes, nullptr, force);
for (TypeId r : result.reducedTypes)
unblock(r);
for (TypePackId r : result.reducedPacks)
unblock(r);
if (force)
return true;
for (TypeId b : result.blockedTypes)
block(b, constraint);
for (TypePackId b : result.blockedPacks)
block(b, constraint);
return result.blockedTypes.empty() && result.blockedPacks.empty();
}
bool ConstraintSolver::tryDispatch(const ReducePackConstraint& c, NotNull<const Constraint> constraint, bool force)
{
TypePackId tp = follow(c.tp);
FamilyGraphReductionResult result = reduceFamilies(tp, constraint->location, NotNull{arena}, builtinTypes, nullptr, force);
for (TypeId r : result.reducedTypes)
unblock(r);
for (TypePackId r : result.reducedPacks)
unblock(r);
if (force)
return true;
for (TypeId b : result.blockedTypes)
block(b, constraint);
for (TypePackId b : result.blockedPacks)
block(b, constraint);
return result.blockedTypes.empty() && result.blockedPacks.empty();
}
bool ConstraintSolver::tryDispatchIterableTable(TypeId iteratorTy, const IterableConstraint& c, NotNull<const Constraint> constraint, bool force)
{
auto block_ = [&](auto&& t) {
@ -2031,7 +2111,7 @@ template <typename TID>
bool ConstraintSolver::tryUnify(NotNull<const Constraint> constraint, TID subTy, TID superTy)
{
Unifier u{normalizer, Mode::Strict, constraint->scope, constraint->location, Covariant};
u.useScopes = true;
u.enableScopeTests();
u.tryUnify(subTy, superTy);
@ -2195,10 +2275,11 @@ void ConstraintSolver::unblock(NotNull<const Constraint> progressed)
return unblock_(progressed.get());
}
void ConstraintSolver::unblock(TypeId progressed)
void ConstraintSolver::unblock(TypeId ty)
{
DenseHashSet<TypeId> seen{nullptr};
TypeId progressed = ty;
while (true)
{
if (seen.find(progressed))
@ -2256,7 +2337,7 @@ bool ConstraintSolver::isBlocked(NotNull<const Constraint> constraint)
void ConstraintSolver::unify(TypeId subType, TypeId superType, NotNull<Scope> scope)
{
Unifier u{normalizer, Mode::Strict, scope, Location{}, Covariant};
u.useScopes = true;
u.enableScopeTests();
u.tryUnify(subType, superType);
@ -2279,7 +2360,7 @@ void ConstraintSolver::unify(TypePackId subPack, TypePackId superPack, NotNull<S
{
UnifierSharedState sharedState{&iceReporter};
Unifier u{normalizer, Mode::Strict, scope, Location{}, Covariant};
u.useScopes = true;
u.enableScopeTests();
u.tryUnify(subPack, superPack);
@ -2374,7 +2455,7 @@ TypeId ConstraintSolver::unionOfTypes(TypeId a, TypeId b, NotNull<Scope> scope,
if (unifyFreeTypes && (get<FreeType>(a) || get<FreeType>(b)))
{
Unifier u{normalizer, Mode::Strict, scope, Location{}, Covariant};
u.useScopes = true;
u.enableScopeTests();
u.tryUnify(b, a);
if (u.errors.empty())

View file

@ -484,6 +484,16 @@ struct ErrorConverter
{
return "Attempting a dynamic property access on type '" + Luau::toString(e.ty) + "' is unsafe and may cause exceptions at runtime";
}
std::string operator()(const UninhabitedTypeFamily& e) const
{
return "Type family instance " + Luau::toString(e.ty) + " is uninhabited";
}
std::string operator()(const UninhabitedTypePackFamily& e) const
{
return "Type pack family instance " + Luau::toString(e.tp) + " is uninhabited";
}
};
struct InvalidNameChecker
@ -786,6 +796,16 @@ bool DynamicPropertyLookupOnClassesUnsafe::operator==(const DynamicPropertyLooku
return ty == rhs.ty;
}
bool UninhabitedTypeFamily::operator==(const UninhabitedTypeFamily& rhs) const
{
return ty == rhs.ty;
}
bool UninhabitedTypePackFamily::operator==(const UninhabitedTypePackFamily& rhs) const
{
return tp == rhs.tp;
}
std::string toString(const TypeError& error)
{
return toString(error, TypeErrorToStringOptions{});
@ -944,6 +964,10 @@ void copyError(T& e, TypeArena& destArena, CloneState cloneState)
}
else if constexpr (std::is_same_v<T, DynamicPropertyLookupOnClassesUnsafe>)
e.ty = clone(e.ty);
else if constexpr (std::is_same_v<T, UninhabitedTypeFamily>)
e.ty = clone(e.ty);
else if constexpr (std::is_same_v<T, UninhabitedTypePackFamily>)
e.tp = clone(e.tp);
else
static_assert(always_false_v<T>, "Non-exhaustive type switch");
}

View file

@ -192,6 +192,10 @@ static void errorToString(std::ostream& stream, const T& err)
stream << "TypePackMismatch { wanted = '" + toString(err.wantedTp) + "', given = '" + toString(err.givenTp) + "' }";
else if constexpr (std::is_same_v<T, DynamicPropertyLookupOnClassesUnsafe>)
stream << "DynamicPropertyLookupOnClassesUnsafe { " << toString(err.ty) << " }";
else if constexpr (std::is_same_v<T, UninhabitedTypeFamily>)
stream << "UninhabitedTypeFamily { " << toString(err.ty) << " }";
else if constexpr (std::is_same_v<T, UninhabitedTypePackFamily>)
stream << "UninhabitedTypePackFamily { " << toString(err.tp) << " }";
else
static_assert(always_false_v<T>, "Non-exhaustive type switch");
}

View file

@ -154,8 +154,8 @@ void quantify(TypeId ty, TypeLevel level)
struct PureQuantifier : Substitution
{
Scope* scope;
std::vector<TypeId> insertedGenerics;
std::vector<TypePackId> insertedGenericPacks;
OrderedMap<TypeId, TypeId> insertedGenerics;
OrderedMap<TypePackId, TypePackId> insertedGenericPacks;
bool seenMutableType = false;
bool seenGenericType = false;
@ -203,7 +203,7 @@ struct PureQuantifier : Substitution
if (auto ftv = get<FreeType>(ty))
{
TypeId result = arena->addType(GenericType{scope});
insertedGenerics.push_back(result);
insertedGenerics.push(ty, result);
return result;
}
else if (auto ttv = get<TableType>(ty))
@ -217,7 +217,10 @@ struct PureQuantifier : Substitution
resultTable->scope = scope;
if (ttv->state == TableState::Free)
{
resultTable->state = TableState::Generic;
insertedGenerics.push(ty, result);
}
else if (ttv->state == TableState::Unsealed)
resultTable->state = TableState::Sealed;
@ -231,8 +234,8 @@ struct PureQuantifier : Substitution
{
if (auto ftp = get<FreeTypePack>(tp))
{
TypePackId result = arena->addTypePack(TypePackVar{GenericTypePack{}});
insertedGenericPacks.push_back(result);
TypePackId result = arena->addTypePack(TypePackVar{GenericTypePack{scope}});
insertedGenericPacks.push(tp, result);
return result;
}
@ -252,7 +255,7 @@ struct PureQuantifier : Substitution
}
};
std::optional<TypeId> quantify(TypeArena* arena, TypeId ty, Scope* scope)
std::optional<QuantifierResult> quantify(TypeArena* arena, TypeId ty, Scope* scope)
{
PureQuantifier quantifier{arena, scope};
std::optional<TypeId> result = quantifier.substitute(ty);
@ -262,11 +265,20 @@ std::optional<TypeId> quantify(TypeArena* arena, TypeId ty, Scope* scope)
FunctionType* ftv = getMutable<FunctionType>(*result);
LUAU_ASSERT(ftv);
ftv->scope = scope;
ftv->generics.insert(ftv->generics.end(), quantifier.insertedGenerics.begin(), quantifier.insertedGenerics.end());
ftv->genericPacks.insert(ftv->genericPacks.end(), quantifier.insertedGenericPacks.begin(), quantifier.insertedGenericPacks.end());
for (auto k : quantifier.insertedGenerics.keys)
{
TypeId g = quantifier.insertedGenerics.pairings[k];
if (get<GenericType>(g))
ftv->generics.push_back(g);
}
for (auto k : quantifier.insertedGenericPacks.keys)
ftv->genericPacks.push_back(quantifier.insertedGenericPacks.pairings[k]);
ftv->hasNoGenerics = ftv->generics.empty() && ftv->genericPacks.empty() && !quantifier.seenGenericType && !quantifier.seenMutableType;
return *result;
return std::optional<QuantifierResult>({*result, std::move(quantifier.insertedGenerics), std::move(quantifier.insertedGenericPacks)});
}
} // namespace Luau

View file

@ -78,6 +78,11 @@ static TypeId DEPRECATED_shallowClone(TypeId ty, TypeArena& dest, const TxnLog*
{
result = dest.addType(NegationType{ntv->ty});
}
else if (const TypeFamilyInstanceType* tfit = get<TypeFamilyInstanceType>(ty))
{
TypeFamilyInstanceType clone{tfit->family, tfit->typeArguments, tfit->packArguments};
result = dest.addType(std::move(clone));
}
else
return result;
@ -168,14 +173,27 @@ static TypeId shallowClone(TypeId ty, TypeArena& dest, const TxnLog* log, bool a
{
if (alwaysClone)
{
ClassType clone{a.name, a.props, a.parent, a.metatable, a.tags, a.userData, a.definitionModuleName};
return dest.addType(std::move(clone));
if (FFlag::LuauTypecheckClassTypeIndexers)
{
ClassType clone{a.name, a.props, a.parent, a.metatable, a.tags, a.userData, a.definitionModuleName, a.indexer};
return dest.addType(std::move(clone));
}
else
{
ClassType clone{a.name, a.props, a.parent, a.metatable, a.tags, a.userData, a.definitionModuleName};
return dest.addType(std::move(clone));
}
}
else
return ty;
}
else if constexpr (std::is_same_v<T, NegationType>)
return dest.addType(NegationType{a.ty});
else if constexpr (std::is_same_v<T, TypeFamilyInstanceType>)
{
TypeFamilyInstanceType clone{a.family, a.typeArguments, a.packArguments};
return dest.addType(std::move(clone));
}
else
static_assert(always_false_v<T>, "Non-exhaustive shallowClone switch");
};
@ -255,6 +273,14 @@ void Tarjan::visitChildren(TypeId ty, int index)
for (TypePackId a : petv->packArguments)
visitChild(a);
}
else if (const TypeFamilyInstanceType* tfit = get<TypeFamilyInstanceType>(ty))
{
for (TypeId a : tfit->typeArguments)
visitChild(a);
for (TypePackId a : tfit->packArguments)
visitChild(a);
}
else if (const ClassType* ctv = get<ClassType>(ty); FFlag::LuauClassTypeVarsInSubstitution && ctv)
{
for (const auto& [name, prop] : ctv->props)
@ -265,6 +291,15 @@ void Tarjan::visitChildren(TypeId ty, int index)
if (ctv->metatable)
visitChild(*ctv->metatable);
if (FFlag::LuauTypecheckClassTypeIndexers)
{
if (ctv->indexer)
{
visitChild(ctv->indexer->indexType);
visitChild(ctv->indexer->indexResultType);
}
}
}
else if (const NegationType* ntv = get<NegationType>(ty))
{
@ -669,6 +704,14 @@ TypePackId Substitution::clone(TypePackId tp)
clone.hidden = vtp->hidden;
return addTypePack(std::move(clone));
}
else if (const TypeFamilyInstanceTypePack* tfitp = get<TypeFamilyInstanceTypePack>(tp))
{
TypeFamilyInstanceTypePack clone{
tfitp->family, std::vector<TypeId>(tfitp->typeArguments.size()), std::vector<TypePackId>(tfitp->packArguments.size())};
clone.typeArguments.assign(tfitp->typeArguments.begin(), tfitp->typeArguments.end());
clone.packArguments.assign(tfitp->packArguments.begin(), tfitp->packArguments.end());
return addTypePack(std::move(clone));
}
else if (FFlag::LuauClonePublicInterfaceLess2)
{
return addTypePack(*tp);
@ -786,6 +829,14 @@ void Substitution::replaceChildren(TypeId ty)
for (TypePackId& a : petv->packArguments)
a = replace(a);
}
else if (TypeFamilyInstanceType* tfit = getMutable<TypeFamilyInstanceType>(ty))
{
for (TypeId& a : tfit->typeArguments)
a = replace(a);
for (TypePackId& a : tfit->packArguments)
a = replace(a);
}
else if (ClassType* ctv = getMutable<ClassType>(ty); FFlag::LuauClassTypeVarsInSubstitution && ctv)
{
for (auto& [name, prop] : ctv->props)
@ -796,6 +847,15 @@ void Substitution::replaceChildren(TypeId ty)
if (ctv->metatable)
ctv->metatable = replace(*ctv->metatable);
if (FFlag::LuauTypecheckClassTypeIndexers)
{
if (ctv->indexer)
{
ctv->indexer->indexType = replace(ctv->indexer->indexType);
ctv->indexer->indexResultType = replace(ctv->indexer->indexResultType);
}
}
}
else if (NegationType* ntv = getMutable<NegationType>(ty))
{
@ -824,6 +884,14 @@ void Substitution::replaceChildren(TypePackId tp)
{
vtp->ty = replace(vtp->ty);
}
else if (TypeFamilyInstanceTypePack* tfitp = getMutable<TypeFamilyInstanceTypePack>(tp))
{
for (TypeId& t : tfitp->typeArguments)
t = replace(t);
for (TypePackId& t : tfitp->packArguments)
t = replace(t);
}
}
} // namespace Luau

View file

@ -257,6 +257,15 @@ void StateDot::visitChildren(TypeId ty, int index)
if (ctv->metatable)
visitChild(*ctv->metatable, index, "[metatable]");
if (FFlag::LuauTypecheckClassTypeIndexers)
{
if (ctv->indexer)
{
visitChild(ctv->indexer->indexType, index, "[index]");
visitChild(ctv->indexer->indexResultType, index, "[value]");
}
}
}
else if (const SingletonType* stv = get<SingletonType>(ty))
{

View file

@ -8,6 +8,7 @@
#include "Luau/TypeInfer.h"
#include "Luau/TypePack.h"
#include "Luau/Type.h"
#include "Luau/TypeFamily.h"
#include "Luau/VisitType.h"
#include <algorithm>
@ -16,11 +17,22 @@
LUAU_FASTFLAG(DebugLuauDeferredConstraintResolution)
/*
* Prefix generic typenames with gen-
* Additionally, free types will be prefixed with free- and suffixed with their level. eg free-a-4
* Fair warning: Setting this will break a lot of Luau unit tests.
* Enables increasing levels of verbosity for Luau type names when stringifying.
* After level 2, test cases will break unpredictably because a pointer to their
* scope will be included in the stringification of generic and free types.
*
* Supported values:
*
* 0: Disabled, no changes.
*
* 1: Prefix free/generic types with free- and gen-, respectively. Also reveal
* hidden variadic tails.
*
* 2: Suffix free/generic types with their scope depth.
*
* 3: Suffix free/generic types with their scope pointer, if present.
*/
LUAU_FASTFLAGVARIABLE(DebugLuauVerboseTypeNames, false)
LUAU_FASTINTVARIABLE(DebugLuauVerboseTypeNames, 0)
LUAU_FASTFLAGVARIABLE(DebugLuauToStringNoLexicalSort, false)
namespace Luau
@ -223,11 +235,15 @@ struct StringifierState
++count;
emit(count);
emit("-");
char buffer[16];
uint32_t s = uint32_t(intptr_t(scope) & 0xFFFFFF);
snprintf(buffer, sizeof(buffer), "0x%x", s);
emit(buffer);
if (FInt::DebugLuauVerboseTypeNames >= 3)
{
emit("-");
char buffer[16];
uint32_t s = uint32_t(intptr_t(scope) & 0xFFFFFF);
snprintf(buffer, sizeof(buffer), "0x%x", s);
emit(buffer);
}
}
void emit(TypeLevel level)
@ -371,11 +387,13 @@ struct TypeStringifier
void operator()(TypeId ty, const FreeType& ftv)
{
state.result.invalid = true;
if (FFlag::DebugLuauVerboseTypeNames)
if (FInt::DebugLuauVerboseTypeNames >= 1)
state.emit("free-");
state.emit(state.getName(ty));
if (FFlag::DebugLuauVerboseTypeNames)
if (FInt::DebugLuauVerboseTypeNames >= 2)
{
state.emit("-");
if (FFlag::DebugLuauDeferredConstraintResolution)
@ -392,6 +410,9 @@ struct TypeStringifier
void operator()(TypeId ty, const GenericType& gtv)
{
if (FInt::DebugLuauVerboseTypeNames >= 1)
state.emit("gen-");
if (gtv.explicitName)
{
state.usedNames.insert(gtv.name);
@ -401,7 +422,7 @@ struct TypeStringifier
else
state.emit(state.getName(ty));
if (FFlag::DebugLuauVerboseTypeNames)
if (FInt::DebugLuauVerboseTypeNames >= 2)
{
state.emit("-");
if (FFlag::DebugLuauDeferredConstraintResolution)
@ -871,6 +892,33 @@ struct TypeStringifier
if (parens)
state.emit(")");
}
void operator()(TypeId, const TypeFamilyInstanceType& tfitv)
{
state.emit(tfitv.family->name);
state.emit("<");
bool comma = false;
for (TypeId ty : tfitv.typeArguments)
{
if (comma)
state.emit(", ");
comma = true;
stringify(ty);
}
for (TypePackId tp : tfitv.packArguments)
{
if (comma)
state.emit(", ");
comma = true;
stringify(tp);
}
state.emit(">");
}
};
struct TypePackStringifier
@ -958,7 +1006,7 @@ struct TypePackStringifier
if (tp.tail && !isEmpty(*tp.tail))
{
TypePackId tail = follow(*tp.tail);
if (auto vtp = get<VariadicTypePack>(tail); !vtp || (!FFlag::DebugLuauVerboseTypeNames && !vtp->hidden))
if (auto vtp = get<VariadicTypePack>(tail); !vtp || (FInt::DebugLuauVerboseTypeNames < 1 && !vtp->hidden))
{
if (first)
first = false;
@ -981,7 +1029,7 @@ struct TypePackStringifier
void operator()(TypePackId, const VariadicTypePack& pack)
{
state.emit("...");
if (FFlag::DebugLuauVerboseTypeNames && pack.hidden)
if (FInt::DebugLuauVerboseTypeNames >= 1 && pack.hidden)
{
state.emit("*hidden*");
}
@ -990,6 +1038,9 @@ struct TypePackStringifier
void operator()(TypePackId tp, const GenericTypePack& pack)
{
if (FInt::DebugLuauVerboseTypeNames >= 1)
state.emit("gen-");
if (pack.explicitName)
{
state.usedNames.insert(pack.name);
@ -1001,7 +1052,7 @@ struct TypePackStringifier
state.emit(state.getName(tp));
}
if (FFlag::DebugLuauVerboseTypeNames)
if (FInt::DebugLuauVerboseTypeNames >= 2)
{
state.emit("-");
if (FFlag::DebugLuauDeferredConstraintResolution)
@ -1009,17 +1060,18 @@ struct TypePackStringifier
else
state.emit(pack.level);
}
state.emit("...");
}
void operator()(TypePackId tp, const FreeTypePack& pack)
{
state.result.invalid = true;
if (FFlag::DebugLuauVerboseTypeNames)
if (FInt::DebugLuauVerboseTypeNames >= 1)
state.emit("free-");
state.emit(state.getName(tp));
if (FFlag::DebugLuauVerboseTypeNames)
if (FInt::DebugLuauVerboseTypeNames >= 2)
{
state.emit("-");
if (FFlag::DebugLuauDeferredConstraintResolution)
@ -1042,6 +1094,33 @@ struct TypePackStringifier
state.emit(btp.index);
state.emit("*");
}
void operator()(TypePackId, const TypeFamilyInstanceTypePack& tfitp)
{
state.emit(tfitp.family->name);
state.emit("<");
bool comma = false;
for (TypeId p : tfitp.typeArguments)
{
if (comma)
state.emit(", ");
comma = true;
stringify(p);
}
for (TypePackId p : tfitp.packArguments)
{
if (comma)
state.emit(", ");
comma = true;
stringify(p);
}
state.emit(">");
}
};
void TypeStringifier::stringify(TypePackId tp)
@ -1560,6 +1639,12 @@ std::string toString(const Constraint& constraint, ToStringOptions& opts)
}
else if constexpr (std::is_same_v<T, UnpackConstraint>)
return tos(c.resultPack) + " ~ unpack " + tos(c.sourcePack);
else if constexpr (std::is_same_v<T, ReduceConstraint>)
return "reduce " + tos(c.ty);
else if constexpr (std::is_same_v<T, ReducePackConstraint>)
{
return "reduce " + tos(c.tp);
}
else
static_assert(always_false_v<T>, "Non-exhaustive constraint switch");
};

View file

@ -1,6 +1,7 @@
// This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details
#include "Luau/TxnLog.h"
#include "Luau/Scope.h"
#include "Luau/ToString.h"
#include "Luau/TypeArena.h"
#include "Luau/TypePack.h"
@ -8,6 +9,8 @@
#include <algorithm>
#include <stdexcept>
LUAU_FASTFLAG(DebugLuauDeferredConstraintResolution)
namespace Luau
{
@ -71,7 +74,11 @@ const TxnLog* TxnLog::empty()
void TxnLog::concat(TxnLog rhs)
{
for (auto& [ty, rep] : rhs.typeVarChanges)
{
if (rep->dead)
continue;
typeVarChanges[ty] = std::move(rep);
}
for (auto& [tp, rep] : rhs.typePackChanges)
typePackChanges[tp] = std::move(rep);
@ -81,7 +88,10 @@ void TxnLog::concatAsIntersections(TxnLog rhs, NotNull<TypeArena> arena)
{
for (auto& [ty, rightRep] : rhs.typeVarChanges)
{
if (auto leftRep = typeVarChanges.find(ty))
if (rightRep->dead)
continue;
if (auto leftRep = typeVarChanges.find(ty); leftRep && !(*leftRep)->dead)
{
TypeId leftTy = arena->addType((*leftRep)->pending);
TypeId rightTy = arena->addType(rightRep->pending);
@ -97,16 +107,94 @@ void TxnLog::concatAsIntersections(TxnLog rhs, NotNull<TypeArena> arena)
void TxnLog::concatAsUnion(TxnLog rhs, NotNull<TypeArena> arena)
{
for (auto& [ty, rightRep] : rhs.typeVarChanges)
if (FFlag::DebugLuauDeferredConstraintResolution)
{
if (auto leftRep = typeVarChanges.find(ty))
/*
* Check for cycles.
*
* We must not combine a log entry that binds 'a to 'b with a log that
* binds 'b to 'a.
*
* Of the two, identify the one with the 'bigger' scope and eliminate the
* entry that rebinds it.
*/
for (const auto& [rightTy, rightRep] : rhs.typeVarChanges)
{
TypeId leftTy = arena->addType((*leftRep)->pending);
TypeId rightTy = arena->addType(rightRep->pending);
typeVarChanges[ty]->pending.ty = UnionType{{leftTy, rightTy}};
if (rightRep->dead)
continue;
// We explicitly use get_if here because we do not wish to do anything
// if the uncommitted type is already bound to something else.
const FreeType* rf = get_if<FreeType>(&rightTy->ty);
if (!rf)
continue;
const BoundType* rb = Luau::get<BoundType>(&rightRep->pending);
if (!rb)
continue;
const TypeId leftTy = rb->boundTo;
const FreeType* lf = get_if<FreeType>(&leftTy->ty);
if (!lf)
continue;
auto leftRep = typeVarChanges.find(leftTy);
if (!leftRep)
continue;
if ((*leftRep)->dead)
continue;
const BoundType* lb = Luau::get<BoundType>(&(*leftRep)->pending);
if (!lb)
continue;
if (lb->boundTo == rightTy)
{
// leftTy has been bound to rightTy, but rightTy has also been bound
// to leftTy. We find the one that belongs to the more deeply nested
// scope and remove it from the log.
const bool discardLeft = useScopes ? subsumes(lf->scope, rf->scope) : lf->level.subsumes(rf->level);
if (discardLeft)
(*leftRep)->dead = true;
else
rightRep->dead = true;
}
}
for (auto& [ty, rightRep] : rhs.typeVarChanges)
{
if (rightRep->dead)
continue;
if (auto leftRep = typeVarChanges.find(ty); leftRep && !(*leftRep)->dead)
{
TypeId leftTy = arena->addType((*leftRep)->pending);
TypeId rightTy = arena->addType(rightRep->pending);
if (follow(leftTy) == follow(rightTy))
typeVarChanges[ty] = std::move(rightRep);
else
typeVarChanges[ty]->pending.ty = UnionType{{leftTy, rightTy}};
}
else
typeVarChanges[ty] = std::move(rightRep);
}
}
else
{
for (auto& [ty, rightRep] : rhs.typeVarChanges)
{
if (auto leftRep = typeVarChanges.find(ty))
{
TypeId leftTy = arena->addType((*leftRep)->pending);
TypeId rightTy = arena->addType(rightRep->pending);
typeVarChanges[ty]->pending.ty = UnionType{{leftTy, rightTy}};
}
else
typeVarChanges[ty] = std::move(rightRep);
}
else
typeVarChanges[ty] = std::move(rightRep);
}
for (auto& [tp, rep] : rhs.typePackChanges)
@ -116,7 +204,10 @@ void TxnLog::concatAsUnion(TxnLog rhs, NotNull<TypeArena> arena)
void TxnLog::commit()
{
for (auto& [ty, rep] : typeVarChanges)
asMutable(ty)->reassign(rep.get()->pending);
{
if (!rep->dead)
asMutable(ty)->reassign(rep.get()->pending);
}
for (auto& [tp, rep] : typePackChanges)
asMutable(tp)->reassign(rep.get()->pending);
@ -135,7 +226,10 @@ TxnLog TxnLog::inverse()
TxnLog inversed(sharedSeen);
for (auto& [ty, _rep] : typeVarChanges)
inversed.typeVarChanges[ty] = std::make_unique<PendingType>(*ty);
{
if (!_rep->dead)
inversed.typeVarChanges[ty] = std::make_unique<PendingType>(*ty);
}
for (auto& [tp, _rep] : typePackChanges)
inversed.typePackChanges[tp] = std::make_unique<PendingTypePack>(*tp);
@ -204,7 +298,7 @@ PendingType* TxnLog::queue(TypeId ty)
// Explicitly don't look in ancestors. If we have discovered something new
// about this type, we don't want to mutate the parent's state.
auto& pending = typeVarChanges[ty];
if (!pending)
if (!pending || (*pending).dead)
{
pending = std::make_unique<PendingType>(*ty);
pending->pending.owningArena = nullptr;
@ -237,7 +331,7 @@ PendingType* TxnLog::pending(TypeId ty) const
for (const TxnLog* current = this; current; current = current->parent)
{
if (auto it = current->typeVarChanges.find(ty))
if (auto it = current->typeVarChanges.find(ty); it && !(*it)->dead)
return it->get();
}

View file

@ -9,6 +9,7 @@
#include "Luau/TypeInfer.h"
#include "Luau/TypePack.h"
#include "Luau/Type.h"
#include "Luau/TypeFamily.h"
#include <string>
@ -362,6 +363,10 @@ public:
// FIXME: do the same thing we do with ErrorType
throw InternalCompilerError("Cannot convert NegationType into AstNode");
}
AstType* operator()(const TypeFamilyInstanceType& tfit)
{
return allocator->alloc<AstTypeReference>(Location(), std::nullopt, AstName{tfit.family->name.c_str()}, std::nullopt, Location());
}
private:
Allocator* allocator;
@ -432,6 +437,11 @@ public:
return allocator->alloc<AstTypePackGeneric>(Location(), AstName("Unifiable<Error>"));
}
AstTypePack* operator()(const TypeFamilyInstanceTypePack& tfitp) const
{
return allocator->alloc<AstTypePackGeneric>(Location(), AstName(tfitp.family->name.c_str()));
}
private:
Allocator* allocator;
SyntheticNames* syntheticNames;

View file

@ -16,6 +16,7 @@
#include "Luau/TypeReduction.h"
#include "Luau/TypeUtils.h"
#include "Luau/Unifier.h"
#include "Luau/TypeFamily.h"
#include <algorithm>
@ -113,6 +114,13 @@ struct TypeChecker2
return std::nullopt;
}
TypeId checkForFamilyInhabitance(TypeId instance, Location location)
{
TxnLog fake{};
reportErrors(reduceFamilies(instance, location, NotNull{&testArena}, builtinTypes, &fake, true).errors);
return instance;
}
TypePackId lookupPack(AstExpr* expr)
{
// If a type isn't in the type graph, it probably means that a recursion limit was exceeded.
@ -132,11 +140,11 @@ struct TypeChecker2
// allows us not to think about this very much in the actual typechecking logic.
TypeId* ty = module->astTypes.find(expr);
if (ty)
return follow(*ty);
return checkForFamilyInhabitance(follow(*ty), expr->location);
TypePackId* tp = module->astTypePacks.find(expr);
if (tp)
return flattenPack(*tp);
return checkForFamilyInhabitance(flattenPack(*tp), expr->location);
return builtinTypes->anyType;
}
@ -159,7 +167,7 @@ struct TypeChecker2
TypeId* ty = module->astResolvedTypes.find(annotation);
LUAU_ASSERT(ty);
return follow(*ty);
return checkForFamilyInhabitance(follow(*ty), annotation->location);
}
TypePackId lookupPackAnnotation(AstTypePack* annotation)
@ -311,6 +319,7 @@ struct TypeChecker2
TypePackId actualRetType = reconstructPack(ret->list, *arena);
Unifier u{NotNull{&normalizer}, Mode::Strict, stack.back(), ret->location, Covariant};
u.hideousFixMeGenericsAreActuallyFree = true;
u.tryUnify(actualRetType, expectedRetType);
const bool ok = u.errors.empty() && u.log.empty();
@ -989,8 +998,11 @@ struct TypeChecker2
return;
}
TxnLog fake{};
LUAU_ASSERT(ftv);
reportErrors(tryUnify(stack.back(), call->location, ftv->retTypes, expectedRetType, CountMismatch::Context::Return));
reportErrors(tryUnify(stack.back(), call->location, ftv->retTypes, expectedRetType, CountMismatch::Context::Return, /* genericsOkay */ true));
reportErrors(reduceFamilies(ftv->retTypes, call->location, NotNull{&testArena}, builtinTypes, &fake, true).errors);
auto it = begin(expectedArgTypes);
size_t i = 0;
@ -1007,7 +1019,8 @@ struct TypeChecker2
Location argLoc = argLocs.at(i >= argLocs.size() ? argLocs.size() - 1 : i);
reportErrors(tryUnify(stack.back(), argLoc, expectedArg, arg));
reportErrors(tryUnify(stack.back(), argLoc, expectedArg, arg, CountMismatch::Context::Arg, /* genericsOkay */ true));
reportErrors(reduceFamilies(arg, argLoc, NotNull{&testArena}, builtinTypes, &fake, true).errors);
++it;
++i;
@ -1018,7 +1031,8 @@ struct TypeChecker2
if (auto tail = it.tail())
{
TypePackId remainingArgs = testArena.addTypePack(TypePack{std::move(slice), std::nullopt});
reportErrors(tryUnify(stack.back(), argLocs.back(), *tail, remainingArgs));
reportErrors(tryUnify(stack.back(), argLocs.back(), *tail, remainingArgs, CountMismatch::Context::Arg, /* genericsOkay */ true));
reportErrors(reduceFamilies(remainingArgs, argLocs.back(), NotNull{&testArena}, builtinTypes, &fake, true).errors);
}
}
@ -1344,7 +1358,7 @@ struct TypeChecker2
else if (get<AnyType>(rightType) || get<ErrorType>(rightType))
return rightType;
if ((get<BlockedType>(leftType) || get<FreeType>(leftType)) && !isEquality && !isLogical)
if ((get<BlockedType>(leftType) || get<FreeType>(leftType) || get<GenericType>(leftType)) && !isEquality && !isLogical)
{
auto name = getIdentifierOfBaseVar(expr->left);
reportError(CannotInferBinaryOperation{expr->op, name,
@ -1591,10 +1605,10 @@ struct TypeChecker2
TypeId computedType = lookupType(expr->expr);
// Note: As an optimization, we try 'number <: number | string' first, as that is the more likely case.
if (isSubtype(annotationType, computedType, stack.back()))
if (isSubtype(annotationType, computedType, stack.back(), true))
return;
if (isSubtype(computedType, annotationType, stack.back()))
if (isSubtype(computedType, annotationType, stack.back(), true))
return;
reportError(TypesAreUnrelated{computedType, annotationType}, expr->location);
@ -1679,6 +1693,10 @@ struct TypeChecker2
void visit(AstType* ty)
{
TypeId* resolvedTy = module->astResolvedTypes.find(ty);
if (resolvedTy)
checkForFamilyInhabitance(follow(*resolvedTy), ty->location);
if (auto t = ty->as<AstTypeReference>())
return visit(t);
else if (auto t = ty->as<AstTypeTable>())
@ -1989,11 +2007,12 @@ struct TypeChecker2
}
template<typename TID>
bool isSubtype(TID subTy, TID superTy, NotNull<Scope> scope)
bool isSubtype(TID subTy, TID superTy, NotNull<Scope> scope, bool genericsOkay = false)
{
TypeArena arena;
Unifier u{NotNull{&normalizer}, Mode::Strict, scope, Location{}, Covariant};
u.useScopes = true;
u.hideousFixMeGenericsAreActuallyFree = genericsOkay;
u.enableScopeTests();
u.tryUnify(subTy, superTy);
const bool ok = u.errors.empty() && u.log.empty();
@ -2001,11 +2020,13 @@ struct TypeChecker2
}
template<typename TID>
ErrorVec tryUnify(NotNull<Scope> scope, const Location& location, TID subTy, TID superTy, CountMismatch::Context context = CountMismatch::Arg)
ErrorVec tryUnify(NotNull<Scope> scope, const Location& location, TID subTy, TID superTy, CountMismatch::Context context = CountMismatch::Arg,
bool genericsOkay = false)
{
Unifier u{NotNull{&normalizer}, Mode::Strict, scope, location, Covariant};
u.ctx = context;
u.useScopes = true;
u.hideousFixMeGenericsAreActuallyFree = genericsOkay;
u.enableScopeTests();
u.tryUnify(subTy, superTy);
return std::move(u.errors);

310
Analysis/src/TypeFamily.cpp Normal file
View file

@ -0,0 +1,310 @@
// This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details
#include "Luau/TypeFamily.h"
#include "Luau/DenseHash.h"
#include "Luau/VisitType.h"
#include "Luau/TxnLog.h"
#include "Luau/Substitution.h"
#include "Luau/ToString.h"
LUAU_DYNAMIC_FASTINTVARIABLE(LuauTypeFamilyGraphReductionMaximumSteps, 1'000'000);
namespace Luau
{
struct InstanceCollector : TypeOnceVisitor
{
std::deque<TypeId> tys;
std::deque<TypePackId> tps;
bool visit(TypeId ty, const TypeFamilyInstanceType&) override
{
// TypeOnceVisitor performs a depth-first traversal in the absence of
// cycles. This means that by pushing to the front of the queue, we will
// try to reduce deeper instances first if we start with the first thing
// in the queue. Consider Add<Add<Add<number, number>, number>, number>:
// we want to reduce the innermost Add<number, number> instantiation
// first.
tys.push_front(ty);
return true;
}
bool visit(TypePackId tp, const TypeFamilyInstanceTypePack&) override
{
// TypeOnceVisitor performs a depth-first traversal in the absence of
// cycles. This means that by pushing to the front of the queue, we will
// try to reduce deeper instances first if we start with the first thing
// in the queue. Consider Add<Add<Add<number, number>, number>, number>:
// we want to reduce the innermost Add<number, number> instantiation
// first.
tps.push_front(tp);
return true;
}
};
struct FamilyReducer
{
std::deque<TypeId> queuedTys;
std::deque<TypePackId> queuedTps;
DenseHashSet<const void*> irreducible{nullptr};
FamilyGraphReductionResult result;
Location location;
NotNull<TypeArena> arena;
NotNull<BuiltinTypes> builtins;
TxnLog* log = nullptr;
NotNull<const TxnLog> reducerLog;
bool force = false;
FamilyReducer(std::deque<TypeId> queuedTys, std::deque<TypePackId> queuedTps, Location location, NotNull<TypeArena> arena,
NotNull<BuiltinTypes> builtins, TxnLog* log = nullptr, bool force = false)
: queuedTys(std::move(queuedTys))
, queuedTps(std::move(queuedTps))
, location(location)
, arena(arena)
, builtins(builtins)
, log(log)
, reducerLog(NotNull{log ? log : TxnLog::empty()})
, force(force)
{
}
enum class SkipTestResult
{
Irreducible,
Defer,
Okay,
};
SkipTestResult testForSkippability(TypeId ty)
{
ty = reducerLog->follow(ty);
if (reducerLog->is<TypeFamilyInstanceType>(ty))
{
if (!irreducible.contains(ty))
return SkipTestResult::Defer;
else
return SkipTestResult::Irreducible;
}
else if (reducerLog->is<GenericType>(ty))
{
return SkipTestResult::Irreducible;
}
return SkipTestResult::Okay;
}
SkipTestResult testForSkippability(TypePackId ty)
{
ty = reducerLog->follow(ty);
if (reducerLog->is<TypeFamilyInstanceTypePack>(ty))
{
if (!irreducible.contains(ty))
return SkipTestResult::Defer;
else
return SkipTestResult::Irreducible;
}
else if (reducerLog->is<GenericTypePack>(ty))
{
return SkipTestResult::Irreducible;
}
return SkipTestResult::Okay;
}
template<typename T>
void replace(T subject, T replacement)
{
if (log)
log->replace(subject, Unifiable::Bound{replacement});
else
asMutable(subject)->ty.template emplace<Unifiable::Bound<T>>(replacement);
if constexpr (std::is_same_v<T, TypeId>)
result.reducedTypes.insert(subject);
else if constexpr (std::is_same_v<T, TypePackId>)
result.reducedPacks.insert(subject);
}
template<typename T>
void handleFamilyReduction(T subject, TypeFamilyReductionResult<T> reduction)
{
if (reduction.result)
replace(subject, *reduction.result);
else
{
irreducible.insert(subject);
if (reduction.uninhabited || force)
{
if constexpr (std::is_same_v<T, TypeId>)
result.errors.push_back(TypeError{location, UninhabitedTypeFamily{subject}});
else if constexpr (std::is_same_v<T, TypePackId>)
result.errors.push_back(TypeError{location, UninhabitedTypePackFamily{subject}});
}
else if (!reduction.uninhabited && !force)
{
for (TypeId b : reduction.blockedTypes)
result.blockedTypes.insert(b);
for (TypePackId b : reduction.blockedPacks)
result.blockedPacks.insert(b);
}
}
}
bool done()
{
return queuedTys.empty() && queuedTps.empty();
}
template<typename T, typename I>
bool testParameters(T subject, const I* tfit)
{
for (TypeId p : tfit->typeArguments)
{
SkipTestResult skip = testForSkippability(p);
if (skip == SkipTestResult::Irreducible)
{
irreducible.insert(subject);
return false;
}
else if (skip == SkipTestResult::Defer)
{
if constexpr (std::is_same_v<T, TypeId>)
queuedTys.push_back(subject);
else if constexpr (std::is_same_v<T, TypePackId>)
queuedTps.push_back(subject);
return false;
}
}
for (TypePackId p : tfit->packArguments)
{
SkipTestResult skip = testForSkippability(p);
if (skip == SkipTestResult::Irreducible)
{
irreducible.insert(subject);
return false;
}
else if (skip == SkipTestResult::Defer)
{
if constexpr (std::is_same_v<T, TypeId>)
queuedTys.push_back(subject);
else if constexpr (std::is_same_v<T, TypePackId>)
queuedTps.push_back(subject);
return false;
}
}
return true;
}
void stepType()
{
TypeId subject = reducerLog->follow(queuedTys.front());
queuedTys.pop_front();
if (irreducible.contains(subject))
return;
if (const TypeFamilyInstanceType* tfit = reducerLog->get<TypeFamilyInstanceType>(subject))
{
if (!testParameters(subject, tfit))
return;
TypeFamilyReductionResult<TypeId> result = tfit->family->reducer(tfit->typeArguments, tfit->packArguments, arena, builtins, reducerLog);
handleFamilyReduction(subject, result);
}
}
void stepPack()
{
TypePackId subject = reducerLog->follow(queuedTps.front());
queuedTps.pop_front();
if (irreducible.contains(subject))
return;
if (const TypeFamilyInstanceTypePack* tfit = reducerLog->get<TypeFamilyInstanceTypePack>(subject))
{
if (!testParameters(subject, tfit))
return;
TypeFamilyReductionResult<TypePackId> result =
tfit->family->reducer(tfit->typeArguments, tfit->packArguments, arena, builtins, reducerLog);
handleFamilyReduction(subject, result);
}
}
void step()
{
if (!queuedTys.empty())
stepType();
else if (!queuedTps.empty())
stepPack();
}
};
static FamilyGraphReductionResult reduceFamiliesInternal(std::deque<TypeId> queuedTys, std::deque<TypePackId> queuedTps, Location location,
NotNull<TypeArena> arena, NotNull<BuiltinTypes> builtins, TxnLog* log, bool force)
{
FamilyReducer reducer{std::move(queuedTys), std::move(queuedTps), location, arena, builtins, log, force};
int iterationCount = 0;
while (!reducer.done())
{
reducer.step();
++iterationCount;
if (iterationCount > DFInt::LuauTypeFamilyGraphReductionMaximumSteps)
{
reducer.result.errors.push_back(TypeError{location, CodeTooComplex{}});
break;
}
}
return std::move(reducer.result);
}
FamilyGraphReductionResult reduceFamilies(
TypeId entrypoint, Location location, NotNull<TypeArena> arena, NotNull<BuiltinTypes> builtins, TxnLog* log, bool force)
{
InstanceCollector collector;
try
{
collector.traverse(entrypoint);
}
catch (RecursionLimitException&)
{
return FamilyGraphReductionResult{};
}
return reduceFamiliesInternal(std::move(collector.tys), std::move(collector.tps), location, arena, builtins, log, force);
}
FamilyGraphReductionResult reduceFamilies(
TypePackId entrypoint, Location location, NotNull<TypeArena> arena, NotNull<BuiltinTypes> builtins, TxnLog* log, bool force)
{
InstanceCollector collector;
try
{
collector.traverse(entrypoint);
}
catch (RecursionLimitException&)
{
return FamilyGraphReductionResult{};
}
return reduceFamiliesInternal(std::move(collector.tys), std::move(collector.tps), location, arena, builtins, log, force);
}
} // namespace Luau

View file

@ -41,6 +41,7 @@ LUAU_FASTFLAG(LuauOccursIsntAlwaysFailure)
LUAU_FASTFLAGVARIABLE(LuauTypecheckTypeguards, false)
LUAU_FASTFLAGVARIABLE(LuauTinyControlFlowAnalysis, false)
LUAU_FASTFLAG(LuauRequirePathTrueModuleName)
LUAU_FASTFLAGVARIABLE(LuauTypecheckClassTypeIndexers, false)
namespace Luau
{
@ -2104,6 +2105,23 @@ std::optional<TypeId> TypeChecker::getIndexTypeFromTypeImpl(
const Property* prop = lookupClassProp(cls, name);
if (prop)
return prop->type();
if (FFlag::LuauTypecheckClassTypeIndexers)
{
if (auto indexer = cls->indexer)
{
// TODO: Property lookup should work with string singletons or unions thereof as the indexer key type.
ErrorVec errors = tryUnify(stringType, indexer->indexType, scope, location);
if (errors.empty())
return indexer->indexResultType;
if (addErrors)
reportError(location, UnknownProperty{type, name});
return std::nullopt;
}
}
}
else if (const UnionType* utv = get<UnionType>(type))
{
@ -3295,14 +3313,38 @@ TypeId TypeChecker::checkLValueBinding(const ScopePtr& scope, const AstExprIndex
}
else if (const ClassType* lhsClass = get<ClassType>(lhs))
{
const Property* prop = lookupClassProp(lhsClass, name);
if (!prop)
if (FFlag::LuauTypecheckClassTypeIndexers)
{
if (const Property* prop = lookupClassProp(lhsClass, name))
{
return prop->type();
}
if (auto indexer = lhsClass->indexer)
{
Unifier state = mkUnifier(scope, expr.location);
state.tryUnify(stringType, indexer->indexType);
if (state.errors.empty())
{
state.log.commit();
return indexer->indexResultType;
}
}
reportError(TypeError{expr.location, UnknownProperty{lhs, name}});
return errorRecoveryType(scope);
}
else
{
const Property* prop = lookupClassProp(lhsClass, name);
if (!prop)
{
reportError(TypeError{expr.location, UnknownProperty{lhs, name}});
return errorRecoveryType(scope);
}
return prop->type();
return prop->type();
}
}
else if (get<IntersectionType>(lhs))
{
@ -3344,23 +3386,57 @@ TypeId TypeChecker::checkLValueBinding(const ScopePtr& scope, const AstExprIndex
{
if (const ClassType* exprClass = get<ClassType>(exprType))
{
const Property* prop = lookupClassProp(exprClass, value->value.data);
if (!prop)
if (FFlag::LuauTypecheckClassTypeIndexers)
{
if (const Property* prop = lookupClassProp(exprClass, value->value.data))
{
return prop->type();
}
if (auto indexer = exprClass->indexer)
{
unify(stringType, indexer->indexType, scope, expr.index->location);
return indexer->indexResultType;
}
reportError(TypeError{expr.location, UnknownProperty{exprType, value->value.data}});
return errorRecoveryType(scope);
}
return prop->type();
else
{
const Property* prop = lookupClassProp(exprClass, value->value.data);
if (!prop)
{
reportError(TypeError{expr.location, UnknownProperty{exprType, value->value.data}});
return errorRecoveryType(scope);
}
return prop->type();
}
}
}
else if (FFlag::LuauAllowIndexClassParameters)
else
{
if (const ClassType* exprClass = get<ClassType>(exprType))
if (FFlag::LuauTypecheckClassTypeIndexers)
{
if (isNonstrictMode())
return unknownType;
reportError(TypeError{expr.location, DynamicPropertyLookupOnClassesUnsafe{exprType}});
return errorRecoveryType(scope);
if (const ClassType* exprClass = get<ClassType>(exprType))
{
if (auto indexer = exprClass->indexer)
{
unify(indexType, indexer->indexType, scope, expr.index->location);
return indexer->indexResultType;
}
}
}
if (FFlag::LuauAllowIndexClassParameters)
{
if (const ClassType* exprClass = get<ClassType>(exprType))
{
if (isNonstrictMode())
return unknownType;
reportError(TypeError{expr.location, DynamicPropertyLookupOnClassesUnsafe{exprType}});
return errorRecoveryType(scope);
}
}
}

View file

@ -12,6 +12,7 @@
#include "Luau/TypeUtils.h"
#include "Luau/Type.h"
#include "Luau/VisitType.h"
#include "Luau/TypeFamily.h"
#include <algorithm>
@ -20,6 +21,7 @@ LUAU_FASTFLAG(LuauErrorRecoveryType)
LUAU_FASTFLAGVARIABLE(LuauInstantiateInSubtyping, false)
LUAU_FASTFLAGVARIABLE(LuauUninhabitedSubAnything2, false)
LUAU_FASTFLAGVARIABLE(LuauVariadicAnyCanBeGeneric, false)
LUAU_FASTFLAGVARIABLE(LuauUnifyTwoOptions, false)
LUAU_FASTFLAGVARIABLE(LuauMaintainScopesInUnifier, false)
LUAU_FASTFLAGVARIABLE(LuauTransitiveSubtyping, false)
LUAU_FASTFLAGVARIABLE(LuauOccursIsntAlwaysFailure, false)
@ -439,6 +441,30 @@ void Unifier::tryUnify_(TypeId subTy, TypeId superTy, bool isFunctionCall, bool
if (superTy == subTy)
return;
if (log.get<TypeFamilyInstanceType>(superTy))
{
// We do not report errors from reducing here. This is because we will
// "double-report" errors in some cases, like when trying to unify
// identical type family instantiations like Add<false, false> with
// Add<false, false>.
reduceFamilies(superTy, location, NotNull(types), builtinTypes, &log);
superTy = log.follow(superTy);
}
if (log.get<TypeFamilyInstanceType>(subTy))
{
reduceFamilies(subTy, location, NotNull(types), builtinTypes, &log);
subTy = log.follow(subTy);
}
// If we can't reduce the families down and we still have type family types
// here, we are stuck. Nothing meaningful can be done here. We don't wish to
// report an error, either.
if (log.get<TypeFamilyInstanceType>(superTy) || log.get<TypeFamilyInstanceType>(subTy))
{
return;
}
auto superFree = log.getMutable<FreeType>(superTy);
auto subFree = log.getMutable<FreeType>(subTy);
@ -509,6 +535,49 @@ void Unifier::tryUnify_(TypeId subTy, TypeId superTy, bool isFunctionCall, bool
return;
}
if (hideousFixMeGenericsAreActuallyFree)
{
auto superGeneric = log.getMutable<GenericType>(superTy);
auto subGeneric = log.getMutable<GenericType>(subTy);
if (superGeneric && subGeneric && subsumes(useScopes, superGeneric, subGeneric))
{
if (!occursCheck(subTy, superTy, /* reversed = */ false))
log.replace(subTy, BoundType(superTy));
return;
}
else if (superGeneric && subGeneric)
{
if (!occursCheck(superTy, subTy, /* reversed = */ true))
log.replace(superTy, BoundType(subTy));
return;
}
else if (superGeneric)
{
if (!occursCheck(superTy, subTy, /* reversed = */ true))
{
Widen widen{types, builtinTypes};
log.replace(superTy, BoundType(widen(subTy)));
}
return;
}
else if (subGeneric)
{
// Normally, if the subtype is free, it should not be bound to any, unknown, or error types.
// But for bug compatibility, we'll only apply this rule to unknown. Doing this will silence cascading type errors.
if (log.get<UnknownType>(superTy))
return;
if (!occursCheck(subTy, superTy, /* reversed = */ false))
log.replace(subTy, BoundType(superTy));
return;
}
}
if (log.get<AnyType>(superTy))
return tryUnifyWithAny(subTy, builtinTypes->anyType);
@ -687,8 +756,93 @@ void Unifier::tryUnify_(TypeId subTy, TypeId superTy, bool isFunctionCall, bool
log.popSeen(superTy, subTy);
}
/*
* If the passed type is an option, strip nil out.
*
* There is an important subtlety to be observed here:
*
* We want to do a peephole fix to unify the subtype relation A? <: B? where we
* instead peel off the options and relate A <: B instead, but only works if we
* are certain that neither A nor B are themselves optional.
*
* For instance, if we want to test that (boolean?)? <: boolean?, we must peel
* off both layers of optionality from the subTy.
*
* We must also handle unions that have more than two choices.
*
* eg (string | nil)? <: boolean?
*/
static std::optional<TypeId> unwrapOption(NotNull<BuiltinTypes> builtinTypes, NotNull<TypeArena> arena, const TxnLog& log, TypeId ty, DenseHashSet<TypeId>& seen)
{
if (seen.find(ty))
return std::nullopt;
seen.insert(ty);
const UnionType* ut = get<UnionType>(follow(ty));
if (!ut)
return std::nullopt;
if (2 == ut->options.size())
{
if (isNil(follow(ut->options[0])))
{
std::optional<TypeId> doubleUnwrapped = unwrapOption(builtinTypes, arena, log, ut->options[1], seen);
return doubleUnwrapped.value_or(ut->options[1]);
}
if (isNil(follow(ut->options[1])))
{
std::optional<TypeId> doubleUnwrapped = unwrapOption(builtinTypes, arena, log, ut->options[0], seen);
return doubleUnwrapped.value_or(ut->options[0]);
}
}
std::set<TypeId> newOptions;
bool found = false;
for (TypeId t : ut)
{
t = log.follow(t);
if (isNil(t))
{
found = true;
continue;
}
else
newOptions.insert(t);
}
if (!found)
return std::nullopt;
else if (newOptions.empty())
return builtinTypes->neverType;
else if (1 == newOptions.size())
return *begin(newOptions);
else
return arena->addType(UnionType{std::vector<TypeId>(begin(newOptions), end(newOptions))});
}
static std::optional<TypeId> unwrapOption(NotNull<BuiltinTypes> builtinTypes, NotNull<TypeArena> arena, const TxnLog& log, TypeId ty)
{
DenseHashSet<TypeId> seen{nullptr};
return unwrapOption(builtinTypes, arena, log, ty, seen);
}
void Unifier::tryUnifyUnionWithType(TypeId subTy, const UnionType* subUnion, TypeId superTy)
{
// Peephole fix: A? <: B? if A <: B
//
// This works around issues that can arise if A or B is free. We do not
// want either of those types to be bound to nil.
if (FFlag::LuauUnifyTwoOptions)
{
if (auto subOption = unwrapOption(builtinTypes, NotNull{types}, log, subTy))
{
if (auto superOption = unwrapOption(builtinTypes, NotNull{types}, log, superTy))
return tryUnify_(*subOption, *superOption);
}
}
// A | B <: T if and only if A <: T and B <: T
bool failed = false;
bool errorsSuppressed = true;
@ -1205,6 +1359,25 @@ void Unifier::tryUnifyNormalizedTypes(
const ClassType* superCtv = get<ClassType>(superClass);
LUAU_ASSERT(superCtv);
if (FFlag::LuauUnifyTwoOptions)
{
if (variance == Invariant)
{
if (subCtv == superCtv)
{
found = true;
/*
* The only way we could care about superNegations is if
* one of them was equal to superCtv. However,
* normalization ensures that this is impossible.
*/
}
else
continue;
}
}
if (isSubclass(subCtv, superCtv))
{
found = true;
@ -1518,6 +1691,12 @@ struct WeirdIter
}
};
void Unifier::enableScopeTests()
{
useScopes = true;
log.useScopes = true;
}
ErrorVec Unifier::canUnify(TypeId subTy, TypeId superTy)
{
Unifier s = makeChildUnifier();
@ -1597,6 +1776,21 @@ void Unifier::tryUnify_(TypePackId subTp, TypePackId superTp, bool isFunctionCal
log.replace(subTp, Unifiable::Bound<TypePackId>(superTp));
}
}
else if (hideousFixMeGenericsAreActuallyFree && log.getMutable<GenericTypePack>(superTp))
{
if (!occursCheck(superTp, subTp, /* reversed = */ true))
{
Widen widen{types, builtinTypes};
log.replace(superTp, Unifiable::Bound<TypePackId>(widen(subTp)));
}
}
else if (hideousFixMeGenericsAreActuallyFree && log.getMutable<GenericTypePack>(subTp))
{
if (!occursCheck(subTp, superTp, /* reversed = */ false))
{
log.replace(subTp, Unifiable::Bound<TypePackId>(superTp));
}
}
else if (log.getMutable<Unifiable::Error>(superTp))
tryUnifyWithAny(subTp, superTp);
else if (log.getMutable<Unifiable::Error>(subTp))
@ -2611,7 +2805,10 @@ void Unifier::tryUnifyVariadics(TypePackId subTp, TypePackId superTp, bool rever
}
else if (get<GenericTypePack>(tail))
{
reportError(location, GenericError{"Cannot unify variadic and generic packs"});
if (!hideousFixMeGenericsAreActuallyFree)
reportError(location, GenericError{"Cannot unify variadic and generic packs"});
else
log.replace(tail, BoundTypePack{superTp});
}
else if (get<Unifiable::Error>(tail))
{
@ -2732,7 +2929,7 @@ std::optional<TypeId> Unifier::findTablePropertyRespectingMeta(TypeId lhsType, N
TxnLog Unifier::combineLogsIntoIntersection(std::vector<TxnLog> logs)
{
LUAU_ASSERT(FFlag::DebugLuauDeferredConstraintResolution);
TxnLog result;
TxnLog result(useScopes);
for (TxnLog& log : logs)
result.concatAsIntersections(std::move(log), NotNull{types});
return result;
@ -2741,7 +2938,7 @@ TxnLog Unifier::combineLogsIntoIntersection(std::vector<TxnLog> logs)
TxnLog Unifier::combineLogsIntoUnion(std::vector<TxnLog> logs)
{
LUAU_ASSERT(FFlag::DebugLuauDeferredConstraintResolution);
TxnLog result;
TxnLog result(useScopes);
for (TxnLog& log : logs)
result.concatAsUnion(std::move(log), NotNull{types});
return result;
@ -2807,7 +3004,7 @@ bool Unifier::occursCheck(DenseHashSet<TypeId>& seen, TypeId needle, TypeId hays
if (log.getMutable<ErrorType>(needle))
return false;
if (!log.getMutable<FreeType>(needle))
if (!log.getMutable<FreeType>(needle) && !(hideousFixMeGenericsAreActuallyFree && log.is<GenericType>(needle)))
ice("Expected needle to be free");
if (needle == haystack)
@ -2821,7 +3018,7 @@ bool Unifier::occursCheck(DenseHashSet<TypeId>& seen, TypeId needle, TypeId hays
return true;
}
if (log.getMutable<FreeType>(haystack))
if (log.getMutable<FreeType>(haystack) || (hideousFixMeGenericsAreActuallyFree && log.is<GenericType>(haystack)))
return false;
else if (auto a = log.getMutable<UnionType>(haystack))
{
@ -2865,7 +3062,7 @@ bool Unifier::occursCheck(DenseHashSet<TypePackId>& seen, TypePackId needle, Typ
if (log.getMutable<ErrorTypePack>(needle))
return false;
if (!log.getMutable<FreeTypePack>(needle))
if (!log.getMutable<FreeTypePack>(needle) && !(hideousFixMeGenericsAreActuallyFree && log.is<GenericTypePack>(needle)))
ice("Expected needle pack to be free");
RecursionLimiter _ra(&sharedState.counters.recursionCount, sharedState.counters.recursionLimit);
@ -2900,7 +3097,10 @@ Unifier Unifier::makeChildUnifier()
Unifier u = Unifier{normalizer, mode, scope, location, variance, &log};
u.normalize = normalize;
u.checkInhabited = checkInhabited;
u.useScopes = useScopes;
if (useScopes)
u.enableScopeTests();
return u;
}

View file

@ -27,6 +27,10 @@
#include <windows.h>
#endif
#ifdef __linux__
#include <unistd.h>
#endif
#ifdef CALLGRIND
#include <valgrind/callgrind.h>
#endif
@ -865,6 +869,7 @@ int replMain(int argc, char** argv)
int profile = 0;
bool coverage = false;
bool interactive = false;
bool codegenPerf = false;
// Set the mode if the user has explicitly specified one.
int argStart = 1;
@ -962,6 +967,11 @@ int replMain(int argc, char** argv)
{
codegen = true;
}
else if (strcmp(argv[i], "--codegen-perf") == 0)
{
codegen = true;
codegenPerf = true;
}
else if (strcmp(argv[i], "--coverage") == 0)
{
coverage = true;
@ -998,6 +1008,24 @@ int replMain(int argc, char** argv)
}
#endif
if (codegenPerf)
{
#if __linux__
char path[128];
snprintf(path, sizeof(path), "/tmp/perf-%d.map", getpid());
// note, there's no need to close the log explicitly as it will be closed when the process exits
FILE* codegenPerfLog = fopen(path, "w");
Luau::CodeGen::setPerfLog(codegenPerfLog, [](void* context, uintptr_t addr, unsigned size, const char* symbol) {
fprintf(static_cast<FILE*>(context), "%016lx %08x %s\n", long(addr), size, symbol);
});
#else
fprintf(stderr, "--codegen-perf option is only supported on Linux\n");
return 1;
#endif
}
const std::vector<std::string> files = getSourceFiles(argc, argv);
if (mode == CliMode::Unknown)
{

View file

@ -56,7 +56,7 @@ public:
void eor(RegisterA64 dst, RegisterA64 src1, RegisterA64 src2, int shift = 0);
void bic(RegisterA64 dst, RegisterA64 src1, RegisterA64 src2, int shift = 0);
void tst(RegisterA64 src1, RegisterA64 src2, int shift = 0);
void mvn(RegisterA64 dst, RegisterA64 src);
void mvn_(RegisterA64 dst, RegisterA64 src);
// Bitwise with immediate
// Note: immediate must have a single contiguous sequence of 1 bits set of length 1..31
@ -199,7 +199,7 @@ private:
void placeR1(const char* name, RegisterA64 dst, RegisterA64 src, uint32_t op);
void placeI12(const char* name, RegisterA64 dst, RegisterA64 src1, int src2, uint8_t op);
void placeI16(const char* name, RegisterA64 dst, int src, uint8_t op, int shift = 0);
void placeA(const char* name, RegisterA64 dst, AddressA64 src, uint8_t op, uint8_t size, int sizelog);
void placeA(const char* name, RegisterA64 dst, AddressA64 src, uint16_t opsize, int sizelog);
void placeB(const char* name, Label& label, uint8_t op);
void placeBC(const char* name, Label& label, uint8_t op, uint8_t cond);
void placeBCR(const char* name, Label& label, uint8_t op, RegisterA64 cond);

View file

@ -2,6 +2,7 @@
#pragma once
#include "Luau/Common.h"
#include "Luau/DenseHash.h"
#include "Luau/Label.h"
#include "Luau/ConditionX64.h"
#include "Luau/OperandX64.h"
@ -250,6 +251,8 @@ private:
std::vector<Label> pendingLabels;
std::vector<uint32_t> labelLocations;
DenseHashMap<uint64_t, int32_t> constCache64;
bool finalized = false;
size_t dataPos = 0;

View file

@ -13,5 +13,7 @@ namespace CodeGen
void* createBlockUnwindInfo(void* context, uint8_t* block, size_t blockSize, size_t& startOffset);
void destroyBlockUnwindInfo(void* context, void* unwindData);
bool isUnwindSupported();
} // namespace CodeGen
} // namespace Luau

View file

@ -3,6 +3,8 @@
#include <string>
#include <stdint.h>
struct lua_State;
namespace Luau
@ -17,7 +19,7 @@ void create(lua_State* L);
// Builds target function and all inner functions
void compile(lua_State* L, int idx);
using annotatorFn = void (*)(void* context, std::string& result, int fid, int instpos);
using AnnotatorFn = void (*)(void* context, std::string& result, int fid, int instpos);
struct AssemblyOptions
{
@ -28,12 +30,16 @@ struct AssemblyOptions
bool includeOutlinedCode = false;
// Optional annotator function can be provided to describe each instruction, it takes function id and sequential instruction id
annotatorFn annotator = nullptr;
AnnotatorFn annotator = nullptr;
void* annotatorContext = nullptr;
};
// Generates assembly for target function and all inner functions
std::string getAssembly(lua_State* L, int idx, AssemblyOptions options = {});
using PerfLogFn = void (*)(void* context, uintptr_t addr, unsigned size, const char* symbol);
void setPerfLog(void* context, PerfLogFn logFn);
} // namespace CodeGen
} // namespace Luau

View file

@ -184,7 +184,7 @@ void AssemblyBuilderA64::tst(RegisterA64 src1, RegisterA64 src2, int shift)
placeSR3("tst", dst, src1, src2, 0b11'01010, shift);
}
void AssemblyBuilderA64::mvn(RegisterA64 dst, RegisterA64 src)
void AssemblyBuilderA64::mvn_(RegisterA64 dst, RegisterA64 src)
{
placeSR2("mvn", dst, src, 0b01'01010, 0b1);
}
@ -287,19 +287,19 @@ void AssemblyBuilderA64::ldr(RegisterA64 dst, AddressA64 src)
switch (dst.kind)
{
case KindA64::w:
placeA("ldr", dst, src, 0b11100001, 0b10, /* sizelog= */ 2);
placeA("ldr", dst, src, 0b10'11100001, /* sizelog= */ 2);
break;
case KindA64::x:
placeA("ldr", dst, src, 0b11100001, 0b11, /* sizelog= */ 3);
placeA("ldr", dst, src, 0b11'11100001, /* sizelog= */ 3);
break;
case KindA64::s:
placeA("ldr", dst, src, 0b11110001, 0b10, /* sizelog= */ 2);
placeA("ldr", dst, src, 0b10'11110001, /* sizelog= */ 2);
break;
case KindA64::d:
placeA("ldr", dst, src, 0b11110001, 0b11, /* sizelog= */ 3);
placeA("ldr", dst, src, 0b11'11110001, /* sizelog= */ 3);
break;
case KindA64::q:
placeA("ldr", dst, src, 0b11110011, 0b00, /* sizelog= */ 4);
placeA("ldr", dst, src, 0b00'11110011, /* sizelog= */ 4);
break;
case KindA64::none:
LUAU_ASSERT(!"Unexpected register kind");
@ -310,35 +310,35 @@ void AssemblyBuilderA64::ldrb(RegisterA64 dst, AddressA64 src)
{
LUAU_ASSERT(dst.kind == KindA64::w);
placeA("ldrb", dst, src, 0b11100001, 0b00, /* sizelog= */ 0);
placeA("ldrb", dst, src, 0b00'11100001, /* sizelog= */ 0);
}
void AssemblyBuilderA64::ldrh(RegisterA64 dst, AddressA64 src)
{
LUAU_ASSERT(dst.kind == KindA64::w);
placeA("ldrh", dst, src, 0b11100001, 0b01, /* sizelog= */ 1);
placeA("ldrh", dst, src, 0b01'11100001, /* sizelog= */ 1);
}
void AssemblyBuilderA64::ldrsb(RegisterA64 dst, AddressA64 src)
{
LUAU_ASSERT(dst.kind == KindA64::x || dst.kind == KindA64::w);
placeA("ldrsb", dst, src, 0b11100010 | uint8_t(dst.kind == KindA64::w), 0b00, /* sizelog= */ 0);
placeA("ldrsb", dst, src, 0b00'11100010 | uint8_t(dst.kind == KindA64::w), /* sizelog= */ 0);
}
void AssemblyBuilderA64::ldrsh(RegisterA64 dst, AddressA64 src)
{
LUAU_ASSERT(dst.kind == KindA64::x || dst.kind == KindA64::w);
placeA("ldrsh", dst, src, 0b11100010 | uint8_t(dst.kind == KindA64::w), 0b01, /* sizelog= */ 1);
placeA("ldrsh", dst, src, 0b01'11100010 | uint8_t(dst.kind == KindA64::w), /* sizelog= */ 1);
}
void AssemblyBuilderA64::ldrsw(RegisterA64 dst, AddressA64 src)
{
LUAU_ASSERT(dst.kind == KindA64::x);
placeA("ldrsw", dst, src, 0b11100010, 0b10, /* sizelog= */ 2);
placeA("ldrsw", dst, src, 0b10'11100010, /* sizelog= */ 2);
}
void AssemblyBuilderA64::ldp(RegisterA64 dst1, RegisterA64 dst2, AddressA64 src)
@ -356,19 +356,19 @@ void AssemblyBuilderA64::str(RegisterA64 src, AddressA64 dst)
switch (src.kind)
{
case KindA64::w:
placeA("str", src, dst, 0b11100000, 0b10, /* sizelog= */ 2);
placeA("str", src, dst, 0b10'11100000, /* sizelog= */ 2);
break;
case KindA64::x:
placeA("str", src, dst, 0b11100000, 0b11, /* sizelog= */ 3);
placeA("str", src, dst, 0b11'11100000, /* sizelog= */ 3);
break;
case KindA64::s:
placeA("str", src, dst, 0b11110000, 0b10, /* sizelog= */ 2);
placeA("str", src, dst, 0b10'11110000, /* sizelog= */ 2);
break;
case KindA64::d:
placeA("str", src, dst, 0b11110000, 0b11, /* sizelog= */ 3);
placeA("str", src, dst, 0b11'11110000, /* sizelog= */ 3);
break;
case KindA64::q:
placeA("str", src, dst, 0b11110010, 0b00, /* sizelog= */ 4);
placeA("str", src, dst, 0b00'11110010, /* sizelog= */ 4);
break;
case KindA64::none:
LUAU_ASSERT(!"Unexpected register kind");
@ -379,14 +379,14 @@ void AssemblyBuilderA64::strb(RegisterA64 src, AddressA64 dst)
{
LUAU_ASSERT(src.kind == KindA64::w);
placeA("strb", src, dst, 0b11100000, 0b00, /* sizelog= */ 0);
placeA("strb", src, dst, 0b00'11100000, /* sizelog= */ 0);
}
void AssemblyBuilderA64::strh(RegisterA64 src, AddressA64 dst)
{
LUAU_ASSERT(src.kind == KindA64::w);
placeA("strh", src, dst, 0b11100000, 0b01, /* sizelog= */ 1);
placeA("strh", src, dst, 0b01'11100000, /* sizelog= */ 1);
}
void AssemblyBuilderA64::stp(RegisterA64 src1, RegisterA64 src2, AddressA64 dst)
@ -487,9 +487,12 @@ void AssemblyBuilderA64::adr(RegisterA64 dst, Label& label)
void AssemblyBuilderA64::fmov(RegisterA64 dst, RegisterA64 src)
{
LUAU_ASSERT(dst.kind == KindA64::d && src.kind == KindA64::d);
LUAU_ASSERT(dst.kind == KindA64::d && (src.kind == KindA64::d || src.kind == KindA64::x));
placeR1("fmov", dst, src, 0b000'11110'01'1'0000'00'10000);
if (src.kind == KindA64::d)
placeR1("fmov", dst, src, 0b000'11110'01'1'0000'00'10000);
else
placeR1("fmov", dst, src, 0b000'11110'01'1'00'111'000000);
}
void AssemblyBuilderA64::fmov(RegisterA64 dst, double src)
@ -825,7 +828,7 @@ void AssemblyBuilderA64::placeI16(const char* name, RegisterA64 dst, int src, ui
commit();
}
void AssemblyBuilderA64::placeA(const char* name, RegisterA64 dst, AddressA64 src, uint8_t op, uint8_t size, int sizelog)
void AssemblyBuilderA64::placeA(const char* name, RegisterA64 dst, AddressA64 src, uint16_t opsize, int sizelog)
{
if (logText)
log(name, dst, src);
@ -833,15 +836,15 @@ void AssemblyBuilderA64::placeA(const char* name, RegisterA64 dst, AddressA64 sr
switch (src.kind)
{
case AddressKindA64::imm:
if (src.data >= 0 && (src.data >> sizelog) < 1024 && (src.data & ((1 << sizelog) - 1)) == 0)
place(dst.index | (src.base.index << 5) | ((src.data >> sizelog) << 10) | (op << 22) | (1 << 24) | (size << 30));
if (unsigned(src.data >> sizelog) < 1024 && (src.data & ((1 << sizelog) - 1)) == 0)
place(dst.index | (src.base.index << 5) | ((src.data >> sizelog) << 10) | (opsize << 22) | (1 << 24));
else if (src.data >= -256 && src.data <= 255)
place(dst.index | (src.base.index << 5) | ((src.data & ((1 << 9) - 1)) << 12) | (op << 22) | (size << 30));
place(dst.index | (src.base.index << 5) | ((src.data & ((1 << 9) - 1)) << 12) | (opsize << 22));
else
LUAU_ASSERT(!"Unable to encode large immediate offset");
break;
case AddressKindA64::reg:
place(dst.index | (src.base.index << 5) | (0b10 << 10) | (0b011 << 13) | (src.offset.index << 16) | (1 << 21) | (op << 22) | (size << 30));
place(dst.index | (src.base.index << 5) | (0b011'0'10 << 10) | (src.offset.index << 16) | (1 << 21) | (opsize << 22));
break;
}

View file

@ -75,6 +75,7 @@ static ABIX64 getCurrentX64ABI()
AssemblyBuilderX64::AssemblyBuilderX64(bool logText, ABIX64 abi)
: logText(logText)
, abi(abi)
, constCache64(~0ull)
{
data.resize(4096);
dataPos = data.size(); // data is filled backwards
@ -885,9 +886,22 @@ void AssemblyBuilderX64::setLabel(Label& label)
OperandX64 AssemblyBuilderX64::i64(int64_t value)
{
uint64_t as64BitKey = value;
if (as64BitKey != ~0ull)
{
if (int32_t* prev = constCache64.find(as64BitKey))
return OperandX64(SizeX64::qword, noreg, 1, rip, *prev);
}
size_t pos = allocateData(8, 8);
writeu64(&data[pos], value);
return OperandX64(SizeX64::qword, noreg, 1, rip, int32_t(pos - data.size()));
int32_t offset = int32_t(pos - data.size());
if (as64BitKey != ~0ull)
constCache64[as64BitKey] = offset;
return OperandX64(SizeX64::qword, noreg, 1, rip, offset);
}
OperandX64 AssemblyBuilderX64::f32(float value)
@ -899,9 +913,24 @@ OperandX64 AssemblyBuilderX64::f32(float value)
OperandX64 AssemblyBuilderX64::f64(double value)
{
uint64_t as64BitKey;
static_assert(sizeof(as64BitKey) == sizeof(value), "Expecting double to be 64-bit");
memcpy(&as64BitKey, &value, sizeof(value));
if (as64BitKey != ~0ull)
{
if (int32_t* prev = constCache64.find(as64BitKey))
return OperandX64(SizeX64::qword, noreg, 1, rip, *prev);
}
size_t pos = allocateData(8, 8);
writef64(&data[pos], value);
return OperandX64(SizeX64::qword, noreg, 1, rip, int32_t(pos - data.size()));
int32_t offset = int32_t(pos - data.size());
if (as64BitKey != ~0ull)
constCache64[as64BitKey] = offset;
return OperandX64(SizeX64::qword, noreg, 1, rip, offset);
}
OperandX64 AssemblyBuilderX64::f32x4(float x, float y, float z, float w)

View file

@ -16,7 +16,7 @@
#endif
#include <Windows.h>
#elif !defined(_WIN32)
#elif defined(__linux__) || defined(__APPLE__)
// Defined in unwind.h which may not be easily discoverable on various platforms
extern "C" void __register_frame(const void*);
@ -26,12 +26,16 @@ extern "C" void __unw_add_dynamic_fde() __attribute__((weak));
#endif
#if defined(__APPLE__) && defined(__aarch64__)
#include <sys/sysctl.h>
#endif
namespace Luau
{
namespace CodeGen
{
#if !defined(_WIN32)
#if defined(__linux__) || defined(__APPLE__)
static void visitFdeEntries(char* pos, void (*cb)(const void*))
{
// When using glibc++ unwinder, we need to call __register_frame/__deregister_frame on the entire .eh_frame data
@ -78,7 +82,7 @@ void* createBlockUnwindInfo(void* context, uint8_t* block, size_t blockSize, siz
LUAU_ASSERT(!"failed to allocate function table");
return nullptr;
}
#elif !defined(_WIN32)
#elif defined(__linux__) || defined(__APPLE__)
visitFdeEntries(unwindData, __register_frame);
#endif
@ -91,10 +95,26 @@ void destroyBlockUnwindInfo(void* context, void* unwindData)
#if defined(_WIN32) && defined(_M_X64)
if (!RtlDeleteFunctionTable((RUNTIME_FUNCTION*)unwindData))
LUAU_ASSERT(!"failed to deallocate function table");
#elif !defined(_WIN32)
#elif defined(__linux__) || defined(__APPLE__)
visitFdeEntries((char*)unwindData, __deregister_frame);
#endif
}
bool isUnwindSupported()
{
#if defined(_WIN32) && defined(_M_X64)
return true;
#elif defined(__APPLE__) && defined(__aarch64__)
char ver[256];
size_t verLength = sizeof(ver);
// libunwind on macOS 12 and earlier (which maps to osrelease 21) assumes JIT frames use pointer authentication without a way to override that
return sysctlbyname("kern.osrelease", ver, &verLength, NULL, 0) == 0 && atoi(ver) >= 22;
#elif defined(__linux__) || defined(__APPLE__)
return true;
#else
return false;
#endif
}
} // namespace CodeGen
} // namespace Luau

View file

@ -58,6 +58,9 @@ namespace Luau
namespace CodeGen
{
static void* gPerfLogContext = nullptr;
static PerfLogFn gPerfLogFn = nullptr;
static NativeProto* createNativeProto(Proto* proto, const IrBuilder& ir)
{
int sizecode = proto->sizecode;
@ -87,6 +90,20 @@ static void destroyNativeProto(NativeProto* nativeProto)
::operator delete(memory);
}
static void logPerfFunction(Proto* p, uintptr_t addr, unsigned size)
{
LUAU_ASSERT(p->source);
const char* source = getstr(p->source);
source = (source[0] == '=' || source[0] == '@') ? source + 1 : "[string]";
char name[256];
snprintf(name, sizeof(name), "<luau> %s:%d %s", source, p->linedefined, p->debugname ? getstr(p->debugname) : "");
if (gPerfLogFn)
gPerfLogFn(gPerfLogContext, addr, size, name);
}
template<typename AssemblyBuilder, typename IrLowering>
static bool lowerImpl(AssemblyBuilder& build, IrLowering& lowering, IrFunction& function, int bytecodeid, AssemblyOptions options)
{
@ -329,24 +346,17 @@ static void onDestroyFunction(lua_State* L, Proto* proto)
static int onEnter(lua_State* L, Proto* proto)
{
if (L->singlestep)
return 1;
NativeState* data = getNativeState(L);
if (!L->ci->savedpc)
L->ci->savedpc = proto->code;
// We will jump into native code through a gateway
bool (*gate)(lua_State*, Proto*, uintptr_t, NativeContext*) = (bool (*)(lua_State*, Proto*, uintptr_t, NativeContext*))data->context.gateEntry;
NativeProto* nativeProto = getProtoExecData(proto);
LUAU_ASSERT(nativeProto);
LUAU_ASSERT(L->ci->savedpc);
// instOffsets uses negative indexing for optimal codegen for RETURN opcode
uintptr_t target = nativeProto->instBase + nativeProto->instOffsets[-(L->ci->savedpc - proto->code)];
uintptr_t target = nativeProto->instBase + nativeProto->instOffsets[proto->code - L->ci->savedpc];
// Returns 1 to finish the function in the VM
return gate(L, proto, target, &data->context);
return GateFn(data->context.gateEntry)(L, proto, target, &data->context);
}
static void onSetBreakpoint(lua_State* L, Proto* proto, int instruction)
@ -375,9 +385,9 @@ static unsigned int getCpuFeaturesA64()
bool isSupported()
{
#if !LUA_CUSTOM_EXECUTION
return false;
#elif defined(__x86_64__) || defined(_M_X64)
if (!LUA_CUSTOM_EXECUTION)
return false;
if (LUA_EXTRA_SIZE != 1)
return false;
@ -387,6 +397,16 @@ bool isSupported()
if (sizeof(LuaNode) != 32)
return false;
// Windows CRT uses stack unwinding in longjmp so we have to use unwind data; on other platforms, it's only necessary for C++ EH.
#if defined(_WIN32)
if (!isUnwindSupported())
return false;
#else
if (!LUA_USE_LONGJMP && !isUnwindSupported())
return false;
#endif
#if defined(__x86_64__) || defined(_M_X64)
int cpuinfo[4] = {};
#ifdef _MSC_VER
__cpuid(cpuinfo, 1);
@ -402,21 +422,6 @@ bool isSupported()
return true;
#elif defined(__aarch64__)
if (LUA_EXTRA_SIZE != 1)
return false;
if (sizeof(TValue) != 16)
return false;
if (sizeof(LuaNode) != 32)
return false;
#ifdef _WIN32
// Unwind info is not supported for Windows-on-ARM yet
if (!LUA_USE_LONGJMP)
return false;
#endif
return true;
#else
return false;
@ -456,6 +461,9 @@ void create(lua_State* L)
}
#endif
if (gPerfLogFn)
gPerfLogFn(gPerfLogContext, uintptr_t(data.context.gateEntry), 4096, "<luau gate>");
lua_ExecutionCallbacks* ecb = getExecutionCallbacks(L);
ecb->close = onCloseState;
@ -540,6 +548,20 @@ void compile(lua_State* L, int idx)
return;
}
if (gPerfLogFn && results.size() > 0)
{
gPerfLogFn(gPerfLogContext, uintptr_t(codeStart), results[0]->instOffsets[0], "<luau helpers>");
for (size_t i = 0; i < results.size(); ++i)
{
uint32_t begin = results[i]->instOffsets[0];
uint32_t end = i + 1 < results.size() ? results[i + 1]->instOffsets[0] : uint32_t(build.code.size() * sizeof(build.code[0]));
LUAU_ASSERT(begin < end);
logPerfFunction(results[i]->proto, uintptr_t(codeStart) + begin, end - begin);
}
}
// Record instruction base address; at runtime, instOffsets[] will be used as offsets from instBase
for (NativeProto* result : results)
{
@ -591,5 +613,11 @@ std::string getAssembly(lua_State* L, int idx, AssemblyOptions options)
return build.text;
}
void setPerfLog(void* context, PerfLogFn logFn)
{
gPerfLogContext = context;
gPerfLogFn = logFn;
}
} // namespace CodeGen
} // namespace Luau

View file

@ -267,7 +267,7 @@ void callStepGc(IrRegAllocX64& regs, AssemblyBuilderX64& build)
void emitExit(AssemblyBuilderX64& build, bool continueInVm)
{
if (continueInVm)
build.mov(al, 1);
build.mov(eax, 1);
else
build.xor_(eax, eax);

View file

@ -1483,7 +1483,7 @@ void IrLoweringA64::lowerInst(IrInst& inst, uint32_t index, IrBlock& next)
{
inst.regA64 = regs.allocReuse(KindA64::w, index, {inst.a});
RegisterA64 temp = tempUint(inst.a);
build.mvn(inst.regA64, temp);
build.mvn_(inst.regA64, temp);
break;
}
case IrCmd::BITLSHIFT_UINT:
@ -1660,8 +1660,28 @@ RegisterA64 IrLoweringA64::tempDouble(IrOp op)
{
RegisterA64 temp1 = regs.allocTemp(KindA64::x);
RegisterA64 temp2 = regs.allocTemp(KindA64::d);
build.adr(temp1, val);
build.ldr(temp2, temp1);
uint64_t vali;
static_assert(sizeof(vali) == sizeof(val), "Expecting double to be 64-bit");
memcpy(&vali, &val, sizeof(val));
if ((vali << 16) == 0)
{
build.movz(temp1, uint16_t(vali >> 48), 48);
build.fmov(temp2, temp1);
}
else if ((vali << 32) == 0)
{
build.movz(temp1, uint16_t(vali >> 48), 48);
build.movk(temp1, uint16_t(vali >> 32), 32);
build.fmov(temp2, temp1);
}
else
{
build.adr(temp1, val);
build.ldr(temp2, temp1);
}
return temp2;
}
}

View file

@ -65,21 +65,42 @@ void translateInstLoadN(IrBuilder& build, const Instruction* pc)
build.inst(IrCmd::STORE_TAG, build.vmReg(ra), build.constTag(LUA_TNUMBER));
}
static void translateInstLoadConstant(IrBuilder& build, int ra, int k)
{
TValue protok = build.function.proto->k[k];
// Compiler only generates LOADK for source-level constants, so dynamic imports are not affected
if (protok.tt == LUA_TNIL)
{
build.inst(IrCmd::STORE_TAG, build.vmReg(ra), build.constTag(LUA_TNIL));
}
else if (protok.tt == LUA_TBOOLEAN)
{
build.inst(IrCmd::STORE_INT, build.vmReg(ra), build.constInt(protok.value.b));
build.inst(IrCmd::STORE_TAG, build.vmReg(ra), build.constTag(LUA_TBOOLEAN));
}
else if (protok.tt == LUA_TNUMBER)
{
build.inst(IrCmd::STORE_DOUBLE, build.vmReg(ra), build.constDouble(protok.value.n));
build.inst(IrCmd::STORE_TAG, build.vmReg(ra), build.constTag(LUA_TNUMBER));
}
else
{
// Remaining tag here right now is LUA_TSTRING, while it can be transformed to LOAD_POINTER/STORE_POINTER/STORE_TAG, it's not profitable right
// now
IrOp load = build.inst(IrCmd::LOAD_TVALUE, build.vmConst(k));
build.inst(IrCmd::STORE_TVALUE, build.vmReg(ra), load);
}
}
void translateInstLoadK(IrBuilder& build, const Instruction* pc)
{
int ra = LUAU_INSN_A(*pc);
IrOp load = build.inst(IrCmd::LOAD_TVALUE, build.vmConst(LUAU_INSN_D(*pc)));
build.inst(IrCmd::STORE_TVALUE, build.vmReg(ra), load);
translateInstLoadConstant(build, LUAU_INSN_A(*pc), LUAU_INSN_D(*pc));
}
void translateInstLoadKX(IrBuilder& build, const Instruction* pc)
{
int ra = LUAU_INSN_A(*pc);
uint32_t aux = pc[1];
IrOp load = build.inst(IrCmd::LOAD_TVALUE, build.vmConst(aux));
build.inst(IrCmd::STORE_TVALUE, build.vmReg(ra), load);
translateInstLoadConstant(build, LUAU_INSN_A(*pc), pc[1]);
}
void translateInstMove(IrBuilder& build, const Instruction* pc)

View file

@ -108,6 +108,8 @@ struct NativeContext
luau_FastFunction luauF_table[256] = {};
};
using GateFn = int (*)(lua_State*, Proto*, uintptr_t, NativeContext*);
struct NativeState
{
NativeState();

View file

@ -14,6 +14,8 @@ inline bool isFlagExperimental(const char* flag)
"LuauInstantiateInSubtyping", // requires some fixes to lua-apps code
"LuauTypecheckTypeguards", // requires some fixes to lua-apps code (CLI-67030)
"LuauTinyControlFlowAnalysis", // waiting for updates to packages depended by internal builtin plugins
"LuauUnifyTwoOptions", // requires some fixes to lua-apps code
// makes sure we always have at least one entry
nullptr,
};

View file

@ -180,6 +180,7 @@ target_sources(Luau.Analysis PRIVATE
Analysis/include/Luau/TypeAttach.h
Analysis/include/Luau/TypeChecker2.h
Analysis/include/Luau/TypedAllocator.h
Analysis/include/Luau/TypeFamily.h
Analysis/include/Luau/TypeInfer.h
Analysis/include/Luau/TypePack.h
Analysis/include/Luau/TypeReduction.h
@ -230,6 +231,7 @@ target_sources(Luau.Analysis PRIVATE
Analysis/src/TypeAttach.cpp
Analysis/src/TypeChecker2.cpp
Analysis/src/TypedAllocator.cpp
Analysis/src/TypeFamily.cpp
Analysis/src/TypeInfer.cpp
Analysis/src/TypePack.cpp
Analysis/src/TypeReduction.cpp
@ -382,6 +384,8 @@ if(TARGET Luau.UnitTest)
tests/TopoSort.test.cpp
tests/ToString.test.cpp
tests/Transpiler.test.cpp
tests/TxnLog.test.cpp
tests/TypeFamily.test.cpp
tests/TypeInfer.aliases.test.cpp
tests/TypeInfer.annotations.test.cpp
tests/TypeInfer.anyerror.test.cpp

View file

@ -12,6 +12,8 @@
#include <string.h>
#include <stdio.h>
LUAU_FASTFLAGVARIABLE(LuauFixBreakpointLineSearch, false)
static const char* getfuncname(Closure* f);
static int currentpc(lua_State* L, CallInfo* ci)
@ -423,11 +425,23 @@ static int getnextline(Proto* p, int line)
if (LUAU_INSN_OP(p->code[i]) == LOP_PREPVARARGS)
continue;
int current = luaG_getline(p, i);
if (current >= line)
int candidate = luaG_getline(p, i);
if (FFlag::LuauFixBreakpointLineSearch)
{
closest = current;
break;
if (candidate == line)
return line;
if (candidate > line && (closest == -1 || candidate < closest))
closest = candidate;
}
else
{
if (candidate >= line)
{
closest = candidate;
break;
}
}
}
}
@ -436,9 +450,21 @@ static int getnextline(Proto* p, int line)
{
// Find the closest line number to the intended one.
int candidate = getnextline(p->p[i], line);
if (closest == -1 || (candidate >= line && candidate < closest))
if (FFlag::LuauFixBreakpointLineSearch)
{
closest = candidate;
if (candidate == line)
return line;
if (candidate > line && (closest == -1 || candidate < closest))
closest = candidate;
}
else
{
if (closest == -1 || (candidate >= line && candidate < closest))
{
closest = candidate;
}
}
}

View file

@ -210,7 +210,7 @@ static void luau_execute(lua_State* L)
#if LUA_CUSTOM_EXECUTION
Proto* p = clvalue(L->ci->func)->l.p;
if (p->execdata)
if (p->execdata && !SingleStep)
{
if (L->global->ecb.enter(L, p) == 0)
return;
@ -952,9 +952,9 @@ reentry:
L->top = p->is_vararg ? argi : ci->top;
#if LUA_CUSTOM_EXECUTION
if (p->execdata)
if (LUAU_UNLIKELY(p->execdata && !SingleStep))
{
LUAU_ASSERT(L->global->ecb.enter);
ci->savedpc = p->code;
if (L->global->ecb.enter(L, p) == 1)
goto reentry;
@ -1050,10 +1050,8 @@ reentry:
Proto* nextproto = nextcl->l.p;
#if LUA_CUSTOM_EXECUTION
if (nextproto->execdata)
if (LUAU_UNLIKELY(nextproto->execdata && !SingleStep))
{
LUAU_ASSERT(L->global->ecb.enter);
if (L->global->ecb.enter(L, nextproto) == 1)
goto reentry;
else

View file

@ -0,0 +1,39 @@
local bench = script and require(script.Parent.bench_support) or require("bench_support")
local function mmul(matrix1, matrix2)
local shapeRows = #matrix1
local shapeColumns = #matrix2[1]
local result = table.create(shapeRows)
for i = 1, shapeRows do
result[i] = table.create(shapeColumns)
for j = 1, shapeColumns do
local sum = 0
for k = 1, shapeColumns do
sum = sum + matrix1[i][k] * matrix2[k][j]
end
result[i][j] = sum
end
end
return result
end
function test()
local n = 100
local mat = table.create(n)
for i = 1, n do
local t = table.create(n)
for k = 1, n do
t[k] = math.random()
end
mat[i] = t
end
local startTime = os.clock()
local result = mmul(mat, mat)
return os.clock() - startTime
end
bench.runCode(test, "matrixmult")

View file

@ -0,0 +1,254 @@
--!strict
local bench = script and require(script.Parent.bench_support) or require("bench_support")
function test()
type Vertex = {
pX: number, pY: number, pZ: number,
uvX: number, uvY: number, uvZ: number,
nX: number, nY: number, nZ: number,
tX: number, tY: number, tZ: number,
bX: number, bY: number, bZ: number,
h: number
}
local grid_size = 100
local mesh: {
vertices: {Vertex},
indices: {number},
triangle_cone_p: {{x: number, y: number, z: number}},
triangle_cone_n: {{x: number, y: number, z: number}}
} = {
vertices = table.create(grid_size * grid_size),
indices = table.create((grid_size - 1) * (grid_size - 1) * 6),
triangle_cone_p = table.create((grid_size - 1) * (grid_size - 1) * 2),
triangle_cone_n = table.create((grid_size - 1) * (grid_size - 1) * 2)
}
local function init_vertices()
local i = 1
for y = 1,grid_size do
for x = 1,grid_size do
local v: Vertex = {}
v.pX = x
v.pY = y
v.pZ = math.cos(x) + math.sin(y)
v.uvX = (x-1)/(grid_size-1)
v.uvY = (y-1)/(grid_size-1)
v.uvZ = 0
v.nX = 0
v.nY = 0
v.nZ = 0
v.bX = 0
v.bY = 0
v.bZ = 0
v.tX = 0
v.tY = 0
v.tZ = 0
v.h = 0
mesh.vertices[i] = v
i += 1
end
end
end
local function init_indices()
local i = 1
for y = 1,grid_size-1 do
for x = 1,grid_size-1 do
mesh.indices[i] = x + (y-1)*grid_size
i += 1
mesh.indices[i] = x + y*grid_size
i += 1
mesh.indices[i] = (x+1) + (y-1)*grid_size
i += 1
mesh.indices[i] = (x+1) + (y-1)*grid_size
i += 1
mesh.indices[i] = x + y*grid_size
i += 1
mesh.indices[i] = (x+1) + y*grid_size
i += 1
end
end
end
local function calculate_normals()
local norm_sum = 0
for i = 1,#mesh.indices,3 do
local a = mesh.vertices[mesh.indices[i]]
local b = mesh.vertices[mesh.indices[i + 1]]
local c = mesh.vertices[mesh.indices[i + 2]]
local abx = a.pX - b.pX
local aby = a.pY - b.pY
local abz = a.pZ - b.pZ
local acx = a.pX - c.pX
local acy = a.pY - c.pY
local acz = a.pZ - c.pZ
local nx = aby * acz - abz * acy;
local ny = abz * acx - abx * acz;
local nz = abx * acy - aby * acx;
a.nX += nx
a.nY += ny
a.nZ += nz
b.nX += nx
b.nY += ny
b.nZ += nz
c.nX += nx
c.nY += ny
c.nZ += nz
end
for _,v in mesh.vertices do
local magnitude = math.sqrt(v.nX * v.nX + v.nY * v.nY + v.nZ * v.nZ)
v.nX /= magnitude
v.nY /= magnitude
v.nZ /= magnitude
norm_sum += v.nX * v.nX + v.nY * v.nY + v.nZ * v.nZ
end
return norm_sum
end
local function compute_triangle_cones()
local mesh_area = 0
local pos = 1
for i = 1,#mesh.indices,3 do
local p0 = mesh.vertices[mesh.indices[i]]
local p1 = mesh.vertices[mesh.indices[i + 1]]
local p2 = mesh.vertices[mesh.indices[i + 2]]
local p10x = p1.pX - p0.pX
local p10y = p1.pY - p0.pY
local p10z = p1.pZ - p0.pZ
local p20x = p2.pX - p0.pX
local p20y = p2.pY - p0.pY
local p20z = p2.pZ - p0.pZ
local normalx = p10y * p20z - p10z * p20y;
local normaly = p10z * p20x - p10x * p20z;
local normalz = p10x * p20y - p10y * p20x;
local area = math.sqrt(normalx * normalx + normaly * normaly + normalz * normalz)
local invarea = if area == 0 then 0 else 1 / area;
local rx = (p0.pX + p1.pX + p2.pX) / 3
local ry = (p0.pY + p1.pY + p2.pY) / 3
local rz = (p0.pZ + p1.pZ + p2.pZ) / 3
mesh.triangle_cone_p[pos] = { x = rx, y = ry, z = rz }
mesh.triangle_cone_n[pos] = { x = normalx * invarea, y = normaly * invarea, z = normalz * invarea}
pos += 1
mesh_area += area
end
return mesh_area
end
local function compute_tangent_space()
local checksum = 0
for i = 1,#mesh.indices,3 do
local a = mesh.vertices[mesh.indices[i]]
local b = mesh.vertices[mesh.indices[i + 1]]
local c = mesh.vertices[mesh.indices[i + 2]]
local x1 = b.pX - a.pX
local x2 = c.pX - a.pX
local y1 = b.pY - a.pY
local y2 = c.pY - a.pY
local z1 = b.pZ - a.pZ
local z2 = c.pZ - a.pZ
local s1 = b.uvX - a.uvX
local s2 = c.uvX - a.uvX
local t1 = b.uvY - a.uvY
local t2 = c.uvY - a.uvY
local r = 1.0 / (s1 * t2 - s2 * t1);
local sdirX = (t2 * x1 - t1 * x2) * r
local sdirY = (t2 * y1 - t1 * y2) * r
local sdirZ = (t2 * z1 - t1 * z2) * r
local tdirX = (s1 * x2 - s2 * x1) * r
local tdirY = (s1 * y2 - s2 * y1) * r
local tdirZ = (s1 * z2 - s2 * z1) * r
a.tX += sdirX
a.tY += sdirY
a.tZ += sdirZ
b.tX += sdirX
b.tY += sdirY
b.tZ += sdirZ
c.tX += sdirX
c.tY += sdirY
c.tZ += sdirZ
a.bX += tdirX
a.bY += tdirY
a.bZ += tdirZ
b.bX += tdirX
b.bY += tdirY
b.bZ += tdirZ
c.bX += tdirX
c.bY += tdirY
c.bZ += tdirZ
end
for _,v in mesh.vertices do
local tX = v.tX
local tY = v.tY
local tZ = v.tZ
-- Gram-Schmidt orthogonalize
local ndt = v.nX * tX + v.nY * tY + v.nZ * tZ
local tmnsX = tX - v.nX * ndt
local tmnsY = tY - v.nY * ndt
local tmnsZ = tZ - v.nZ * ndt
local l = math.sqrt(tmnsX * tmnsX + tmnsY * tmnsY + tmnsZ * tmnsZ)
local invl = 1 / l
v.tX = tmnsX * invl
v.tY = tmnsY * invl
v.tZ = tmnsZ * invl
local normalx = v.nY * tZ - v.nZ * tY;
local normaly = v.nZ * tX - v.nX * tZ;
local normalz = v.nX * tY - v.nY * tX;
local ht = normalx * v.bX + normaly * v.bY + normalz * v.bZ
v.h = ht < 0 and -1 or 1
checksum += v.tX + v.h
end
return checksum
end
init_vertices()
init_indices()
calculate_normals()
compute_triangle_cones()
compute_tangent_space()
end
bench.runCode(test, "mesh-normal-scalar")

View file

@ -70,7 +70,7 @@ TEST_CASE_FIXTURE(AssemblyBuilderA64Fixture, "Unary")
{
SINGLE_COMPARE(neg(x0, x1), 0xCB0103E0);
SINGLE_COMPARE(neg(w0, w1), 0x4B0103E0);
SINGLE_COMPARE(mvn(x0, x1), 0xAA2103E0);
SINGLE_COMPARE(mvn_(x0, x1), 0xAA2103E0);
SINGLE_COMPARE(clz(x0, x1), 0xDAC01020);
SINGLE_COMPARE(clz(w0, w1), 0x5AC01020);
@ -338,6 +338,7 @@ TEST_CASE_FIXTURE(AssemblyBuilderA64Fixture, "AddressOfLabel")
TEST_CASE_FIXTURE(AssemblyBuilderA64Fixture, "FPBasic")
{
SINGLE_COMPARE(fmov(d0, d1), 0x1E604020);
SINGLE_COMPARE(fmov(d0, x1), 0x9E670020);
}
TEST_CASE_FIXTURE(AssemblyBuilderA64Fixture, "FPMath")

View file

@ -684,15 +684,30 @@ TEST_CASE("ConstantStorage")
build.finalize();
LUAU_ASSERT(build.data.size() == 12004);
CHECK(build.data.size() == 12004);
for (int i = 0; i <= 3000; i++)
{
LUAU_ASSERT(build.data[i * 4 + 0] == 0x00);
LUAU_ASSERT(build.data[i * 4 + 1] == 0x00);
LUAU_ASSERT(build.data[i * 4 + 2] == 0x80);
LUAU_ASSERT(build.data[i * 4 + 3] == 0x3f);
CHECK(build.data[i * 4 + 0] == 0x00);
CHECK(build.data[i * 4 + 1] == 0x00);
CHECK(build.data[i * 4 + 2] == 0x80);
CHECK(build.data[i * 4 + 3] == 0x3f);
}
}
TEST_CASE("ConstantCaching")
{
AssemblyBuilderX64 build(/* logText= */ false);
OperandX64 two = build.f64(2);
// Force data relocation
for (int i = 0; i < 4096; i++)
build.f64(i);
CHECK(build.f64(2).imm == two.imm);
build.finalize();
}
TEST_SUITE_END();

View file

@ -105,6 +105,19 @@ ClassFixture::ClassFixture()
};
globals.globalScope->exportedTypeBindings["CallableClass"] = TypeFun{{}, callableClassType};
auto addIndexableClass = [&arena, &globals](const char* className, TypeId keyType, TypeId returnType) {
ScopedFastFlag LuauTypecheckClassTypeIndexers("LuauTypecheckClassTypeIndexers", true);
TypeId indexableClassMetaType = arena.addType(TableType{});
TypeId indexableClassType =
arena.addType(ClassType{className, {}, nullopt, indexableClassMetaType, {}, {}, "Test", TableIndexer{keyType, returnType}});
globals.globalScope->exportedTypeBindings[className] = TypeFun{{}, indexableClassType};
};
// IndexableClass has a table indexer with a key type of 'number | string' and a return type of 'number'
addIndexableClass("IndexableClass", arena.addType(Luau::UnionType{{builtinTypes->stringType, numberType}}), numberType);
// IndexableNumericKeyClass has a table indexer with a key type of 'number' and a return type of 'number'
addIndexableClass("IndexableNumericKeyClass", numberType, numberType);
for (const auto& [name, tf] : globals.globalScope->exportedTypeBindings)
persist(tf.type);

View file

@ -646,6 +646,10 @@ static void throwing(int64_t arg)
TEST_CASE("GeneratedCodeExecutionWithThrowA64")
{
// macOS 12 doesn't support JIT frames without pointer authentication
if (!isUnwindSupported())
return;
using namespace A64;
AssemblyBuilderA64 build(/* logText= */ false);

View file

@ -561,6 +561,8 @@ TEST_CASE("Debug")
TEST_CASE("Debugger")
{
ScopedFastFlag luauFixBreakpointLineSearch{"LuauFixBreakpointLineSearch", true};
static int breakhits = 0;
static lua_State* interruptedthread = nullptr;
static bool singlestep = false;
@ -703,6 +705,15 @@ TEST_CASE("Debugger")
CHECK(lua_tointeger(L, -1) == 9);
lua_pop(L, 1);
}
else if (breakhits == 13)
{
// validate assignment via lua_getlocal
const char* l = lua_getlocal(L, 0, 1);
REQUIRE(l);
CHECK(strcmp(l, "a") == 0);
CHECK(lua_isnil(L, -1));
lua_pop(L, 1);
}
if (interruptedthread)
{
@ -712,7 +723,7 @@ TEST_CASE("Debugger")
},
nullptr, &copts, /* skipCodegen */ true); // Native code doesn't support debugging yet
CHECK(breakhits == 12); // 2 hits per breakpoint
CHECK(breakhits == 14); // 2 hits per breakpoint
if (singlestep)
CHECK(stephits > 100); // note; this will depend on number of instructions which can vary, so we just make sure the callback gets hit often

113
tests/TxnLog.test.cpp Normal file
View file

@ -0,0 +1,113 @@
// This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details
#include "doctest.h"
#include "Luau/Scope.h"
#include "Luau/ToString.h"
#include "Luau/TxnLog.h"
#include "Luau/Type.h"
#include "Luau/TypeArena.h"
#include "ScopedFlags.h"
using namespace Luau;
struct TxnLogFixture
{
TxnLog log{/*useScopes*/ true};
TxnLog log2{/*useScopes*/ true};
TypeArena arena;
BuiltinTypes builtinTypes;
ScopePtr globalScope = std::make_shared<Scope>(builtinTypes.anyTypePack);
ScopePtr childScope = std::make_shared<Scope>(globalScope);
TypeId a = arena.freshType(globalScope.get());
TypeId b = arena.freshType(globalScope.get());
TypeId c = arena.freshType(childScope.get());
};
TEST_SUITE_BEGIN("TxnLog");
TEST_CASE_FIXTURE(TxnLogFixture, "colliding_union_incoming_type_has_greater_scope")
{
ScopedFastFlag sff{"DebugLuauDeferredConstraintResolution", true};
log.replace(c, BoundType{a});
log2.replace(a, BoundType{c});
CHECK(nullptr != log.pending(c));
log.concatAsUnion(std::move(log2), NotNull{&arena});
// 'a has greater scope than 'c, so we expect the incoming binding of 'a to
// be discarded.
CHECK(nullptr == log.pending(a));
const PendingType* pt = log.pending(c);
REQUIRE(pt != nullptr);
CHECK(!pt->dead);
const BoundType* bt = get_if<BoundType>(&pt->pending.ty);
CHECK(a == bt->boundTo);
log.commit();
REQUIRE(get<FreeType>(a));
const BoundType* bound = get<BoundType>(c);
REQUIRE(bound);
CHECK(a == bound->boundTo);
}
TEST_CASE_FIXTURE(TxnLogFixture, "colliding_union_incoming_type_has_lesser_scope")
{
ScopedFastFlag sff{"DebugLuauDeferredConstraintResolution", true};
log.replace(a, BoundType{c});
log2.replace(c, BoundType{a});
CHECK(nullptr != log.pending(a));
log.concatAsUnion(std::move(log2), NotNull{&arena});
// 'a has greater scope than 'c, so we expect the binding of 'a to be
// discarded, and for that of 'c to be brought in.
CHECK(nullptr == log.pending(a));
const PendingType* pt = log.pending(c);
REQUIRE(pt != nullptr);
CHECK(!pt->dead);
const BoundType* bt = get_if<BoundType>(&pt->pending.ty);
CHECK(a == bt->boundTo);
log.commit();
REQUIRE(get<FreeType>(a));
const BoundType* bound = get<BoundType>(c);
REQUIRE(bound);
CHECK(a == bound->boundTo);
}
TEST_CASE_FIXTURE(TxnLogFixture, "colliding_coincident_logs_do_not_create_degenerate_unions")
{
ScopedFastFlag sff{"DebugLuauDeferredConstraintResolution", true};
log.replace(a, BoundType{b});
log2.replace(a, BoundType{b});
log.concatAsUnion(std::move(log2), NotNull{&arena});
log.commit();
CHECK("a" == toString(a));
CHECK("a" == toString(b));
}
TEST_SUITE_END();

205
tests/TypeFamily.test.cpp Normal file
View file

@ -0,0 +1,205 @@
// This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details
#include "Luau/TypeFamily.h"
#include "Luau/Type.h"
#include "Fixture.h"
#include "doctest.h"
using namespace Luau;
LUAU_FASTFLAG(DebugLuauDeferredConstraintResolution)
struct FamilyFixture : Fixture
{
TypeFamily swapFamily;
FamilyFixture()
: Fixture(true, false)
{
swapFamily = TypeFamily{/* name */ "Swap",
/* reducer */
[](std::vector<TypeId> tys, std::vector<TypePackId> tps, NotNull<TypeArena> arena, NotNull<BuiltinTypes> builtins,
NotNull<const TxnLog> log) -> TypeFamilyReductionResult<TypeId> {
LUAU_ASSERT(tys.size() == 1);
TypeId param = log->follow(tys.at(0));
if (isString(param))
{
return TypeFamilyReductionResult<TypeId>{builtins->numberType, false, {}, {}};
}
else if (isNumber(param))
{
return TypeFamilyReductionResult<TypeId>{builtins->stringType, false, {}, {}};
}
else if (log->get<BlockedType>(param) || log->get<FreeType>(param) || log->get<PendingExpansionType>(param) ||
log->get<TypeFamilyInstanceType>(param))
{
return TypeFamilyReductionResult<TypeId>{std::nullopt, false, {param}, {}};
}
else
{
return TypeFamilyReductionResult<TypeId>{std::nullopt, true, {}, {}};
}
}};
unfreeze(frontend.globals.globalTypes);
TypeId t = frontend.globals.globalTypes.addType(GenericType{"T"});
GenericTypeDefinition genericT{t};
ScopePtr globalScope = frontend.globals.globalScope;
globalScope->exportedTypeBindings["Swap"] =
TypeFun{{genericT}, frontend.globals.globalTypes.addType(TypeFamilyInstanceType{NotNull{&swapFamily}, {t}, {}})};
freeze(frontend.globals.globalTypes);
}
};
TEST_SUITE_BEGIN("TypeFamilyTests");
TEST_CASE_FIXTURE(FamilyFixture, "basic_type_family")
{
if (!FFlag::DebugLuauDeferredConstraintResolution)
return;
CheckResult result = check(R"(
type A = Swap<number>
type B = Swap<string>
type C = Swap<boolean>
local x = 123
local y: Swap<typeof(x)> = "foo"
)");
LUAU_REQUIRE_ERROR_COUNT(1, result);
CHECK("string" == toString(requireTypeAlias("A")));
CHECK("number" == toString(requireTypeAlias("B")));
CHECK("Swap<boolean>" == toString(requireTypeAlias("C")));
CHECK("string" == toString(requireType("y")));
CHECK("Type family instance Swap<boolean> is uninhabited" == toString(result.errors[0]));
};
TEST_CASE_FIXTURE(FamilyFixture, "type_reduction_reduces_families")
{
if (!FFlag::DebugLuauDeferredConstraintResolution)
return;
CheckResult result = check(R"(
local x: Swap<string> & nil
)");
CHECK("never" == toString(requireType("x")));
}
TEST_CASE_FIXTURE(FamilyFixture, "family_as_fn_ret")
{
if (!FFlag::DebugLuauDeferredConstraintResolution)
return;
CheckResult result = check(R"(
local swapper: <T>(T) -> Swap<T>
local a = swapper(123)
local b = swapper("foo")
local c = swapper(false)
)");
LUAU_REQUIRE_ERROR_COUNT(1, result);
CHECK("string" == toString(requireType("a")));
CHECK("number" == toString(requireType("b")));
CHECK("Swap<boolean>" == toString(requireType("c")));
CHECK("Type family instance Swap<boolean> is uninhabited" == toString(result.errors[0]));
}
TEST_CASE_FIXTURE(FamilyFixture, "family_as_fn_arg")
{
if (!FFlag::DebugLuauDeferredConstraintResolution)
return;
CheckResult result = check(R"(
local swapper: <T>(Swap<T>) -> T
local a = swapper(123)
local b = swapper(false)
)");
LUAU_REQUIRE_ERROR_COUNT(2, result);
// FIXME: Can we constrain these to `never` or `unknown`?
CHECK("a" == toString(requireType("a")));
CHECK("a" == toString(requireType("b")));
CHECK("Type family instance Swap<a> is uninhabited" == toString(result.errors[0]));
CHECK("Type family instance Swap<a> is uninhabited" == toString(result.errors[1]));
}
TEST_CASE_FIXTURE(FamilyFixture, "resolve_deep_families")
{
if (!FFlag::DebugLuauDeferredConstraintResolution)
return;
CheckResult result = check(R"(
local x: Swap<Swap<Swap<string>>>
)");
LUAU_REQUIRE_NO_ERRORS(result);
CHECK("number" == toString(requireType("x")));
}
TEST_CASE_FIXTURE(FamilyFixture, "unsolvable_family")
{
if (!FFlag::DebugLuauDeferredConstraintResolution)
return;
CheckResult result = check(R"(
local impossible: <T>(Swap<T>) -> Swap<Swap<T>>
local a = impossible(123)
local b = impossible(true)
)");
LUAU_REQUIRE_ERROR_COUNT(4, result);
for (size_t i = 0; i < 4; ++i)
{
CHECK(toString(result.errors[i]) == "Type family instance Swap<a> is uninhabited");
}
}
TEST_CASE_FIXTURE(FamilyFixture, "table_internal_families")
{
if (!FFlag::DebugLuauDeferredConstraintResolution)
return;
CheckResult result = check(R"(
local t: <T>({T}) -> {Swap<T>}
local a = t({1, 2, 3})
local b = t({"a", "b", "c"})
local c = t({true, false, true})
)");
LUAU_REQUIRE_ERROR_COUNT(1, result);
CHECK(toString(requireType("a")) == "{string}");
CHECK(toString(requireType("b")) == "{number}");
CHECK(toString(requireType("c")) == "{Swap<boolean>}");
CHECK(toString(result.errors[0]) == "Type family instance Swap<boolean> is uninhabited");
}
TEST_CASE_FIXTURE(FamilyFixture, "function_internal_families")
{
// This test is broken right now, but it's not because of type families. See
// CLI-71143.
if (!FFlag::DebugLuauDeferredConstraintResolution)
return;
CheckResult result = check(R"(
local f0: <T>(T) -> (() -> T)
local f: <T>(T) -> (() -> Swap<T>)
local a = f(1)
local b = f("a")
local c = f(true)
local d = f0(1)
)");
LUAU_REQUIRE_ERROR_COUNT(1, result);
CHECK(toString(requireType("a")) == "() -> string");
CHECK(toString(requireType("b")) == "() -> number");
CHECK(toString(requireType("c")) == "() -> Swap<boolean>");
CHECK(toString(result.errors[0]) == "Type family instance Swap<boolean> is uninhabited");
}
TEST_SUITE_END();

View file

@ -481,4 +481,150 @@ TEST_CASE_FIXTURE(ClassFixture, "callable_classes")
CHECK_EQ("number", toString(requireType("y")));
}
TEST_CASE_FIXTURE(ClassFixture, "indexable_classes")
{
// Test reading from an index
ScopedFastFlag LuauTypecheckClassTypeIndexers("LuauTypecheckClassTypeIndexers", true);
{
CheckResult result = check(R"(
local x : IndexableClass
local y = x.stringKey
)");
LUAU_REQUIRE_NO_ERRORS(result);
}
{
CheckResult result = check(R"(
local x : IndexableClass
local y = x["stringKey"]
)");
LUAU_REQUIRE_NO_ERRORS(result);
}
{
CheckResult result = check(R"(
local x : IndexableClass
local str : string
local y = x[str] -- Index with a non-const string
)");
LUAU_REQUIRE_NO_ERRORS(result);
}
{
CheckResult result = check(R"(
local x : IndexableClass
local y = x[7] -- Index with a numeric key
)");
LUAU_REQUIRE_NO_ERRORS(result);
}
// Test writing to an index
{
CheckResult result = check(R"(
local x : IndexableClass
x.stringKey = 42
)");
LUAU_REQUIRE_NO_ERRORS(result);
}
{
CheckResult result = check(R"(
local x : IndexableClass
x["stringKey"] = 42
)");
LUAU_REQUIRE_NO_ERRORS(result);
}
{
CheckResult result = check(R"(
local x : IndexableClass
local str : string
x[str] = 42 -- Index with a non-const string
)");
LUAU_REQUIRE_NO_ERRORS(result);
}
{
CheckResult result = check(R"(
local x : IndexableClass
x[1] = 42 -- Index with a numeric key
)");
LUAU_REQUIRE_NO_ERRORS(result);
}
// Try to index the class using an invalid type for the key (key type is 'number | string'.)
{
CheckResult result = check(R"(
local x : IndexableClass
local y = x[true]
)");
CHECK_EQ(
toString(result.errors[0]), "Type 'boolean' could not be converted into 'number | string'; none of the union options are compatible");
}
{
CheckResult result = check(R"(
local x : IndexableClass
x[true] = 42
)");
CHECK_EQ(
toString(result.errors[0]), "Type 'boolean' could not be converted into 'number | string'; none of the union options are compatible");
}
// Test type checking for the return type of the indexer (i.e. a number)
{
CheckResult result = check(R"(
local x : IndexableClass
x.key = "string value"
)");
CHECK_EQ(toString(result.errors[0]), "Type 'string' could not be converted into 'number'");
}
{
CheckResult result = check(R"(
local x : IndexableClass
local str : string = x.key
)");
CHECK_EQ(toString(result.errors[0]), "Type 'number' could not be converted into 'string'");
}
// Check that we string key are rejected if the indexer's key type is not compatible with string
{
CheckResult result = check(R"(
local x : IndexableNumericKeyClass
x.key = 1
)");
CHECK_EQ(toString(result.errors.at(0)), "Key 'key' not found in class 'IndexableNumericKeyClass'");
}
{
CheckResult result = check(R"(
local x : IndexableNumericKeyClass
x["key"] = 1
)");
CHECK_EQ(toString(result.errors[0]), "Type 'string' could not be converted into 'number'");
}
{
CheckResult result = check(R"(
local x : IndexableNumericKeyClass
local str : string
x[str] = 1 -- Index with a non-const string
)");
CHECK_EQ(toString(result.errors[0]), "Type 'string' could not be converted into 'number'");
}
{
CheckResult result = check(R"(
local x : IndexableNumericKeyClass
local y = x.key
)");
CHECK_EQ(toString(result.errors[0]), "Key 'key' not found in class 'IndexableNumericKeyClass'");
}
{
CheckResult result = check(R"(
local x : IndexableNumericKeyClass
local y = x["key"]
)");
CHECK_EQ(toString(result.errors[0]), "Type 'string' could not be converted into 'number'");
}
{
CheckResult result = check(R"(
local x : IndexableNumericKeyClass
local str : string
local y = x[str] -- Index with a non-const string
)");
CHECK_EQ(toString(result.errors[0]), "Type 'string' could not be converted into 'number'");
}
}
TEST_SUITE_END();

View file

@ -1952,4 +1952,40 @@ TEST_CASE_FIXTURE(Fixture, "instantiated_type_packs_must_have_a_non_null_scope")
LUAU_REQUIRE_NO_ERRORS(result);
}
TEST_CASE_FIXTURE(Fixture, "inner_frees_become_generic_in_dcr")
{
if (!FFlag::DebugLuauDeferredConstraintResolution)
return;
CheckResult result = check(R"(
function f(x)
local z = x
return x
end
)");
LUAU_REQUIRE_NO_ERRORS(result);
std::optional<TypeId> ty = findTypeAtPosition(Position{3, 19});
REQUIRE(ty);
CHECK(get<GenericType>(*ty));
}
TEST_CASE_FIXTURE(Fixture, "function_exprs_are_generalized_at_signature_scope_not_enclosing")
{
CheckResult result = check(R"(
local foo
local bar
-- foo being a function expression is deliberate: the bug we're testing
-- only existed for function expressions, not for function statements.
foo = function(a)
return bar
end
)");
LUAU_REQUIRE_NO_ERRORS(result);
// note that b is not in the generic list; it is free, the unconstrained type of `bar`.
CHECK(toString(requireType("foo")) == "<a>(a) -> b");
}
TEST_SUITE_END();

View file

@ -550,6 +550,8 @@ TEST_CASE_FIXTURE(Fixture, "intersection_of_tables")
TEST_CASE_FIXTURE(Fixture, "intersection_of_tables_with_top_properties")
{
ScopedFastFlag sff{"LuauUnifyTwoOptions", true};
CheckResult result = check(R"(
local x : { p : number?, q : any } & { p : unknown, q : string? }
local y : { p : number?, q : string? } = x -- OK
@ -563,27 +565,19 @@ TEST_CASE_FIXTURE(Fixture, "intersection_of_tables_with_top_properties")
CHECK_EQ(toString(result.errors[0]),
"Type '{| p: number?, q: string? |}' could not be converted into '{| p: string?, q: number? |}'\n"
"caused by:\n"
" Property 'p' is not compatible. Type 'number?' could not be converted into 'string?'\n"
"caused by:\n"
" Not all union options are compatible. Type 'number' could not be converted into 'string?'\n"
"caused by:\n"
" None of the union options are compatible. For example: Type 'number' could not be converted into 'string' in an invariant context");
" Property 'p' is not compatible. Type 'number' could not be converted into 'string' in an invariant context");
CHECK_EQ(toString(result.errors[1]),
"Type '{| p: number?, q: string? |}' could not be converted into '{| p: string?, q: number? |}'\n"
"caused by:\n"
" Property 'q' is not compatible. Type 'string?' could not be converted into 'number?'\n"
"caused by:\n"
" Not all union options are compatible. Type 'string' could not be converted into 'number?'\n"
"caused by:\n"
" None of the union options are compatible. For example: Type 'string' could not be converted into 'number' in an invariant context");
" Property 'q' is not compatible. Type 'string' could not be converted into 'number' in an invariant context");
}
else
{
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");
"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");
}
}

View file

@ -1134,7 +1134,7 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "luau_polyfill_is_array_simplified")
return false
end
return true
end
end
)");
LUAU_REQUIRE_NO_ERRORS(result);
@ -1179,4 +1179,30 @@ end
LUAU_REQUIRE_NO_ERRORS(result);
}
TEST_CASE_FIXTURE(BuiltinsFixture, "luau-polyfill.Array.startswith")
{
// This test also exercises whether the binary operator == passes the correct expected type
// to it's l,r operands
CheckResult result = check(R"(
--!strict
local function startsWith(value: string, substring: string, position: number?): boolean
-- Luau FIXME: we have to use a tmp variable, as Luau doesn't understand the logic below narrow position to `number`
local position_
if position == nil or position < 1 then
position_ = 1
else
position_ = position
end
return value:find(substring, position_, true) == position_
end
return startsWith
)");
LUAU_REQUIRE_NO_ERRORS(result);
}
TEST_SUITE_END();

View file

@ -482,41 +482,6 @@ TEST_CASE_FIXTURE(Fixture, "dcr_can_partially_dispatch_a_constraint")
CHECK("<a>(a, number) -> ()" == toString(requireType("prime_iter")));
}
TEST_CASE_FIXTURE(Fixture, "free_options_cannot_be_unified_together")
{
ScopedFastFlag sff[] = {
{"LuauTransitiveSubtyping", true},
};
TypeArena arena;
TypeId nilType = builtinTypes->nilType;
std::unique_ptr scope = std::make_unique<Scope>(builtinTypes->anyTypePack);
TypeId free1 = arena.addType(FreeType{scope.get()});
TypeId option1 = arena.addType(UnionType{{nilType, free1}});
TypeId free2 = arena.addType(FreeType{scope.get()});
TypeId option2 = arena.addType(UnionType{{nilType, free2}});
InternalErrorReporter iceHandler;
UnifierSharedState sharedState{&iceHandler};
Normalizer normalizer{&arena, builtinTypes, NotNull{&sharedState}};
Unifier u{NotNull{&normalizer}, Mode::Strict, NotNull{scope.get()}, Location{}, Variance::Covariant};
u.tryUnify(option1, option2);
CHECK(!u.failure);
u.log.commit();
ToStringOptions opts;
CHECK("a?" == toString(option1, opts));
// CHECK("a?" == toString(option2, opts)); // This should hold, but does not.
CHECK("b?" == toString(option2, opts)); // This should not hold.
}
TEST_CASE_FIXTURE(BuiltinsFixture, "for_in_loop_with_zero_iterators")
{
ScopedFastFlag sff{"DebugLuauDeferredConstraintResolution", false};

View file

@ -390,6 +390,8 @@ TEST_CASE_FIXTURE(Fixture, "widen_the_supertype_if_it_is_free_and_subtype_has_si
TEST_CASE_FIXTURE(Fixture, "return_type_of_f_is_not_widened")
{
ScopedFastFlag sff{"LuauUnifyTwoOptions", true};
CheckResult result = check(R"(
local function foo(f, x): "hello"? -- anyone there?
return if x == "hi"
@ -401,7 +403,9 @@ TEST_CASE_FIXTURE(Fixture, "return_type_of_f_is_not_widened")
LUAU_REQUIRE_NO_ERRORS(result);
CHECK_EQ(R"("hi")", toString(requireTypeAtPosition({3, 23})));
CHECK_EQ(R"(<a, b, c...>((string) -> (a, c...), b) -> "hello"?)", toString(requireType("foo")));
CHECK_EQ(R"(<a, b...>((string) -> ("hello", b...), a) -> "hello"?)", toString(requireType("foo")));
// This is more accurate but we're not there yet:
// CHECK_EQ(R"(<a, b...>((string) -> ("hello"?, b...), a) -> "hello"?)", toString(requireType("foo")));
}

View file

@ -1233,4 +1233,69 @@ TEST_CASE_FIXTURE(Fixture, "dcr_delays_expansion_of_function_containing_blocked_
)");
}
TEST_CASE_FIXTURE(BuiltinsFixture, "recursive_function_that_invokes_itself_with_a_refinement_of_its_parameter")
{
CheckResult result = check(R"(
local TRUE: true = true
local function matches(value, t: true)
if value then
return true
end
end
local function readValue(breakpoint)
if matches(breakpoint, TRUE) then
readValue(breakpoint)
end
end
)");
CHECK("<a>(a) -> ()" == toString(requireType("readValue")));
}
TEST_CASE_FIXTURE(BuiltinsFixture, "recursive_function_that_invokes_itself_with_a_refinement_of_its_parameter_2")
{
CheckResult result = check(R"(
local function readValue(breakpoint)
if type(breakpoint) == 'number' then
readValue(breakpoint)
end
end
)");
CHECK("(number) -> ()" == toString(requireType("readValue")));
}
/*
* We got into a case where, as we unified two nearly identical unions with one
* another, where we had a concatenated TxnLog that created a cycle between two
* free types.
*
* This code used to crash the type checker. See CLI-71190
*/
TEST_CASE_FIXTURE(BuiltinsFixture, "convoluted_case_where_two_TypeVars_were_bound_to_each_other")
{
check(R"(
type React_Ref<ElementType> = { current: ElementType } | ((ElementType) -> ())
type React_AbstractComponent<Config, Instance> = {
render: ((ref: React_Ref<Instance>) -> nil)
}
local createElement : <P, T>(React_AbstractComponent<P, T>) -> ()
function ScrollView:render()
local one = table.unpack(
if true then a else b
)
createElement(one)
createElement(one)
end
)");
// If this code does not crash, we are in good shape.
}
TEST_SUITE_END();

View file

@ -409,4 +409,91 @@ local l0:(any)&(typeof(_)),l0:(any)|(any) = _,_
LUAU_REQUIRE_ERRORS(result);
}
static TypeId createTheType(TypeArena& arena, NotNull<BuiltinTypes> builtinTypes, Scope* scope, TypeId freeTy)
{
/*
({|
render: (
(('a) -> ()) | {| current: 'a |}
) -> nil
|}) -> ()
*/
TypePackId emptyPack = arena.addTypePack({});
return arena.addType(FunctionType{
arena.addTypePack({arena.addType(TableType{
TableType::Props{{{"render",
Property(arena.addType(FunctionType{
arena.addTypePack({arena.addType(UnionType{{arena.addType(FunctionType{arena.addTypePack({freeTy}), emptyPack}),
arena.addType(TableType{TableType::Props{{"current", {freeTy}}}, std::nullopt, TypeLevel{}, scope, TableState::Sealed})}})}),
arena.addTypePack({builtinTypes->nilType})}))}}},
std::nullopt, TypeLevel{}, scope, TableState::Sealed})}),
emptyPack});
};
// See CLI-71190
TEST_CASE_FIXTURE(TryUnifyFixture, "unifying_two_unions_under_dcr_does_not_create_a_BoundType_cycle")
{
const std::shared_ptr<Scope> scope = globalScope;
const std::shared_ptr<Scope> nestedScope = std::make_shared<Scope>(scope);
const TypeId outerType = arena.freshType(scope.get());
const TypeId outerType2 = arena.freshType(scope.get());
const TypeId innerType = arena.freshType(nestedScope.get());
ScopedFastFlag sff{"DebugLuauDeferredConstraintResolution", true};
state.enableScopeTests();
SUBCASE("equal_scopes")
{
TypeId one = createTheType(arena, builtinTypes, scope.get(), outerType);
TypeId two = createTheType(arena, builtinTypes, scope.get(), outerType2);
state.tryUnify(one, two);
state.log.commit();
ToStringOptions opts;
CHECK(follow(outerType) == follow(outerType2));
}
SUBCASE("outer_scope_is_subtype")
{
TypeId one = createTheType(arena, builtinTypes, scope.get(), outerType);
TypeId two = createTheType(arena, builtinTypes, scope.get(), innerType);
state.tryUnify(one, two);
state.log.commit();
ToStringOptions opts;
CHECK(follow(outerType) == follow(innerType));
// The scope of outerType exceeds that of innerType. The latter should be bound to the former.
const BoundType* bt = get_if<BoundType>(&innerType->ty);
REQUIRE(bt);
CHECK(bt->boundTo == outerType);
}
SUBCASE("outer_scope_is_supertype")
{
TypeId one = createTheType(arena, builtinTypes, scope.get(), innerType);
TypeId two = createTheType(arena, builtinTypes, scope.get(), outerType);
state.tryUnify(one, two);
state.log.commit();
ToStringOptions opts;
CHECK(follow(outerType) == follow(innerType));
// The scope of outerType exceeds that of innerType. The latter should be bound to the former.
const BoundType* bt = get_if<BoundType>(&innerType->ty);
REQUIRE(bt);
CHECK(bt->boundTo == outerType);
}
}
TEST_SUITE_END();

View file

@ -792,4 +792,82 @@ TEST_CASE_FIXTURE(Fixture, "lookup_prop_of_intersection_containing_unions")
CHECK("variables" == unknownProp->key);
}
TEST_CASE_FIXTURE(Fixture, "free_options_can_be_unified_together")
{
ScopedFastFlag sff[] = {
{"LuauTransitiveSubtyping", true},
{"LuauUnifyTwoOptions", true}
};
TypeArena arena;
TypeId nilType = builtinTypes->nilType;
std::unique_ptr scope = std::make_unique<Scope>(builtinTypes->anyTypePack);
TypeId free1 = arena.addType(FreeType{scope.get()});
TypeId option1 = arena.addType(UnionType{{nilType, free1}});
TypeId free2 = arena.addType(FreeType{scope.get()});
TypeId option2 = arena.addType(UnionType{{nilType, free2}});
InternalErrorReporter iceHandler;
UnifierSharedState sharedState{&iceHandler};
Normalizer normalizer{&arena, builtinTypes, NotNull{&sharedState}};
Unifier u{NotNull{&normalizer}, Mode::Strict, NotNull{scope.get()}, Location{}, Variance::Covariant};
u.tryUnify(option1, option2);
CHECK(!u.failure);
u.log.commit();
ToStringOptions opts;
CHECK("a?" == toString(option1, opts));
CHECK("a?" == toString(option2, opts));
}
TEST_CASE_FIXTURE(Fixture, "unify_more_complex_unions_that_include_nil")
{
CheckResult result = check(R"(
type Record = {prop: (string | boolean)?}
function concatPagination(prop: (string | boolean | nil)?): Record
return {prop = prop}
end
)");
LUAU_REQUIRE_NO_ERRORS(result);
}
TEST_CASE_FIXTURE(Fixture, "optional_class_instances_are_invariant")
{
ScopedFastFlag sff[] = {
{"LuauUnifyTwoOptions", true},
{"LuauTypeMismatchInvarianceInError", true}
};
createSomeClasses(&frontend);
CheckResult result = check(R"(
function foo(ref: {current: Parent?})
end
function bar(ref: {current: Child?})
foo(ref)
end
)");
LUAU_REQUIRE_ERROR_COUNT(1, result);
// The last line of this error is the most important part. We need to
// communicate that this is an invariant context.
std::string expectedError =
"Type '{| current: Child? |}' could not be converted into '{| current: Parent? |}'\n"
"caused by:\n"
" Property 'current' is not compatible. Type 'Child' could not be converted into 'Parent' in an invariant context"
;
CHECK(expectedError == toString(result.errors[0]));
}
TEST_SUITE_END();

View file

@ -69,4 +69,17 @@ end
breakpointSetFromMetamethod()
-- break inside function with non-monotonic line info
local function cond(a)
if a then
print('a')
else
print('not a')
end
end
breakpoint(77)
pcall(cond, nil) -- prevent inlining
return 'OK'

View file

@ -1,5 +1,6 @@
AnnotationTests.too_many_type_params
AstQuery.last_argument_function_call_type
AutocompleteTest.autocomplete_response_perf1
BuiltinTests.aliased_string_format
BuiltinTests.assert_removes_falsy_types
BuiltinTests.assert_removes_falsy_types2
@ -34,7 +35,6 @@ GenericsTests.generic_functions_should_be_memory_safe
GenericsTests.generic_type_pack_parentheses
GenericsTests.higher_rank_polymorphism_should_not_accept_instantiated_arguments
GenericsTests.infer_generic_function_function_argument_2
GenericsTests.infer_generic_function_function_argument_3
GenericsTests.infer_generic_function_function_argument_overloaded
GenericsTests.infer_generic_lib_function_function_argument
GenericsTests.instantiated_function_argument_names
@ -47,7 +47,6 @@ ProvisionalTests.assign_table_with_refined_property_with_a_similar_type_is_illeg
ProvisionalTests.bail_early_if_unification_is_too_complicated
ProvisionalTests.do_not_ice_when_trying_to_pick_first_of_generic_type_pack
ProvisionalTests.error_on_eq_metamethod_returning_a_type_other_than_boolean
ProvisionalTests.free_options_cannot_be_unified_together
ProvisionalTests.generic_type_leak_to_module_interface_variadic
ProvisionalTests.greedy_inference_with_shared_self_triggers_function_with_no_returns
ProvisionalTests.luau-polyfill.Array.filter
@ -60,7 +59,6 @@ RefinementTest.type_narrow_to_vector
RefinementTest.typeguard_cast_free_table_to_vector
RefinementTest.typeguard_in_assert_position
RefinementTest.x_as_any_if_x_is_instance_elseif_x_is_table
RuntimeLimits.typescript_port_of_Result_type
TableTests.a_free_shape_cannot_turn_into_a_scalar_if_it_is_not_compatible
TableTests.checked_prop_too_early
TableTests.disallow_indexing_into_an_unsealed_table_with_no_indexer_in_strict_mode
@ -124,6 +122,7 @@ TypeAliases.type_alias_local_mutation
TypeAliases.type_alias_local_rename
TypeAliases.type_alias_locations
TypeAliases.type_alias_of_an_imported_recursive_generic_type
TypeFamilyTests.function_internal_families
TypeInfer.check_type_infer_recursion_count
TypeInfer.cli_50041_committing_txnlog_in_apollo_client_error
TypeInfer.dont_report_type_errors_within_an_AstExprError
@ -133,6 +132,7 @@ TypeInfer.fuzz_free_table_type_change_during_index_check
TypeInfer.infer_assignment_value_types_mutable_lval
TypeInfer.no_stack_overflow_from_isoptional
TypeInfer.no_stack_overflow_from_isoptional2
TypeInfer.recursive_function_that_invokes_itself_with_a_refinement_of_its_parameter_2
TypeInfer.tc_after_error_recovery_no_replacement_name_in_error
TypeInfer.type_infer_recursion_limit_no_ice
TypeInfer.type_infer_recursion_limit_normalizer
@ -165,9 +165,7 @@ TypeInferFunctions.too_many_return_values_no_function
TypeInferLoops.for_in_loop_error_on_factory_not_returning_the_right_amount_of_values
TypeInferLoops.for_in_loop_with_next
TypeInferLoops.for_in_with_generic_next
TypeInferLoops.loop_iter_metamethod_ok_with_inference
TypeInferLoops.loop_iter_trailing_nil
TypeInferLoops.properly_infer_iteratee_is_a_free_table
TypeInferLoops.unreachable_code_after_infinite_loop
TypeInferModules.do_not_modify_imported_types_5
TypeInferModules.module_type_conflict
@ -177,7 +175,6 @@ TypeInferOOP.methods_are_topologically_sorted
TypeInferOperators.CallAndOrOfFunctions
TypeInferOperators.CallOrOfFunctions
TypeInferOperators.cli_38355_recursive_union
TypeInferOperators.compound_assign_metatable
TypeInferOperators.compound_assign_mismatch_metatable
TypeInferOperators.disallow_string_and_types_without_metatables_from_arithmetic_binary_ops
TypeInferOperators.operator_eq_completely_incompatible