Sync to upstream/release/628 (#1278)

### What's new?

* Remove a case of unsound `table.move` optimization
* Add Luau stack slot reservations that were missing in REPL (fixes
#1273)

### New Type Solver

* Assignments have been completely reworked to fix a case of cyclic
constraint dependency
* When indexing, if the fresh type's upper bound already contains a
compatible indexer, do not add another upper bound
* Distribute type arguments over all type families sans `eq`, `keyof`,
`rawkeyof`, and other internal type families
* Fix a case where `buffers` component weren't read in two places (fixes
#1267)
* Fix a case where things that constitutes a strong ref were slightly
incorrect
* Fix a case where constraint dependencies weren't setup wrt `for ...
in` statement

### Native Codegen

* Fix an optimization that splits TValue store only when its value and
its tag are compatible
* Implement a system to plug additional type information for custom host
userdata types

---

### Internal Contributors

Co-authored-by: Aaron Weiss <aaronweiss@roblox.com>
Co-authored-by: Alexander McCord <amccord@roblox.com>
Co-authored-by: Andy Friesen <afriesen@roblox.com>
Co-authored-by: Vighnesh Vijay <vvijay@roblox.com>
Co-authored-by: Vyacheslav Egorov <vegorov@roblox.com>

---------

Co-authored-by: Aaron Weiss <aaronweiss@roblox.com>
Co-authored-by: Andy Friesen <afriesen@roblox.com>
Co-authored-by: Vighnesh <vvijay@roblox.com>
Co-authored-by: Aviral Goel <agoel@roblox.com>
Co-authored-by: David Cope <dcope@roblox.com>
Co-authored-by: Lily Brown <lbrown@roblox.com>
Co-authored-by: Vyacheslav Egorov <vegorov@roblox.com>
This commit is contained in:
Alexander McCord 2024-05-31 12:18:18 -07:00 committed by GitHub
parent c8fe77c268
commit daf79328fc
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
62 changed files with 1986 additions and 1213 deletions

View File

@ -179,23 +179,6 @@ struct HasPropConstraint
bool suppressSimplification = false;
};
// result ~ setProp subjectType ["prop", "prop2", ...] propType
//
// If the subject is a table or table-like thing that already has the named
// property chain, we unify propType with that existing property type.
//
// If the subject is a free table, we augment it in place.
//
// If the subject is an unsealed table, result is an augmented table that
// includes that new prop.
struct SetPropConstraint
{
TypeId resultType;
TypeId subjectType;
std::vector<std::string> path;
TypeId propType;
};
// resultType ~ hasIndexer subjectType indexType
//
// If the subject type is a table or table-like thing that supports indexing,
@ -209,16 +192,37 @@ struct HasIndexerConstraint
TypeId indexType;
};
// result ~ setIndexer subjectType indexType propType
//
// If the subject is a table or table-like thing that already has an indexer,
// unify its indexType and propType with those from this constraint.
//
// If the table is a free or unsealed table, we augment it with a new indexer.
struct SetIndexerConstraint
struct AssignConstraint
{
TypeId subjectType;
TypeId lhsType;
TypeId rhsType;
};
// assign lhsType propName rhsType
//
// Assign a value of type rhsType into the named property of lhsType.
struct AssignPropConstraint
{
TypeId lhsType;
std::string propName;
TypeId rhsType;
/// The canonical write type of the property. It is _solely_ used to
/// populate astTypes during constraint resolution. Nothing should ever
/// block on it.
TypeId propType;
};
struct AssignIndexConstraint
{
TypeId lhsType;
TypeId indexType;
TypeId rhsType;
/// The canonical write type of the property. It is _solely_ used to
/// populate astTypes during constraint resolution. Nothing should ever
/// block on it.
TypeId propType;
};
@ -230,25 +234,6 @@ struct UnpackConstraint
{
TypePackId resultPack;
TypePackId sourcePack;
// UnpackConstraint is sometimes used to resolve the types of assignments.
// When this is the case, any LocalTypes in resultPack can have their
// domains extended by the corresponding type from sourcePack.
bool resultIsLValue = false;
};
// resultType ~ unpack sourceType
//
// The same as UnpackConstraint, but specialized for a pair of types as opposed to packs.
struct Unpack1Constraint
{
TypeId resultType;
TypeId sourceType;
// UnpackConstraint is sometimes used to resolve the types of assignments.
// When this is the case, any LocalTypes in resultPack can have their
// domains extended by the corresponding type from sourcePack.
bool resultIsLValue = false;
};
// ty ~ reduce ty
@ -268,8 +253,8 @@ struct ReducePackConstraint
};
using ConstraintV = Variant<SubtypeConstraint, PackSubtypeConstraint, GeneralizationConstraint, IterableConstraint, NameConstraint,
TypeAliasExpansionConstraint, FunctionCallConstraint, FunctionCheckConstraint, PrimitiveTypeConstraint, HasPropConstraint, SetPropConstraint,
HasIndexerConstraint, SetIndexerConstraint, UnpackConstraint, Unpack1Constraint, ReduceConstraint, ReducePackConstraint, EqualityConstraint>;
TypeAliasExpansionConstraint, FunctionCallConstraint, FunctionCheckConstraint, PrimitiveTypeConstraint, HasPropConstraint, HasIndexerConstraint,
AssignConstraint, AssignPropConstraint, AssignIndexConstraint, UnpackConstraint, ReduceConstraint, ReducePackConstraint, EqualityConstraint>;
struct Constraint
{

View File

@ -254,18 +254,11 @@ private:
Inference check(const ScopePtr& scope, AstExprTable* expr, std::optional<TypeId> expectedType);
std::tuple<TypeId, TypeId, RefinementId> checkBinary(const ScopePtr& scope, AstExprBinary* binary, std::optional<TypeId> expectedType);
struct LValueBounds
{
std::optional<TypeId> annotationTy;
std::optional<TypeId> assignedTy;
};
LValueBounds checkLValue(const ScopePtr& scope, AstExpr* expr);
LValueBounds checkLValue(const ScopePtr& scope, AstExprLocal* local);
LValueBounds checkLValue(const ScopePtr& scope, AstExprGlobal* global);
LValueBounds checkLValue(const ScopePtr& scope, AstExprIndexName* indexName);
LValueBounds checkLValue(const ScopePtr& scope, AstExprIndexExpr* indexExpr);
LValueBounds updateProperty(const ScopePtr& scope, AstExpr* expr);
void visitLValue(const ScopePtr& scope, AstExpr* expr, TypeId rhsType);
void visitLValue(const ScopePtr& scope, AstExprLocal* local, TypeId rhsType);
void visitLValue(const ScopePtr& scope, AstExprGlobal* global, TypeId rhsType);
void visitLValue(const ScopePtr& scope, AstExprIndexName* indexName, TypeId rhsType);
void visitLValue(const ScopePtr& scope, AstExprIndexExpr* indexExpr, TypeId rhsType);
struct FunctionSignature
{

View File

@ -134,7 +134,6 @@ struct ConstraintSolver
bool tryDispatch(const FunctionCheckConstraint& c, NotNull<const Constraint> constraint);
bool tryDispatch(const PrimitiveTypeConstraint& c, NotNull<const Constraint> constraint);
bool tryDispatch(const HasPropConstraint& c, NotNull<const Constraint> constraint);
bool tryDispatch(const SetPropConstraint& c, NotNull<const Constraint> constraint);
bool tryDispatchHasIndexer(
int& recursionDepth, NotNull<const Constraint> constraint, TypeId subjectType, TypeId indexType, TypeId resultType, Set<TypeId>& seen);
@ -142,11 +141,13 @@ struct ConstraintSolver
std::pair<bool, std::optional<TypeId>> tryDispatchSetIndexer(
NotNull<const Constraint> constraint, TypeId subjectType, TypeId indexType, TypeId propType, bool expandFreeTypeBounds);
bool tryDispatch(const SetIndexerConstraint& c, NotNull<const Constraint> constraint, bool force);
bool tryDispatchUnpack1(NotNull<const Constraint> constraint, TypeId resultType, TypeId sourceType, bool resultIsLValue);
bool tryDispatch(const AssignConstraint& c, NotNull<const Constraint> constraint);
bool tryDispatch(const AssignPropConstraint& c, NotNull<const Constraint> constraint);
bool tryDispatch(const AssignIndexConstraint& c, NotNull<const Constraint> constraint);
bool tryDispatchUnpack1(NotNull<const Constraint> constraint, TypeId resultType, TypeId sourceType);
bool tryDispatch(const UnpackConstraint& c, NotNull<const Constraint> constraint);
bool tryDispatch(const Unpack1Constraint& 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);
@ -165,6 +166,17 @@ struct ConstraintSolver
std::pair<std::vector<TypeId>, std::optional<TypeId>> lookupTableProp(NotNull<const Constraint> constraint, TypeId subjectType,
const std::string& propName, ValueContext context, bool inConditional, bool suppressSimplification, DenseHashSet<TypeId>& seen);
/**
* Generate constraints to unpack the types of srcTypes and assign each
* value to the corresponding LocalType in destTypes.
*
* @param destTypes A finite TypePack comprised of LocalTypes.
* @param srcTypes A TypePack that represents rvalues to be assigned.
* @returns The underlying UnpackConstraint. There's a bit of code in
* iteration that needs to pass blocks on to this constraint.
*/
NotNull<const Constraint> unpackAndAssign(TypePackId destTypes, TypePackId srcTypes, NotNull<const Constraint> constraint);
void block(NotNull<const Constraint> target, NotNull<const Constraint> constraint);
/**
* Block a constraint on the resolution of a Type.

View File

@ -5,6 +5,7 @@
#include "Luau/DenseHash.h"
#include "Luau/NotNull.h"
#include "Luau/TypeFwd.h"
#include <set>
namespace Luau
{
@ -19,6 +20,8 @@ struct SimplifyResult
};
SimplifyResult simplifyIntersection(NotNull<BuiltinTypes> builtinTypes, NotNull<TypeArena> arena, TypeId ty, TypeId discriminant);
SimplifyResult simplifyIntersection(NotNull<BuiltinTypes> builtinTypes, NotNull<TypeArena> arena, std::set<TypeId> parts);
SimplifyResult simplifyUnion(NotNull<BuiltinTypes> builtinTypes, NotNull<TypeArena> arena, TypeId ty, TypeId discriminant);
enum class Relation

View File

@ -6,7 +6,6 @@
#include "Luau/NotNull.h"
#include "Luau/TypeCheckLimits.h"
#include "Luau/TypeFwd.h"
#include "Luau/Variant.h"
#include <functional>
#include <string>
@ -19,22 +18,6 @@ struct TypeArena;
struct TxnLog;
class Normalizer;
struct TypeFamilyQueue
{
NotNull<VecDeque<TypeId>> queuedTys;
NotNull<VecDeque<TypePackId>> queuedTps;
void add(TypeId instanceTy);
void add(TypePackId instanceTp);
template<typename T>
void add(const std::vector<T>& ts)
{
for (const T& t : ts)
enqueue(t);
}
};
struct TypeFamilyContext
{
NotNull<TypeArena> arena;
@ -99,8 +82,8 @@ struct TypeFamilyReductionResult
};
template<typename T>
using ReducerFunction = std::function<TypeFamilyReductionResult<T>(
T, NotNull<TypeFamilyQueue>, const std::vector<TypeId>&, const std::vector<TypePackId>&, NotNull<TypeFamilyContext>)>;
using ReducerFunction = std::function<TypeFamilyReductionResult<T>(T, const std::vector<TypeId>&, const std::vector<TypePackId>&,
NotNull<TypeFamilyContext>)>;
/// Represents a type function that may be applied to map a series of types and
/// type packs to a single output type.

View File

@ -55,6 +55,9 @@ struct InConditionalContext
using ScopePtr = std::shared_ptr<struct Scope>;
std::optional<Property> findTableProperty(
NotNull<BuiltinTypes> builtinTypes, ErrorVec& errors, TypeId ty, const std::string& name, Location location);
std::optional<TypeId> findMetatableEntry(
NotNull<BuiltinTypes> builtinTypes, ErrorVec& errors, TypeId type, const std::string& entry, Location location);
std::optional<TypeId> findTablePropertyRespectingMeta(

View File

@ -56,6 +56,11 @@ bool isReferenceCountedType(const TypeId typ)
DenseHashSet<TypeId> Constraint::getMaybeMutatedFreeTypes() const
{
// For the purpose of this function and reference counting in general, we are only considering
// mutations that affect the _bounds_ of the free type, and not something that may bind the free
// type itself to a new type. As such, `ReduceConstraint` and `GeneralizationConstraint` have no
// contribution to the output set here.
DenseHashSet<TypeId> types{{}};
ReferenceCountInitializer rci{&types};
@ -74,11 +79,6 @@ DenseHashSet<TypeId> Constraint::getMaybeMutatedFreeTypes() const
rci.traverse(psc->subPack);
rci.traverse(psc->superPack);
}
else if (auto gc = get<GeneralizationConstraint>(*this))
{
rci.traverse(gc->generalizedType);
// `GeneralizationConstraints` should not mutate `sourceType` or `interiorTypes`.
}
else if (auto itc = get<IterableConstraint>(*this))
{
rci.traverse(itc->variables);
@ -101,36 +101,32 @@ DenseHashSet<TypeId> Constraint::getMaybeMutatedFreeTypes() const
rci.traverse(hpc->resultType);
// `HasPropConstraints` should not mutate `subjectType`.
}
else if (auto spc = get<SetPropConstraint>(*this))
{
rci.traverse(spc->resultType);
// `SetPropConstraints` should not mutate `subjectType` or `propType`.
// TODO: is this true? it "unifies" with `propType`, so maybe mutates that one too?
}
else if (auto hic = get<HasIndexerConstraint>(*this))
{
rci.traverse(hic->resultType);
// `HasIndexerConstraint` should not mutate `subjectType` or `indexType`.
}
else if (auto sic = get<SetIndexerConstraint>(*this))
else if (auto ac = get<AssignConstraint>(*this))
{
rci.traverse(sic->propType);
// `SetIndexerConstraints` should not mutate `subjectType` or `indexType`.
rci.traverse(ac->lhsType);
rci.traverse(ac->rhsType);
}
else if (auto apc = get<AssignPropConstraint>(*this))
{
rci.traverse(apc->lhsType);
rci.traverse(apc->rhsType);
}
else if (auto aic = get<AssignIndexConstraint>(*this))
{
rci.traverse(aic->lhsType);
rci.traverse(aic->indexType);
rci.traverse(aic->rhsType);
}
else if (auto uc = get<UnpackConstraint>(*this))
{
rci.traverse(uc->resultPack);
// `UnpackConstraint` should not mutate `sourcePack`.
}
else if (auto u1c = get<Unpack1Constraint>(*this))
{
rci.traverse(u1c->resultType);
// `Unpack1Constraint` should not mutate `sourceType`.
}
else if (auto rc = get<ReduceConstraint>(*this))
{
rci.traverse(rc->ty);
}
else if (auto rpc = get<ReducePackConstraint>(*this))
{
rci.traverse(rpc->tp);

View File

@ -310,7 +310,7 @@ std::optional<TypeId> ConstraintGenerator::lookup(const ScopePtr& scope, Locatio
std::optional<TypeId> ty = lookup(scope, location, operand, /*prototype*/ false);
if (!ty)
{
ty = arena->addType(BlockedType{});
ty = arena->addType(LocalType{builtinTypes->neverType});
rootScope->lvalueTypes[operand] = *ty;
}
@ -739,12 +739,28 @@ ControlFlow ConstraintGenerator::visit(const ScopePtr& scope, AstStatLocal* stat
if (hasAnnotation)
{
for (size_t i = 0; i < statLocal->vars.size; ++i)
addConstraint(scope, statLocal->location, AssignConstraint{assignees[i], annotatedTypes[i]});
TypePackId annotatedPack = arena->addTypePack(std::move(annotatedTypes));
addConstraint(scope, statLocal->location, UnpackConstraint{arena->addTypePack(std::move(assignees)), annotatedPack, /*resultIsLValue*/ true});
addConstraint(scope, statLocal->location, PackSubtypeConstraint{rvaluePack, annotatedPack});
}
else
addConstraint(scope, statLocal->location, UnpackConstraint{arena->addTypePack(std::move(assignees)), rvaluePack, /*resultIsLValue*/ true});
{
std::vector<TypeId> valueTypes;
valueTypes.reserve(statLocal->vars.size);
for (size_t i = 0; i < statLocal->vars.size; ++i)
valueTypes.push_back(arena->addType(BlockedType{}));
auto uc = addConstraint(scope, statLocal->location, UnpackConstraint{arena->addTypePack(valueTypes), rvaluePack});
for (size_t i = 0; i < statLocal->vars.size; ++i)
{
getMutable<BlockedType>(valueTypes[i])->setOwner(uc);
addConstraint(scope, statLocal->location, AssignConstraint{assignees[i], valueTypes[i]});
}
}
if (statLocal->vars.size == 1 && statLocal->values.size == 1 && firstValueType && scope.get() == rootScope && !hasAnnotation)
{
@ -837,7 +853,6 @@ ControlFlow ConstraintGenerator::visit(const ScopePtr& scope, AstStatFor* for_)
ControlFlow ConstraintGenerator::visit(const ScopePtr& scope, AstStatForIn* forIn)
{
ScopePtr loopScope = childScope(forIn, scope);
TypePackId iterator = checkPack(scope, forIn->values).tp;
std::vector<TypeId> variableTypes;
@ -862,10 +877,17 @@ ControlFlow ConstraintGenerator::visit(const ScopePtr& scope, AstStatForIn* forI
}
TypePackId variablePack = arena->addTypePack(std::move(variableTypes));
addConstraint(
auto iterable = addConstraint(
loopScope, getLocation(forIn->values), IterableConstraint{iterator, variablePack, forIn->values.data[0], &module->astForInNextTypes});
Checkpoint start = checkpoint(this);
visit(loopScope, forIn->body);
Checkpoint end = checkpoint(this);
// This iter constraint must dispatch first.
forEachConstraint(start, end, this, [&iterable](const ConstraintPtr& runLater) {
runLater->dependencies.push_back(iterable);
});
return ControlFlow::None;
}
@ -957,67 +979,63 @@ ControlFlow ConstraintGenerator::visit(const ScopePtr& scope, AstStatFunction* f
// Name could be AstStatLocal, AstStatGlobal, AstStatIndexName.
// With or without self
TypeId generalizedType = arena->addType(BlockedType{});
Checkpoint start = checkpoint(this);
FunctionSignature sig = checkFunctionSignature(scope, function->func, /* expectedType */ std::nullopt, function->name->location);
bool sigFullyDefined = !hasFreeType(sig.signature);
checkFunctionBody(sig.bodyScope, function->func);
Checkpoint end = checkpoint(this);
TypeId generalizedType = arena->addType(BlockedType{});
if (sigFullyDefined)
emplaceType<BoundType>(asMutable(generalizedType), sig.signature);
else
{
const ScopePtr& constraintScope = sig.signatureScope ? sig.signatureScope : sig.bodyScope;
DenseHashSet<Constraint*> excludeList{nullptr};
NotNull<Constraint> c = addConstraint(constraintScope, function->name->location, GeneralizationConstraint{generalizedType, sig.signature});
getMutable<BlockedType>(generalizedType)->setOwner(c);
Constraint* previous = nullptr;
forEachConstraint(start, end, this, [&c, &previous](const ConstraintPtr& constraint) {
c->dependencies.push_back(NotNull{constraint.get()});
if (auto psc = get<PackSubtypeConstraint>(*constraint); psc && psc->returns)
{
if (previous)
constraint->dependencies.push_back(NotNull{previous});
previous = constraint.get();
}
});
}
DefId def = dfg->getDef(function->name);
std::optional<TypeId> existingFunctionTy = follow(lookup(scope, function->name->location, def));
if (get<BlockedType>(existingFunctionTy) && sigFullyDefined)
emplaceType<BoundType>(asMutable(*existingFunctionTy), sig.signature);
if (AstExprLocal* localName = function->name->as<AstExprLocal>())
{
if (existingFunctionTy)
{
addConstraint(scope, function->name->location, SubtypeConstraint{generalizedType, *existingFunctionTy});
Symbol sym{localName->local};
scope->bindings[sym].typeId = generalizedType;
}
else
scope->bindings[localName->local] = Binding{generalizedType, localName->location};
visitLValue(scope, localName, generalizedType);
scope->bindings[localName->local] = Binding{sig.signature, localName->location};
scope->lvalueTypes[def] = sig.signature;
scope->rvalueRefinements[def] = sig.signature;
}
else if (AstExprGlobal* globalName = function->name->as<AstExprGlobal>())
{
if (!existingFunctionTy)
ice->ice("prepopulateGlobalScope did not populate a global name", globalName->location);
if (!sigFullyDefined)
generalizedType = *existingFunctionTy;
// Sketchy: We're specifically looking for BlockedTypes that were
// initially created by ConstraintGenerator::prepopulateGlobalScope.
if (auto bt = get<BlockedType>(*existingFunctionTy); bt && nullptr == bt->getOwner())
emplaceType<BoundType>(asMutable(*existingFunctionTy), generalizedType);
scope->bindings[globalName->name] = Binding{sig.signature, globalName->location};
scope->lvalueTypes[def] = sig.signature;
scope->rvalueRefinements[def] = sig.signature;
}
else if (AstExprIndexName* indexName = function->name->as<AstExprIndexName>())
{
Checkpoint check1 = checkpoint(this);
auto [_, lvalueType] = checkLValue(scope, indexName);
Checkpoint check2 = checkpoint(this);
forEachConstraint(check1, check2, this, [&excludeList](const ConstraintPtr& c) {
excludeList.insert(c.get());
});
// TODO figure out how to populate the location field of the table Property.
if (lvalueType && *lvalueType != generalizedType)
{
LUAU_ASSERT(get<BlockedType>(lvalueType));
emplaceType<BoundType>(asMutable(*lvalueType), generalizedType);
}
visitLValue(scope, indexName, generalizedType);
}
else if (AstExprError* err = function->name->as<AstExprError>())
{
@ -1029,48 +1047,6 @@ ControlFlow ConstraintGenerator::visit(const ScopePtr& scope, AstStatFunction* f
scope->rvalueRefinements[def] = generalizedType;
checkFunctionBody(sig.bodyScope, function->func);
Checkpoint end = checkpoint(this);
if (!sigFullyDefined)
{
NotNull<Scope> constraintScope{sig.signatureScope ? sig.signatureScope.get() : sig.bodyScope.get()};
std::unique_ptr<Constraint> c =
std::make_unique<Constraint>(constraintScope, function->name->location, GeneralizationConstraint{generalizedType, sig.signature});
Constraint* previous = nullptr;
forEachConstraint(start, end, this, [&c, &excludeList, &previous](const ConstraintPtr& constraint) {
if (!excludeList.contains(constraint.get()))
c->dependencies.push_back(NotNull{constraint.get()});
if (auto psc = get<PackSubtypeConstraint>(*constraint); psc && psc->returns)
{
if (previous)
constraint->dependencies.push_back(NotNull{previous});
previous = constraint.get();
}
});
// We need to check if the blocked type has no owner here because
// if a function is defined twice anywhere in the program like:
// `function f() end` and then later like `function f() end`
// Then there will be exactly one definition in the scope for it because it's a global
// (this is the same as writing f = function() end)
// Therefore, when we visit() the multiple different expression of this global variable
// They will all be aliased to the same blocked type, which means we can create multiple constraints
// for the same blocked type.
if (auto blocked = getMutable<BlockedType>(generalizedType); blocked && !blocked->getOwner())
blocked->setOwner(addConstraint(scope, std::move(c)));
}
if (BlockedType* bt = getMutable<BlockedType>(follow(existingFunctionTy)); bt && !bt->getOwner())
{
auto uc = addConstraint(scope, function->name->location, Unpack1Constraint{*existingFunctionTy, generalizedType});
bt->setOwner(uc);
}
return ControlFlow::None;
}
@ -1124,38 +1100,20 @@ static void bindFreeType(TypeId a, TypeId b)
ControlFlow ConstraintGenerator::visit(const ScopePtr& scope, AstStatAssign* assign)
{
std::vector<TypeId> upperBounds;
upperBounds.reserve(assign->vars.size);
std::vector<TypeId> typeStates;
typeStates.reserve(assign->vars.size);
Checkpoint lvalueBeginCheckpoint = checkpoint(this);
for (AstExpr* lvalue : assign->vars)
{
auto [upperBound, typeState] = checkLValue(scope, lvalue);
upperBounds.push_back(upperBound.value_or(builtinTypes->unknownType));
typeStates.push_back(typeState.value_or(builtinTypes->unknownType));
}
Checkpoint lvalueEndCheckpoint = checkpoint(this);
TypePackId resultPack = checkPack(scope, assign->values).tp;
auto uc = addConstraint(scope, assign->location, UnpackConstraint{arena->addTypePack(typeStates), resultPack, /*resultIsLValue*/ true});
forEachConstraint(lvalueBeginCheckpoint, lvalueEndCheckpoint, this, [uc](const ConstraintPtr& constraint) {
uc->dependencies.push_back(NotNull{constraint.get()});
});
auto psc = addConstraint(scope, assign->location, PackSubtypeConstraint{resultPack, arena->addTypePack(std::move(upperBounds))});
psc->dependencies.push_back(uc);
std::vector<TypeId> valueTypes;
valueTypes.reserve(assign->vars.size);
for (TypeId assignee : typeStates)
for (size_t i = 0; i < assign->vars.size; ++i)
valueTypes.push_back(arena->addType(BlockedType{}));
auto uc = addConstraint(scope, assign->location, UnpackConstraint{arena->addTypePack(valueTypes), resultPack});
for (size_t i = 0; i < assign->vars.size; ++i)
{
auto blocked = getMutable<BlockedType>(assignee);
if (blocked && !blocked->getOwner())
blocked->setOwner(uc);
getMutable<BlockedType>(valueTypes[i])->setOwner(uc);
visitLValue(scope, assign->vars.data[i], valueTypes[i]);
}
return ControlFlow::None;
@ -1166,24 +1124,7 @@ ControlFlow ConstraintGenerator::visit(const ScopePtr& scope, AstStatCompoundAss
AstExprBinary binop = AstExprBinary{assign->location, assign->op, assign->var, assign->value};
TypeId resultTy = check(scope, &binop).ty;
auto [upperBound, typeState] = checkLValue(scope, assign->var);
Constraint* sc = nullptr;
if (upperBound)
sc = addConstraint(scope, assign->location, SubtypeConstraint{resultTy, *upperBound});
if (typeState)
{
NotNull<Constraint> uc = addConstraint(scope, assign->location, Unpack1Constraint{*typeState, resultTy, /*resultIsLValue=*/true});
if (auto blocked = getMutable<BlockedType>(*typeState); blocked && !blocked->getOwner())
blocked->setOwner(uc);
if (sc)
uc->dependencies.push_back(NotNull{sc});
}
DefId def = dfg->getDef(assign->var);
scope->lvalueTypes[def] = resultTy;
visitLValue(scope, assign->var, resultTy);
return ControlFlow::None;
}
@ -1897,7 +1838,8 @@ Inference ConstraintGenerator::check(const ScopePtr& scope, AstExprGlobal* globa
return Inference{builtinTypes->errorRecoveryType()};
}
Inference ConstraintGenerator::checkIndexName(const ScopePtr& scope, const RefinementKey* key, AstExpr* indexee, const std::string& index, Location indexLocation)
Inference ConstraintGenerator::checkIndexName(
const ScopePtr& scope, const RefinementKey* key, AstExpr* indexee, const std::string& index, Location indexLocation)
{
TypeId obj = check(scope, indexee).ty;
TypeId result = arena->addType(BlockedType{});
@ -2272,26 +2214,25 @@ std::tuple<TypeId, TypeId, RefinementId> ConstraintGenerator::checkBinary(
}
}
ConstraintGenerator::LValueBounds ConstraintGenerator::checkLValue(const ScopePtr& scope, AstExpr* expr)
void ConstraintGenerator::visitLValue(const ScopePtr& scope, AstExpr* expr, TypeId rhsType)
{
if (auto local = expr->as<AstExprLocal>())
return checkLValue(scope, local);
else if (auto global = expr->as<AstExprGlobal>())
return checkLValue(scope, global);
else if (auto indexName = expr->as<AstExprIndexName>())
return checkLValue(scope, indexName);
else if (auto indexExpr = expr->as<AstExprIndexExpr>())
return checkLValue(scope, indexExpr);
else if (auto error = expr->as<AstExprError>())
if (auto e = expr->as<AstExprLocal>())
visitLValue(scope, e, rhsType);
else if (auto e = expr->as<AstExprGlobal>())
visitLValue(scope, e, rhsType);
else if (auto e = expr->as<AstExprIndexName>())
visitLValue(scope, e, rhsType);
else if (auto e = expr->as<AstExprIndexExpr>())
visitLValue(scope, e, rhsType);
else if (auto e = expr->as<AstExprError>())
{
check(scope, error);
return {builtinTypes->errorRecoveryType(), builtinTypes->errorRecoveryType()};
// Nothing?
}
else
ice->ice("checkLValue is inexhaustive");
ice->ice("Unexpected lvalue expression", expr->location);
}
ConstraintGenerator::LValueBounds ConstraintGenerator::checkLValue(const ScopePtr& scope, AstExprLocal* local)
void ConstraintGenerator::visitLValue(const ScopePtr& scope, AstExprLocal* local, TypeId rhsType)
{
std::optional<TypeId> annotatedTy = scope->lookup(local->local);
LUAU_ASSERT(annotatedTy);
@ -2332,186 +2273,53 @@ ConstraintGenerator::LValueBounds ConstraintGenerator::checkLValue(const ScopePt
scope->lvalueTypes[defId] = *ty;
}
// TODO: Need to clip this, but this requires more code to be reworked first before we can clip this.
std::optional<TypeId> assignedTy = arena->addType(BlockedType{});
auto unpackC = addConstraint(scope, local->location, Unpack1Constraint{*ty, *assignedTy, /*resultIsLValue*/ true});
if (auto blocked = get<BlockedType>(*ty))
{
if (blocked->getOwner())
unpackC->dependencies.push_back(NotNull{blocked->getOwner()});
else if (auto blocked = getMutable<BlockedType>(*ty))
blocked->setOwner(unpackC);
}
recordInferredBinding(local->local, *ty);
return {annotatedTy, assignedTy};
if (annotatedTy)
addConstraint(scope, local->location, SubtypeConstraint{rhsType, *annotatedTy});
addConstraint(scope, local->location, AssignConstraint{*ty, rhsType});
}
ConstraintGenerator::LValueBounds ConstraintGenerator::checkLValue(const ScopePtr& scope, AstExprGlobal* global)
void ConstraintGenerator::visitLValue(const ScopePtr& scope, AstExprGlobal* global, TypeId rhsType)
{
std::optional<TypeId> annotatedTy = scope->lookup(Symbol{global->name});
if (annotatedTy)
{
DefId def = dfg->getDef(global);
TypeId assignedTy = arena->addType(BlockedType{});
rootScope->lvalueTypes[def] = assignedTy;
return {annotatedTy, assignedTy};
rootScope->lvalueTypes[def] = rhsType;
addConstraint(scope, global->location, SubtypeConstraint{rhsType, *annotatedTy});
addConstraint(scope, global->location, AssignConstraint{*annotatedTy, rhsType});
}
else
return {annotatedTy, std::nullopt};
}
ConstraintGenerator::LValueBounds ConstraintGenerator::checkLValue(const ScopePtr& scope, AstExprIndexName* indexName)
void ConstraintGenerator::visitLValue(const ScopePtr& scope, AstExprIndexName* expr, TypeId rhsType)
{
return updateProperty(scope, indexName);
TypeId lhsTy = check(scope, expr->expr).ty;
TypeId propTy = arena->addType(BlockedType{});
module->astTypes[expr] = propTy;
addConstraint(scope, expr->location, AssignPropConstraint{lhsTy, expr->index.value, rhsType, propTy});
}
ConstraintGenerator::LValueBounds ConstraintGenerator::checkLValue(const ScopePtr& scope, AstExprIndexExpr* indexExpr)
void ConstraintGenerator::visitLValue(const ScopePtr& scope, AstExprIndexExpr* expr, TypeId rhsType)
{
return updateProperty(scope, indexExpr);
}
/**
* This function is mostly about identifying properties that are being inserted into unsealed tables.
*
* If expr has the form name.a.b.c
*/
ConstraintGenerator::LValueBounds ConstraintGenerator::updateProperty(const ScopePtr& scope, AstExpr* expr)
{
// There are a bunch of cases where we realize that this is not the kind of
// assignment that potentially changes the shape of a table. When we
// encounter them, we call this to fall back and do the "usual thing."
auto fallback = [&]() -> LValueBounds {
TypeId resTy = check(scope, expr).ty;
return {resTy, std::nullopt};
};
LUAU_ASSERT(expr->is<AstExprIndexName>() || expr->is<AstExprIndexExpr>());
if (auto indexExpr = expr->as<AstExprIndexExpr>(); indexExpr && !indexExpr->index->is<AstExprConstantString>())
if (auto constantString = expr->index->as<AstExprConstantString>())
{
// An indexer is only interesting in an lvalue-ey way if it is at the
// tail of an expression.
//
// If the indexer is not at the tail, then we are not interested in
// augmenting the lhs data structure with a new indexer. Constraint
// generation can treat it as an ordinary lvalue.
//
// eg
//
// a.b.c[1] = 44 -- lvalue
// a.b[4].c = 2 -- rvalue
TypeId lhsTy = check(scope, expr->expr).ty;
TypeId propTy = arena->addType(BlockedType{});
module->astTypes[expr] = propTy;
module->astTypes[expr->index] = builtinTypes->stringType; // FIXME? Singleton strings exist.
std::string propName{constantString->value.data, constantString->value.size};
addConstraint(scope, expr->location, AssignPropConstraint{lhsTy, std::move(propName), rhsType, propTy});
TypeId subjectType = check(scope, indexExpr->expr).ty;
TypeId indexType = check(scope, indexExpr->index).ty;
TypeId assignedTy = arena->addType(BlockedType{});
auto sic = addConstraint(scope, expr->location, SetIndexerConstraint{subjectType, indexType, assignedTy});
getMutable<BlockedType>(assignedTy)->setOwner(sic);
module->astTypes[expr] = assignedTy;
return {assignedTy, assignedTy};
return;
}
Symbol sym;
const Def* def = nullptr;
std::vector<std::string> segments;
std::vector<AstExpr*> exprs;
AstExpr* e = expr;
while (e)
{
if (auto global = e->as<AstExprGlobal>())
{
sym = global->name;
def = dfg->getDef(global);
break;
}
else if (auto local = e->as<AstExprLocal>())
{
sym = local->local;
def = dfg->getDef(local);
break;
}
else if (auto indexName = e->as<AstExprIndexName>())
{
segments.push_back(indexName->index.value);
exprs.push_back(e);
e = indexName->expr;
}
else if (auto indexExpr = e->as<AstExprIndexExpr>())
{
if (auto strIndex = indexExpr->index->as<AstExprConstantString>())
{
// We need to populate astTypes for the index value.
check(scope, indexExpr->index);
segments.push_back(std::string(strIndex->value.data, strIndex->value.size));
exprs.push_back(e);
e = indexExpr->expr;
}
else
{
return fallback();
}
}
else
{
return fallback();
}
}
LUAU_ASSERT(!segments.empty());
std::reverse(begin(segments), end(segments));
std::reverse(begin(exprs), end(exprs));
LUAU_ASSERT(def);
std::optional<std::pair<TypeId, Scope*>> lookupResult = scope->lookupEx(NotNull{def});
if (!lookupResult)
return fallback();
const auto [subjectType, subjectScope] = *lookupResult;
std::vector<std::string> segmentStrings(begin(segments), end(segments));
TypeId updatedType = arena->addType(BlockedType{});
TypeId assignedTy = arena->addType(BlockedType{});
auto setC = addConstraint(scope, expr->location, SetPropConstraint{updatedType, subjectType, std::move(segmentStrings), assignedTy});
getMutable<BlockedType>(updatedType)->setOwner(setC);
TypeId prevSegmentTy = updatedType;
for (size_t i = 0; i < segments.size(); ++i)
{
TypeId segmentTy = arena->addType(BlockedType{});
module->astTypes[exprs[i]] = segmentTy;
ValueContext ctx = i == segments.size() - 1 ? ValueContext::LValue : ValueContext::RValue;
auto hasC = addConstraint(scope, expr->location, HasPropConstraint{segmentTy, prevSegmentTy, segments[i], ctx, inConditional(typeContext)});
getMutable<BlockedType>(segmentTy)->setOwner(hasC);
setC->dependencies.push_back(hasC);
prevSegmentTy = segmentTy;
}
module->astTypes[expr] = prevSegmentTy;
module->astTypes[e] = updatedType;
if (!subjectType->persistent)
{
subjectScope->bindings[sym].typeId = updatedType;
// This can fail if the user is erroneously trying to augment a builtin
// table like os or string.
if (auto key = dfg->getRefinementKey(e))
{
subjectScope->lvalueTypes[key->def] = updatedType;
subjectScope->rvalueRefinements[key->def] = updatedType;
}
}
return {assignedTy, assignedTy};
TypeId lhsTy = check(scope, expr->expr).ty;
TypeId indexTy = check(scope, expr->index).ty;
TypeId propTy = arena->addType(BlockedType{});
module->astTypes[expr] = propTy;
addConstraint(scope, expr->location, AssignIndexConstraint{lhsTy, indexTy, rhsType, propTy});
}
Inference ConstraintGenerator::check(const ScopePtr& scope, AstExprTable* expr, std::optional<TypeId> expectedType)
@ -3194,6 +3002,11 @@ void ConstraintGenerator::reportCodeTooComplex(Location location)
TypeId ConstraintGenerator::makeUnion(const ScopePtr& scope, Location location, TypeId lhs, TypeId rhs)
{
if (get<NeverType>(follow(lhs)))
return rhs;
if (get<NeverType>(follow(rhs)))
return lhs;
TypeId resultType = createTypeFamilyInstance(kBuiltinTypeFamilies.unionFamily, {lhs, rhs}, {}, scope, location);
return resultType;

View File

@ -206,6 +206,12 @@ static std::pair<std::vector<TypeId>, std::vector<TypePackId>> saturateArguments
saturatedPackArguments.push_back(builtinTypes->errorRecoveryTypePack());
}
for (TypeId& arg : saturatedTypeArguments)
arg = follow(arg);
for (TypePackId& pack : saturatedPackArguments)
pack = follow(pack);
// At this point, these two conditions should be true. If they aren't we
// will run into access violations.
LUAU_ASSERT(saturatedTypeArguments.size() == fn.typeParams.size());
@ -407,11 +413,17 @@ void ConstraintSolver::run()
// decrement the referenced free types for this constraint if we dispatched successfully!
for (auto ty : c->getMaybeMutatedFreeTypes())
{
// this is a little weird, but because we're only counting free types in subtyping constraints,
// some constraints (like unpack) might actually produce _more_ references to a free type.
size_t& refCount = unresolvedConstraints[ty];
if (refCount > 0)
refCount -= 1;
// We have two constraints that are designed to wait for the
// refCount on a free type to be equal to 1: the
// PrimitiveTypeConstraint and ReduceConstraint. We
// therefore wake any constraint waiting for a free type's
// refcount to be 1 or 0.
if (refCount <= 1)
unblock(ty, Location{});
}
if (logger)
@ -518,15 +530,15 @@ bool ConstraintSolver::tryDispatch(NotNull<const Constraint> constraint, bool fo
success = tryDispatch(*fcc, constraint);
else if (auto hpc = get<HasPropConstraint>(*constraint))
success = tryDispatch(*hpc, constraint);
else if (auto spc = get<SetPropConstraint>(*constraint))
success = tryDispatch(*spc, constraint);
else if (auto spc = get<HasIndexerConstraint>(*constraint))
success = tryDispatch(*spc, constraint);
else if (auto spc = get<SetIndexerConstraint>(*constraint))
success = tryDispatch(*spc, constraint, force);
else if (auto uc = get<UnpackConstraint>(*constraint))
else if (auto uc = get<AssignConstraint>(*constraint))
success = tryDispatch(*uc, constraint);
else if (auto uc = get<Unpack1Constraint>(*constraint))
else if (auto uc = get<AssignPropConstraint>(*constraint))
success = tryDispatch(*uc, constraint);
else if (auto uc = get<AssignIndexConstraint>(*constraint))
success = tryDispatch(*uc, constraint);
else if (auto uc = get<UnpackConstraint>(*constraint))
success = tryDispatch(*uc, constraint);
else if (auto rc = get<ReduceConstraint>(*constraint))
success = tryDispatch(*rc, constraint, force);
@ -688,7 +700,18 @@ bool ConstraintSolver::tryDispatch(const IterableConstraint& c, NotNull<const Co
getMutable<TableType>(tableTy)->indexer = TableIndexer{keyTy, valueTy};
pushConstraint(constraint->scope, constraint->location, SubtypeConstraint{nextTy, tableTy});
pushConstraint(constraint->scope, constraint->location, UnpackConstraint{c.variables, arena->addTypePack({keyTy, valueTy}), /*resultIsLValue=*/true});
auto it = begin(c.variables);
auto endIt = end(c.variables);
if (it != endIt)
{
pushConstraint(constraint->scope, constraint->location, AssignConstraint{*it, keyTy});
++it;
}
if (it != endIt)
pushConstraint(constraint->scope, constraint->location, AssignConstraint{*it, valueTy});
return true;
}
@ -915,7 +938,17 @@ bool ConstraintSolver::tryDispatch(const TypeAliasExpansionConstraint& c, NotNul
// Type function application will happily give us the exact same type if
// there are e.g. generic saturatedTypeArguments that go unused.
const TableType* tfTable = getTableType(tf->type);
bool needsClone = follow(tf->type) == target || (tfTable != nullptr && tfTable == getTableType(target));
//clang-format off
bool needsClone =
follow(tf->type) == target ||
(tfTable != nullptr && tfTable == getTableType(target)) ||
std::any_of(typeArguments.begin(), typeArguments.end(), [&](const auto& other) {
return other == target;
}
);
//clang-format on
// Only tables have the properties we're trying to set.
TableType* ttv = getMutableTableType(target);
@ -1291,158 +1324,6 @@ bool ConstraintSolver::tryDispatch(const HasPropConstraint& c, NotNull<const Con
return true;
}
static bool isUnsealedTable(TypeId ty)
{
ty = follow(ty);
const TableType* ttv = get<TableType>(ty);
return ttv && ttv->state == TableState::Unsealed;
}
/**
* Given a path into a set of nested unsealed tables `ty`, insert a new property `replaceTy` as the leaf-most property.
*
* Fails and does nothing if every table along the way is not unsealed.
*
* Mutates the innermost table type in-place.
*/
static void updateTheTableType(
NotNull<BuiltinTypes> builtinTypes, NotNull<TypeArena> arena, TypeId ty, const std::vector<std::string>& path, TypeId replaceTy)
{
if (path.empty())
return;
// First walk the path and ensure that it's unsealed tables all the way
// to the end.
{
TypeId t = ty;
for (size_t i = 0; i < path.size() - 1; ++i)
{
if (!isUnsealedTable(t))
return;
const TableType* tbl = get<TableType>(t);
auto it = tbl->props.find(path[i]);
if (it == tbl->props.end())
return;
t = follow(it->second.type());
}
// The last path segment should not be a property of the table at all.
// We are not changing property types. We are only admitting this one
// new property to be appended.
if (!isUnsealedTable(t))
return;
const TableType* tbl = get<TableType>(t);
if (0 != tbl->props.count(path.back()))
return;
}
TypeId t = ty;
ErrorVec dummy;
for (size_t i = 0; i < path.size() - 1; ++i)
{
t = follow(t);
auto propTy = findTablePropertyRespectingMeta(builtinTypes, dummy, t, path[i], ValueContext::LValue, Location{});
dummy.clear();
if (!propTy)
return;
t = *propTy;
}
const std::string& lastSegment = path.back();
t = follow(t);
TableType* tt = getMutable<TableType>(t);
if (auto mt = get<MetatableType>(t))
tt = getMutable<TableType>(mt->table);
if (!tt)
return;
tt->props[lastSegment].setType(replaceTy);
}
bool ConstraintSolver::tryDispatch(const SetPropConstraint& c, NotNull<const Constraint> constraint)
{
TypeId subjectType = follow(c.subjectType);
const TypeId propType = follow(c.propType);
if (isBlocked(subjectType))
return block(subjectType, constraint);
std::optional<TypeId> existingPropType = subjectType;
LUAU_ASSERT(!c.path.empty());
if (c.path.empty())
return false;
for (size_t i = 0; i < c.path.size(); ++i)
{
const std::string& segment = c.path[i];
if (!existingPropType)
break;
ValueContext ctx = i == c.path.size() - 1 ? ValueContext::LValue : ValueContext::RValue;
auto [blocked, result] = lookupTableProp(constraint, *existingPropType, segment, ctx);
if (!blocked.empty())
{
for (TypeId blocked : blocked)
block(blocked, constraint);
return false;
}
existingPropType = result;
}
auto bind = [&](TypeId a, TypeId b) {
bindBlockedType(a, b, subjectType, constraint);
};
if (existingPropType)
{
unify(constraint, propType, *existingPropType);
unify(constraint, *existingPropType, propType);
bind(c.resultType, c.subjectType);
unblock(c.resultType, constraint->location);
return true;
}
const TypeId originalSubjectType = subjectType;
if (auto mt = get<MetatableType>(subjectType))
subjectType = follow(mt->table);
if (get<FreeType>(subjectType))
return false;
else if (auto ttv = getMutable<TableType>(subjectType))
{
if (ttv->state == TableState::Free)
{
LUAU_ASSERT(!subjectType->persistent);
ttv->props[c.path[0]] = Property{propType};
bind(c.resultType, subjectType);
unblock(c.resultType, constraint->location);
return true;
}
else if (ttv->state == TableState::Unsealed)
{
LUAU_ASSERT(!subjectType->persistent);
updateTheTableType(builtinTypes, NotNull{arena}, subjectType, c.path, propType);
}
}
bind(c.resultType, originalSubjectType);
unblock(c.resultType, constraint->location);
return true;
}
bool ConstraintSolver::tryDispatchHasIndexer(
int& recursionDepth, NotNull<const Constraint> constraint, TypeId subjectType, TypeId indexType, TypeId resultType, Set<TypeId>& seen)
{
@ -1460,6 +1341,13 @@ bool ConstraintSolver::tryDispatchHasIndexer(
if (auto ft = get<FreeType>(subjectType))
{
if (auto tbl = get<TableType>(follow(ft->upperBound)); tbl && tbl->indexer)
{
unify(constraint, indexType, tbl->indexer->indexType);
bindBlockedType(resultType, tbl->indexer->indexResultType, subjectType, constraint);
return true;
}
FreeType freeResult{ft->scope, builtinTypes->neverType, builtinTypes->unknownType};
emplaceType<FreeType>(asMutable(resultType), freeResult);
@ -1708,33 +1596,20 @@ std::pair<bool, std::optional<TypeId>> ConstraintSolver::tryDispatchSetIndexer(
return {true, std::nullopt};
}
bool ConstraintSolver::tryDispatch(const SetIndexerConstraint& c, NotNull<const Constraint> constraint, bool force)
bool ConstraintSolver::tryDispatch(const AssignConstraint& c, NotNull<const Constraint> constraint)
{
TypeId subjectType = follow(c.subjectType);
if (isBlocked(subjectType))
return block(subjectType, constraint);
const TypeId lhsTy = follow(c.lhsType);
const TypeId rhsTy = follow(c.rhsType);
auto [dispatched, resultTy] = tryDispatchSetIndexer(constraint, subjectType, c.indexType, c.propType, /*expandFreeTypeBounds=*/true);
if (dispatched)
{
bindBlockedType(c.propType, resultTy.value_or(builtinTypes->errorRecoveryType()), subjectType, constraint);
unblock(c.propType, constraint->location);
}
return dispatched;
}
bool ConstraintSolver::tryDispatchUnpack1(NotNull<const Constraint> constraint, TypeId resultTy, TypeId srcTy, bool resultIsLValue)
{
resultTy = follow(resultTy);
LUAU_ASSERT(canMutate(resultTy, constraint));
if (!get<LocalType>(lhsTy) && isBlocked(lhsTy))
return block(lhsTy, constraint);
auto tryExpand = [&](TypeId ty) {
LocalType* lt = getMutable<LocalType>(ty);
if (!lt || !resultIsLValue)
if (!lt)
return;
lt->domain = simplifyUnion(builtinTypes, arena, lt->domain, srcTy).result;
lt->domain = simplifyUnion(builtinTypes, arena, lt->domain, rhsTy).result;
LUAU_ASSERT(lt->blockCount > 0);
--lt->blockCount;
@ -1745,11 +1620,289 @@ bool ConstraintSolver::tryDispatchUnpack1(NotNull<const Constraint> constraint,
}
};
if (auto ut = get<UnionType>(resultTy))
std::for_each(begin(ut), end(ut), tryExpand);
else if (get<LocalType>(resultTy))
tryExpand(resultTy);
else if (get<BlockedType>(resultTy))
if (auto ut = get<UnionType>(lhsTy))
{
// FIXME: I suspect there's a bug here where lhsTy is a union that contains no LocalTypes.
for (TypeId t : ut)
tryExpand(t);
}
else if (get<LocalType>(lhsTy))
tryExpand(lhsTy);
else
unify(constraint, rhsTy, lhsTy);
unblock(lhsTy, constraint->location);
return true;
}
bool ConstraintSolver::tryDispatch(const AssignPropConstraint& c, NotNull<const Constraint> constraint)
{
TypeId lhsType = follow(c.lhsType);
const std::string& propName = c.propName;
const TypeId rhsType = follow(c.rhsType);
if (isBlocked(lhsType))
return block(lhsType, constraint);
// 1. lhsType is a class that already has the prop
// 2. lhsType is a table that already has the prop (or a union or
// intersection that has the prop in aggregate)
// 3. lhsType has a metatable that already has the prop
// 4. lhsType is an unsealed table that does not have the prop, but has a
// string indexer
// 5. lhsType is an unsealed table that does not have the prop or a string
// indexer
// Important: In every codepath through this function, the type `c.propType`
// must be bound to something, even if it's just the errorType.
if (auto lhsClass = get<ClassType>(lhsType))
{
const Property* prop = lookupClassProp(lhsClass, propName);
if (!prop || !prop->writeTy.has_value())
return true;
emplaceType<BoundType>(asMutable(c.propType), *prop->writeTy);
unify(constraint, rhsType, *prop->writeTy);
return true;
}
if (auto lhsFree = getMutable<FreeType>(lhsType))
{
if (get<TableType>(lhsFree->upperBound) || get<MetatableType>(lhsFree->upperBound))
lhsType = lhsFree->upperBound;
else
{
TypeId newUpperBound = arena->addType(TableType{TableState::Free, TypeLevel{}, constraint->scope});
TableType* upperTable = getMutable<TableType>(newUpperBound);
LUAU_ASSERT(upperTable);
upperTable->props[c.propName] = rhsType;
// Food for thought: Could we block if simplification encounters a blocked type?
lhsFree->upperBound = simplifyIntersection(builtinTypes, arena, lhsFree->upperBound, newUpperBound).result;
emplaceType<BoundType>(asMutable(c.propType), rhsType);
return true;
}
}
// Handle the case that lhsType is a table that already has the property or
// a matching indexer. This also handles unions and intersections.
const auto [blocked, maybeTy] = lookupTableProp(constraint, lhsType, propName, ValueContext::LValue);
if (!blocked.empty())
{
for (TypeId t : blocked)
block(t, constraint);
return false;
}
if (maybeTy)
{
const TypeId propTy = *maybeTy;
emplaceType<BoundType>(asMutable(c.propType), propTy);
unify(constraint, rhsType, propTy);
return true;
}
if (auto lhsMeta = get<MetatableType>(lhsType))
lhsType = follow(lhsMeta->table);
// Handle the case where the lhs type is a table that does not have the
// named property. It could be a table with a string indexer, or an unsealed
// or free table that can grow.
if (auto lhsTable = getMutable<TableType>(lhsType))
{
if (auto it = lhsTable->props.find(propName); it != lhsTable->props.end())
{
Property& prop = it->second;
if (prop.writeTy.has_value())
{
emplaceType<BoundType>(asMutable(c.propType), *prop.writeTy);
unify(constraint, rhsType, *prop.writeTy);
return true;
}
else
{
LUAU_ASSERT(prop.isReadOnly());
if (lhsTable->state == TableState::Unsealed || lhsTable->state == TableState::Free)
{
prop.writeTy = prop.readTy;
emplaceType<BoundType>(asMutable(c.propType), *prop.writeTy);
unify(constraint, rhsType, *prop.writeTy);
return true;
}
else
{
emplaceType<BoundType>(asMutable(c.propType), builtinTypes->errorType);
return true;
}
}
}
if (lhsTable->indexer && maybeString(lhsTable->indexer->indexType))
{
emplaceType<BoundType>(asMutable(c.propType), rhsType);
unify(constraint, rhsType, lhsTable->indexer->indexResultType);
return true;
}
if (lhsTable->state == TableState::Unsealed || lhsTable->state == TableState::Free)
{
emplaceType<BoundType>(asMutable(c.propType), rhsType);
lhsTable->props[propName] = Property::rw(rhsType);
return true;
}
}
emplaceType<BoundType>(asMutable(c.propType), builtinTypes->errorType);
return true;
}
bool ConstraintSolver::tryDispatch(const AssignIndexConstraint& c, NotNull<const Constraint> constraint)
{
const TypeId lhsType = follow(c.lhsType);
const TypeId indexType = follow(c.indexType);
const TypeId rhsType = follow(c.rhsType);
if (isBlocked(lhsType))
return block(lhsType, constraint);
// 0. lhsType could be an intersection or union.
// 1. lhsType is a class with an indexer
// 2. lhsType is a table with an indexer, or it has a metatable that has an indexer
// 3. lhsType is a free or unsealed table and can grow an indexer
// Important: In every codepath through this function, the type `c.propType`
// must be bound to something, even if it's just the errorType.
auto tableStuff = [&](TableType* lhsTable) -> std::optional<bool> {
if (lhsTable->indexer)
{
unify(constraint, indexType, lhsTable->indexer->indexType);
unify(constraint, rhsType, lhsTable->indexer->indexResultType);
emplaceType<BoundType>(asMutable(c.propType), lhsTable->indexer->indexResultType);
return true;
}
if (lhsTable->state == TableState::Unsealed || lhsTable->state == TableState::Free)
{
lhsTable->indexer = TableIndexer{indexType, rhsType};
emplaceType<BoundType>(asMutable(c.propType), rhsType);
return true;
}
return {};
};
if (auto lhsFree = getMutable<FreeType>(lhsType))
{
if (auto lhsTable = getMutable<TableType>(lhsFree->upperBound))
{
if (auto res = tableStuff(lhsTable))
return *res;
}
TypeId newUpperBound =
arena->addType(TableType{/*props*/ {}, TableIndexer{indexType, rhsType}, TypeLevel{}, constraint->scope, TableState::Free});
const TableType* newTable = get<TableType>(newUpperBound);
LUAU_ASSERT(newTable);
unify(constraint, lhsType, newUpperBound);
LUAU_ASSERT(newTable->indexer);
emplaceType<BoundType>(asMutable(c.propType), newTable->indexer->indexResultType);
return true;
}
if (auto lhsTable = getMutable<TableType>(lhsType))
{
std::optional<bool> res = tableStuff(lhsTable);
if (res.has_value())
return *res;
}
if (auto lhsClass = get<ClassType>(lhsType))
{
while (true)
{
if (lhsClass->indexer)
{
unify(constraint, indexType, lhsClass->indexer->indexType);
unify(constraint, rhsType, lhsClass->indexer->indexResultType);
emplaceType<BoundType>(asMutable(c.propType), lhsClass->indexer->indexResultType);
return true;
}
if (lhsClass->parent)
lhsClass = get<ClassType>(lhsClass->parent);
else
break;
}
return true;
}
if (auto lhsIntersection = getMutable<IntersectionType>(lhsType))
{
std::set<TypeId> parts;
for (TypeId t : lhsIntersection)
{
if (auto tbl = getMutable<TableType>(follow(t)))
{
if (tbl->indexer)
{
unify(constraint, indexType, tbl->indexer->indexType);
parts.insert(tbl->indexer->indexResultType);
}
if (tbl->state == TableState::Unsealed || tbl->state == TableState::Free)
{
tbl->indexer = TableIndexer{indexType, rhsType};
parts.insert(rhsType);
}
}
else if (auto cls = get<ClassType>(follow(t)))
{
while (true)
{
if (cls->indexer)
{
unify(constraint, indexType, cls->indexer->indexType);
parts.insert(cls->indexer->indexResultType);
break;
}
if (cls->parent)
cls = get<ClassType>(cls->parent);
else
break;
}
}
}
TypeId res = simplifyIntersection(builtinTypes, arena, std::move(parts)).result;
unify(constraint, rhsType, res);
}
// Other types do not support index assignment.
emplaceType<BoundType>(asMutable(c.propType), builtinTypes->errorType);
return true;
}
bool ConstraintSolver::tryDispatchUnpack1(NotNull<const Constraint> constraint, TypeId resultTy, TypeId srcTy)
{
resultTy = follow(resultTy);
LUAU_ASSERT(canMutate(resultTy, constraint));
LUAU_ASSERT(get<BlockedType>(resultTy));
if (get<BlockedType>(resultTy))
{
if (follow(srcTy) == resultTy)
{
@ -1765,10 +1918,7 @@ bool ConstraintSolver::tryDispatchUnpack1(NotNull<const Constraint> constraint,
bindBlockedType(resultTy, srcTy, srcTy, constraint);
}
else
{
LUAU_ASSERT(resultIsLValue);
unify(constraint, srcTy, resultTy);
}
unblock(resultTy, constraint->location);
return true;
@ -1804,7 +1954,7 @@ bool ConstraintSolver::tryDispatch(const UnpackConstraint& c, NotNull<const Cons
TypeId srcTy = follow(srcPack.head[i]);
TypeId resultTy = follow(*resultIter);
tryDispatchUnpack1(constraint, resultTy, srcTy, c.resultIsLValue);
tryDispatchUnpack1(constraint, resultTy, srcTy);
++resultIter;
++i;
@ -1818,19 +1968,7 @@ bool ConstraintSolver::tryDispatch(const UnpackConstraint& c, NotNull<const Cons
{
TypeId resultTy = follow(*resultIter);
LUAU_ASSERT(canMutate(resultTy, constraint));
if (auto lt = getMutable<LocalType>(resultTy); c.resultIsLValue && lt)
{
lt->domain = simplifyUnion(builtinTypes, arena, lt->domain, builtinTypes->nilType).result;
LUAU_ASSERT(0 <= lt->blockCount);
--lt->blockCount;
if (0 == lt->blockCount)
{
shiftReferences(resultTy, lt->domain);
emplaceType<BoundType>(asMutable(resultTy), lt->domain);
}
}
else if (get<BlockedType>(resultTy) || get<PendingExpansionType>(resultTy))
if (get<BlockedType>(resultTy) || get<PendingExpansionType>(resultTy))
{
emplaceType<BoundType>(asMutable(resultTy), builtinTypes->nilType);
unblock(resultTy, constraint->location);
@ -1842,11 +1980,6 @@ bool ConstraintSolver::tryDispatch(const UnpackConstraint& c, NotNull<const Cons
return true;
}
bool ConstraintSolver::tryDispatch(const Unpack1Constraint& c, NotNull<const Constraint> constraint)
{
return tryDispatchUnpack1(constraint, c.resultType, c.sourceType, c.resultIsLValue);
}
bool ConstraintSolver::tryDispatch(const ReduceConstraint& c, NotNull<const Constraint> constraint, bool force)
{
TypeId ty = follow(c.ty);
@ -1942,13 +2075,23 @@ bool ConstraintSolver::tryDispatchIterableTable(TypeId iteratorTy, const Iterabl
getMutable<TableType>(tableTy)->indexer = TableIndexer{keyTy, valueTy};
pushConstraint(constraint->scope, constraint->location, SubtypeConstraint{iteratorTy, tableTy});
pushConstraint(constraint->scope, constraint->location, UnpackConstraint{c.variables, arena->addTypePack({keyTy, valueTy}), /*resultIsLValue=*/true});
auto it = begin(c.variables);
auto endIt = end(c.variables);
if (it != endIt)
{
pushConstraint(constraint->scope, constraint->location, AssignConstraint{*it, keyTy});
++it;
}
if (it != endIt)
pushConstraint(constraint->scope, constraint->location, AssignConstraint{*it, valueTy});
return true;
}
auto unpack = [&](TypeId ty) {
TypePackId variadic = arena->addTypePack(VariadicTypePack{ty});
pushConstraint(constraint->scope, constraint->location, UnpackConstraint{c.variables, variadic, /* resultIsLValue */ true});
for (TypeId varTy : c.variables)
pushConstraint(constraint->scope, constraint->location, AssignConstraint{varTy, ty});
};
if (get<AnyType>(iteratorTy))
@ -2043,10 +2186,7 @@ bool ConstraintSolver::tryDispatchIterableTable(TypeId iteratorTy, const Iterabl
// If nextFn is nullptr, then the iterator function has an improper signature.
if (nextFn)
{
const TypePackId nextRetPack = nextFn->retTypes;
pushConstraint(constraint->scope, constraint->location, UnpackConstraint{c.variables, nextRetPack, /* resultIsLValue=*/true});
}
unpackAndAssign(c.variables, nextFn->retTypes, constraint);
return true;
}
@ -2119,12 +2259,37 @@ bool ConstraintSolver::tryDispatchIterableFunction(
modifiedNextRetHead.push_back(*it);
TypePackId modifiedNextRetPack = arena->addTypePack(std::move(modifiedNextRetHead), it.tail());
auto psc = pushConstraint(constraint->scope, constraint->location, UnpackConstraint{c.variables, modifiedNextRetPack, /* resultIsLValue */ true});
inheritBlocks(constraint, psc);
auto unpackConstraint = unpackAndAssign(c.variables, modifiedNextRetPack, constraint);
inheritBlocks(constraint, unpackConstraint);
return true;
}
NotNull<const Constraint> ConstraintSolver::unpackAndAssign(TypePackId destTypes, TypePackId srcTypes, NotNull<const Constraint> constraint)
{
std::vector<TypeId> unpackedTys;
for (TypeId _ty : destTypes)
{
(void) _ty;
unpackedTys.push_back(arena->addType(BlockedType{}));
}
TypePackId unpackedTp = arena->addTypePack(TypePack{unpackedTys});
auto unpackConstraint = pushConstraint(constraint->scope, constraint->location, UnpackConstraint{unpackedTp, srcTypes});
size_t i = 0;
for (TypeId varTy : destTypes)
{
pushConstraint(constraint->scope, constraint->location, AssignConstraint{varTy, unpackedTys[i]});
getMutable<BlockedType>(unpackedTys[i])->setOwner(unpackConstraint);
++i;
}
return unpackConstraint;
}
std::pair<std::vector<TypeId>, std::optional<TypeId>> ConstraintSolver::lookupTableProp(NotNull<const Constraint> constraint, TypeId subjectType,
const std::string& propName, ValueContext context, bool inConditional, bool suppressSimplification)
{
@ -2759,8 +2924,13 @@ std::optional<TypeId> ConstraintSolver::generalizeFreeType(NotNull<Scope> scope,
if (get<FreeType>(t))
{
auto refCount = unresolvedConstraints.find(t);
if (!refCount || *refCount > 1)
if (refCount && *refCount > 0)
return {};
// if no reference count is present, then that means the only constraints referring to
// this free type need only for it to be generalized. in principle, this means we could
// have actually never generated the free type in the first place, but we couldn't know
// that until all constraint generation is complete.
}
return generalize(NotNull{arena}, builtinTypes, scope, type);
@ -2769,7 +2939,7 @@ std::optional<TypeId> ConstraintSolver::generalizeFreeType(NotNull<Scope> scope,
bool ConstraintSolver::hasUnresolvedConstraints(TypeId ty)
{
if (auto refCount = unresolvedConstraints.find(ty))
return *refCount > 1;
return *refCount > 0;
return false;
}

View File

@ -1368,6 +1368,17 @@ SimplifyResult simplifyIntersection(NotNull<BuiltinTypes> builtinTypes, NotNull<
return SimplifyResult{res, std::move(s.blockedTypes)};
}
SimplifyResult simplifyIntersection(NotNull<BuiltinTypes> builtinTypes, NotNull<TypeArena> arena, std::set<TypeId> parts)
{
LUAU_ASSERT(FFlag::DebugLuauDeferredConstraintResolution);
TypeSimplifier s{builtinTypes, arena};
TypeId res = s.intersectFromParts(std::move(parts));
return SimplifyResult{res, std::move(s.blockedTypes)};
}
SimplifyResult simplifyUnion(NotNull<BuiltinTypes> builtinTypes, NotNull<TypeArena> arena, TypeId left, TypeId right)
{
LUAU_ASSERT(FFlag::DebugLuauDeferredConstraintResolution);

View File

@ -1438,6 +1438,7 @@ SubtypingResult Subtyping::isCovariantWith(
result.andAlso(isCovariantWith(env, subNorm->strings, superNorm->strings));
result.andAlso(isCovariantWith(env, subNorm->strings, superNorm->tables));
result.andAlso(isCovariantWith(env, subNorm->threads, superNorm->threads));
result.andAlso(isCovariantWith(env, subNorm->buffers, superNorm->buffers));
result.andAlso(isCovariantWith(env, subNorm->tables, superNorm->tables));
result.andAlso(isCovariantWith(env, subNorm->functions, superNorm->functions));
// isCovariantWith(subNorm->tyvars, superNorm->tyvars);

View File

@ -1787,23 +1787,18 @@ std::string toString(const Constraint& constraint, ToStringOptions& opts)
{
return tos(c.resultType) + " ~ hasProp " + tos(c.subjectType) + ", \"" + c.prop + "\" ctx=" + std::to_string(int(c.context));
}
else if constexpr (std::is_same_v<T, SetPropConstraint>)
{
const std::string pathStr = c.path.size() == 1 ? "\"" + c.path[0] + "\"" : "[\"" + join(c.path, "\", \"") + "\"]";
return tos(c.resultType) + " ~ setProp " + tos(c.subjectType) + ", " + pathStr + " " + tos(c.propType);
}
else if constexpr (std::is_same_v<T, HasIndexerConstraint>)
{
return tos(c.resultType) + " ~ hasIndexer " + tos(c.subjectType) + " " + tos(c.indexType);
}
else if constexpr (std::is_same_v<T, SetIndexerConstraint>)
{
return "setIndexer " + tos(c.subjectType) + " [ " + tos(c.indexType) + " ] " + tos(c.propType);
}
else if constexpr (std::is_same_v<T, AssignConstraint>)
return "assign " + tos(c.lhsType) + " " + tos(c.rhsType);
else if constexpr (std::is_same_v<T, AssignPropConstraint>)
return "assignProp " + tos(c.lhsType) + " " + c.propName + " " + tos(c.rhsType);
else if constexpr (std::is_same_v<T, AssignIndexConstraint>)
return "assignIndex " + tos(c.lhsType) + " " + tos(c.indexType) + " " + tos(c.rhsType);
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, Unpack1Constraint>)
return tos(c.resultType) + " ~ unpack " + tos(c.sourceType);
else if constexpr (std::is_same_v<T, ReduceConstraint>)
return "reduce " + tos(c.ty);
else if constexpr (std::is_same_v<T, ReducePackConstraint>)

View File

@ -1561,6 +1561,18 @@ struct TypeChecker2
else
reportError(CannotExtendTable{exprType, CannotExtendTable::Indexer, "indexer??"}, indexExpr->location);
}
else if (auto mt = get<MetatableType>(exprType))
{
const TableType* tt = get<TableType>(follow(mt->table));
LUAU_ASSERT(tt);
if (tt->indexer)
testIsSubtype(indexType, tt->indexer->indexType, indexExpr->index->location);
else
{
// TODO: Maybe the metatable has a suitable indexer?
reportError(CannotExtendTable{exprType, CannotExtendTable::Indexer, "indexer??"}, indexExpr->location);
}
}
else if (auto cls = get<ClassType>(exprType))
{
if (cls->indexer)
@ -1581,6 +1593,19 @@ struct TypeChecker2
reportError(OptionalValueAccess{exprType}, indexExpr->location);
}
}
else if (auto exprIntersection = get<IntersectionType>(exprType))
{
for (TypeId part : exprIntersection)
{
(void)part;
}
}
else if (get<NeverType>(exprType) || isErrorSuppressing(indexExpr->location, exprType))
{
// Nothing
}
else
reportError(NotATable{exprType}, indexExpr->location);
}
void visit(AstExprFunction* fn)
@ -2720,6 +2745,8 @@ struct TypeChecker2
fetch(builtinTypes->stringType);
if (normValid)
fetch(norm->threads);
if (normValid)
fetch(norm->buffers);
if (normValid)
{

View File

@ -11,12 +11,10 @@
#include "Luau/OverloadResolution.h"
#include "Luau/Set.h"
#include "Luau/Simplify.h"
#include "Luau/Substitution.h"
#include "Luau/Subtyping.h"
#include "Luau/ToString.h"
#include "Luau/TxnLog.h"
#include "Luau/Type.h"
#include "Luau/TypeCheckLimits.h"
#include "Luau/TypeFamilyReductionGuesser.h"
#include "Luau/TypeFwd.h"
#include "Luau/TypeUtils.h"
@ -346,9 +344,8 @@ struct FamilyReducer
if (tryGuessing(subject))
return;
TypeFamilyQueue queue{NotNull{&queuedTys}, NotNull{&queuedTps}};
TypeFamilyReductionResult<TypeId> result =
tfit->family->reducer(subject, NotNull{&queue}, tfit->typeArguments, tfit->packArguments, NotNull{&ctx});
tfit->family->reducer(subject, tfit->typeArguments, tfit->packArguments, NotNull{&ctx});
handleFamilyReduction(subject, result);
}
}
@ -372,9 +369,8 @@ struct FamilyReducer
if (tryGuessing(subject))
return;
TypeFamilyQueue queue{NotNull{&queuedTys}, NotNull{&queuedTps}};
TypeFamilyReductionResult<TypePackId> result =
tfit->family->reducer(subject, NotNull{&queue}, tfit->typeArguments, tfit->packArguments, NotNull{&ctx});
tfit->family->reducer(subject, tfit->typeArguments, tfit->packArguments, NotNull{&ctx});
handleFamilyReduction(subject, result);
}
}
@ -449,24 +445,89 @@ FamilyGraphReductionResult reduceFamilies(TypePackId entrypoint, Location locati
std::move(collector.cyclicInstance), location, ctx, force);
}
void TypeFamilyQueue::add(TypeId instanceTy)
{
LUAU_ASSERT(get<TypeFamilyInstanceType>(instanceTy));
queuedTys->push_back(instanceTy);
}
void TypeFamilyQueue::add(TypePackId instanceTp)
{
LUAU_ASSERT(get<TypeFamilyInstanceTypePack>(instanceTp));
queuedTps->push_back(instanceTp);
}
bool isPending(TypeId ty, ConstraintSolver* solver)
{
return is<BlockedType, PendingExpansionType, TypeFamilyInstanceType, LocalType>(ty) || (solver && solver->hasUnresolvedConstraints(ty));
}
TypeFamilyReductionResult<TypeId> notFamilyFn(TypeId instance, NotNull<TypeFamilyQueue> queue, const std::vector<TypeId>& typeParams,
template<typename F, typename... Args>
static std::optional<TypeFamilyReductionResult<TypeId>> tryDistributeTypeFamilyApp(F f, TypeId instance,
const std::vector<TypeId>& typeParams, const std::vector<TypePackId>& packParams, NotNull<TypeFamilyContext> ctx, Args&& ...args)
{
// op (a | b) (c | d) ~ (op a (c | d)) | (op b (c | d)) ~ (op a c) | (op a d) | (op b c) | (op b d)
bool uninhabited = false;
std::vector<TypeId> blockedTypes;
std::vector<TypeId> results;
size_t cartesianProductSize = 1;
const UnionType* firstUnion = nullptr;
size_t unionIndex;
std::vector<TypeId> arguments = typeParams;
for (size_t i = 0; i < arguments.size(); ++i)
{
const UnionType* ut = get<UnionType>(follow(arguments[i]));
if (!ut)
continue;
// We want to find the first union type in the set of arguments to distribute that one and only that one union.
// The function `f` we have is recursive, so `arguments[unionIndex]` will be updated in-place for each option in
// the union we've found in this context, so that index will no longer be a union type. Any other arguments at
// index + 1 or after will instead be distributed, if those are a union, which will be subjected to the same rules.
if (!firstUnion && ut)
{
firstUnion = ut;
unionIndex = i;
}
cartesianProductSize *= std::distance(begin(ut), end(ut));
// TODO: We'd like to report that the type family application is too complex here.
if (size_t(DFInt::LuauTypeFamilyApplicationCartesianProductLimit) <= cartesianProductSize)
return {{std::nullopt, true, {}, {}}};
}
if (!firstUnion)
{
// If we couldn't find any union type argument, we're not distributing.
return std::nullopt;
}
for (TypeId option : firstUnion)
{
arguments[unionIndex] = option;
TypeFamilyReductionResult<TypeId> result = f(instance, arguments, packParams, ctx, args...);
blockedTypes.insert(blockedTypes.end(), result.blockedTypes.begin(), result.blockedTypes.end());
uninhabited |= result.uninhabited;
if (result.uninhabited || !result.result)
break;
else
results.push_back(*result.result);
}
if (uninhabited || !blockedTypes.empty())
return {{std::nullopt, uninhabited, blockedTypes, {}}};
if (!results.empty())
{
if (results.size() == 1)
return {{results[0], false, {}, {}}};
TypeId resultTy = ctx->arena->addType(TypeFamilyInstanceType{
NotNull{&kBuiltinTypeFamilies.unionFamily},
std::move(results),
{},
});
return {{resultTy, false, {}, {}}};
}
return std::nullopt;
}
TypeFamilyReductionResult<TypeId> notFamilyFn(TypeId instance, const std::vector<TypeId>& typeParams,
const std::vector<TypePackId>& packParams, NotNull<TypeFamilyContext> ctx)
{
if (typeParams.size() != 1 || !packParams.empty())
@ -477,14 +538,20 @@ TypeFamilyReductionResult<TypeId> notFamilyFn(TypeId instance, NotNull<TypeFamil
TypeId ty = follow(typeParams.at(0));
if (ty == instance)
return {ctx->builtins->neverType, false, {}, {}};
if (isPending(ty, ctx->solver))
return {std::nullopt, false, {ty}, {}};
if (auto result = tryDistributeTypeFamilyApp(notFamilyFn, instance, typeParams, packParams, ctx))
return *result;
// `not` operates on anything and returns a `boolean` always.
return {ctx->builtins->booleanType, false, {}, {}};
}
TypeFamilyReductionResult<TypeId> lenFamilyFn(TypeId instance, NotNull<TypeFamilyQueue> queue, const std::vector<TypeId>& typeParams,
TypeFamilyReductionResult<TypeId> lenFamilyFn(TypeId instance, const std::vector<TypeId>& typeParams,
const std::vector<TypePackId>& packParams, NotNull<TypeFamilyContext> ctx)
{
if (typeParams.size() != 1 || !packParams.empty())
@ -495,6 +562,9 @@ TypeFamilyReductionResult<TypeId> lenFamilyFn(TypeId instance, NotNull<TypeFamil
TypeId operandTy = follow(typeParams.at(0));
if (operandTy == instance)
return {ctx->builtins->neverType, false, {}, {}};
// check to see if the operand type is resolved enough, and wait to reduce if not
// the use of `typeFromNormal` later necessitates blocking on local types.
if (isPending(operandTy, ctx->solver) || get<LocalType>(operandTy))
@ -533,6 +603,9 @@ TypeFamilyReductionResult<TypeId> lenFamilyFn(TypeId instance, NotNull<TypeFamil
if (normTy->hasTopTable() || get<TableType>(normalizedOperand))
return {ctx->builtins->numberType, false, {}, {}};
if (auto result = tryDistributeTypeFamilyApp(notFamilyFn, instance, typeParams, packParams, ctx))
return *result;
// findMetatableEntry demands the ability to emit errors, so we must give it
// the necessary state to do that, even if we intend to just eat the errors.
ErrorVec dummy;
@ -570,7 +643,7 @@ TypeFamilyReductionResult<TypeId> lenFamilyFn(TypeId instance, NotNull<TypeFamil
return {ctx->builtins->numberType, false, {}, {}};
}
TypeFamilyReductionResult<TypeId> unmFamilyFn(TypeId instance, NotNull<TypeFamilyQueue> queue, const std::vector<TypeId>& typeParams,
TypeFamilyReductionResult<TypeId> unmFamilyFn(TypeId instance, const std::vector<TypeId>& typeParams,
const std::vector<TypePackId>& packParams, NotNull<TypeFamilyContext> ctx)
{
if (typeParams.size() != 1 || !packParams.empty())
@ -581,6 +654,9 @@ TypeFamilyReductionResult<TypeId> unmFamilyFn(TypeId instance, NotNull<TypeFamil
TypeId operandTy = follow(typeParams.at(0));
if (operandTy == instance)
return {ctx->builtins->neverType, false, {}, {}};
// check to see if the operand type is resolved enough, and wait to reduce if not
if (isPending(operandTy, ctx->solver))
return {std::nullopt, false, {operandTy}, {}};
@ -612,6 +688,9 @@ TypeFamilyReductionResult<TypeId> unmFamilyFn(TypeId instance, NotNull<TypeFamil
if (normTy->isExactlyNumber())
return {ctx->builtins->numberType, false, {}, {}};
if (auto result = tryDistributeTypeFamilyApp(notFamilyFn, instance, typeParams, packParams, ctx))
return *result;
// findMetatableEntry demands the ability to emit errors, so we must give it
// the necessary state to do that, even if we intend to just eat the errors.
ErrorVec dummy;
@ -664,7 +743,7 @@ NotNull<Constraint> TypeFamilyContext::pushConstraint(ConstraintV&& c)
return newConstraint;
}
TypeFamilyReductionResult<TypeId> numericBinopFamilyFn(TypeId instance, NotNull<TypeFamilyQueue> queue, const std::vector<TypeId>& typeParams,
TypeFamilyReductionResult<TypeId> numericBinopFamilyFn(TypeId instance, const std::vector<TypeId>& typeParams,
const std::vector<TypePackId>& packParams, NotNull<TypeFamilyContext> ctx, const std::string metamethod)
{
if (typeParams.size() != 2 || !packParams.empty())
@ -723,67 +802,8 @@ TypeFamilyReductionResult<TypeId> numericBinopFamilyFn(TypeId instance, NotNull<
if (normLhsTy->isExactlyNumber() && normRhsTy->isExactlyNumber())
return {ctx->builtins->numberType, false, {}, {}};
// op (a | b) (c | d) ~ (op a (c | d)) | (op b (c | d)) ~ (op a c) | (op a d) | (op b c) | (op b d)
std::vector<TypeId> results;
bool uninhabited = false;
std::vector<TypeId> blockedTypes;
std::vector<TypeId> arguments = typeParams;
auto distributeFamilyApp = [&](const UnionType* ut, size_t argumentIndex) {
// Returning true here means we completed the loop without any problems.
for (TypeId option : ut)
{
arguments[argumentIndex] = option;
TypeFamilyReductionResult<TypeId> result = numericBinopFamilyFn(instance, queue, arguments, packParams, ctx, metamethod);
blockedTypes.insert(blockedTypes.end(), result.blockedTypes.begin(), result.blockedTypes.end());
uninhabited |= result.uninhabited;
if (result.uninhabited)
return false;
else if (!result.result)
return false;
else
results.push_back(*result.result);
}
return true;
};
const UnionType* lhsUnion = get<UnionType>(lhsTy);
const UnionType* rhsUnion = get<UnionType>(rhsTy);
if (lhsUnion || rhsUnion)
{
// TODO: We'd like to report that the type family application is too complex here.
size_t lhsUnionSize = lhsUnion ? std::distance(begin(lhsUnion), end(lhsUnion)) : 1;
size_t rhsUnionSize = rhsUnion ? std::distance(begin(rhsUnion), end(rhsUnion)) : 1;
if (size_t(DFInt::LuauTypeFamilyApplicationCartesianProductLimit) <= lhsUnionSize * rhsUnionSize)
return {std::nullopt, true, {}, {}};
if (lhsUnion && !distributeFamilyApp(lhsUnion, 0))
return {std::nullopt, uninhabited, std::move(blockedTypes), {}};
if (rhsUnion && !distributeFamilyApp(rhsUnion, 1))
return {std::nullopt, uninhabited, std::move(blockedTypes), {}};
if (results.empty())
{
// If this happens, it means `distributeFamilyApp` has improperly returned `true` even
// though there exists no arm of the union that is inhabited or have a reduced type.
ctx->ice->ice("`distributeFamilyApp` failed to add any types to the results vector?");
}
if (results.size() == 1)
return {results[0], false, {}, {}};
TypeId resultTy = ctx->arena->addType(TypeFamilyInstanceType{
NotNull{&kBuiltinTypeFamilies.unionFamily},
std::move(results),
{},
});
queue->add(resultTy);
return {resultTy, false, {}, {}};
}
if (auto result = tryDistributeTypeFamilyApp(numericBinopFamilyFn, instance, typeParams, packParams, ctx, metamethod))
return *result;
// findMetatableEntry demands the ability to emit errors, so we must give it
// the necessary state to do that, even if we intend to just eat the errors.
@ -826,7 +846,7 @@ TypeFamilyReductionResult<TypeId> numericBinopFamilyFn(TypeId instance, NotNull<
return {extracted.head.front(), false, {}, {}};
}
TypeFamilyReductionResult<TypeId> addFamilyFn(TypeId instance, NotNull<TypeFamilyQueue> queue, const std::vector<TypeId>& typeParams,
TypeFamilyReductionResult<TypeId> addFamilyFn(TypeId instance, const std::vector<TypeId>& typeParams,
const std::vector<TypePackId>& packParams, NotNull<TypeFamilyContext> ctx)
{
if (typeParams.size() != 2 || !packParams.empty())
@ -835,10 +855,10 @@ TypeFamilyReductionResult<TypeId> addFamilyFn(TypeId instance, NotNull<TypeFamil
LUAU_ASSERT(false);
}
return numericBinopFamilyFn(instance, queue, typeParams, packParams, ctx, "__add");
return numericBinopFamilyFn(instance, typeParams, packParams, ctx, "__add");
}
TypeFamilyReductionResult<TypeId> subFamilyFn(TypeId instance, NotNull<TypeFamilyQueue> queue, const std::vector<TypeId>& typeParams,
TypeFamilyReductionResult<TypeId> subFamilyFn(TypeId instance, const std::vector<TypeId>& typeParams,
const std::vector<TypePackId>& packParams, NotNull<TypeFamilyContext> ctx)
{
if (typeParams.size() != 2 || !packParams.empty())
@ -847,10 +867,10 @@ TypeFamilyReductionResult<TypeId> subFamilyFn(TypeId instance, NotNull<TypeFamil
LUAU_ASSERT(false);
}
return numericBinopFamilyFn(instance, queue, typeParams, packParams, ctx, "__sub");
return numericBinopFamilyFn(instance, typeParams, packParams, ctx, "__sub");
}
TypeFamilyReductionResult<TypeId> mulFamilyFn(TypeId instance, NotNull<TypeFamilyQueue> queue, const std::vector<TypeId>& typeParams,
TypeFamilyReductionResult<TypeId> mulFamilyFn(TypeId instance, const std::vector<TypeId>& typeParams,
const std::vector<TypePackId>& packParams, NotNull<TypeFamilyContext> ctx)
{
if (typeParams.size() != 2 || !packParams.empty())
@ -859,10 +879,10 @@ TypeFamilyReductionResult<TypeId> mulFamilyFn(TypeId instance, NotNull<TypeFamil
LUAU_ASSERT(false);
}
return numericBinopFamilyFn(instance, queue, typeParams, packParams, ctx, "__mul");
return numericBinopFamilyFn(instance, typeParams, packParams, ctx, "__mul");
}
TypeFamilyReductionResult<TypeId> divFamilyFn(TypeId instance, NotNull<TypeFamilyQueue> queue, const std::vector<TypeId>& typeParams,
TypeFamilyReductionResult<TypeId> divFamilyFn(TypeId instance, const std::vector<TypeId>& typeParams,
const std::vector<TypePackId>& packParams, NotNull<TypeFamilyContext> ctx)
{
if (typeParams.size() != 2 || !packParams.empty())
@ -871,10 +891,10 @@ TypeFamilyReductionResult<TypeId> divFamilyFn(TypeId instance, NotNull<TypeFamil
LUAU_ASSERT(false);
}
return numericBinopFamilyFn(instance, queue, typeParams, packParams, ctx, "__div");
return numericBinopFamilyFn(instance, typeParams, packParams, ctx, "__div");
}
TypeFamilyReductionResult<TypeId> idivFamilyFn(TypeId instance, NotNull<TypeFamilyQueue> queue, const std::vector<TypeId>& typeParams,
TypeFamilyReductionResult<TypeId> idivFamilyFn(TypeId instance, const std::vector<TypeId>& typeParams,
const std::vector<TypePackId>& packParams, NotNull<TypeFamilyContext> ctx)
{
if (typeParams.size() != 2 || !packParams.empty())
@ -883,10 +903,10 @@ TypeFamilyReductionResult<TypeId> idivFamilyFn(TypeId instance, NotNull<TypeFami
LUAU_ASSERT(false);
}
return numericBinopFamilyFn(instance, queue, typeParams, packParams, ctx, "__idiv");
return numericBinopFamilyFn(instance, typeParams, packParams, ctx, "__idiv");
}
TypeFamilyReductionResult<TypeId> powFamilyFn(TypeId instance, NotNull<TypeFamilyQueue> queue, const std::vector<TypeId>& typeParams,
TypeFamilyReductionResult<TypeId> powFamilyFn(TypeId instance, const std::vector<TypeId>& typeParams,
const std::vector<TypePackId>& packParams, NotNull<TypeFamilyContext> ctx)
{
if (typeParams.size() != 2 || !packParams.empty())
@ -895,10 +915,10 @@ TypeFamilyReductionResult<TypeId> powFamilyFn(TypeId instance, NotNull<TypeFamil
LUAU_ASSERT(false);
}
return numericBinopFamilyFn(instance, queue, typeParams, packParams, ctx, "__pow");
return numericBinopFamilyFn(instance, typeParams, packParams, ctx, "__pow");
}
TypeFamilyReductionResult<TypeId> modFamilyFn(TypeId instance, NotNull<TypeFamilyQueue> queue, const std::vector<TypeId>& typeParams,
TypeFamilyReductionResult<TypeId> modFamilyFn(TypeId instance, const std::vector<TypeId>& typeParams,
const std::vector<TypePackId>& packParams, NotNull<TypeFamilyContext> ctx)
{
if (typeParams.size() != 2 || !packParams.empty())
@ -907,10 +927,10 @@ TypeFamilyReductionResult<TypeId> modFamilyFn(TypeId instance, NotNull<TypeFamil
LUAU_ASSERT(false);
}
return numericBinopFamilyFn(instance, queue, typeParams, packParams, ctx, "__mod");
return numericBinopFamilyFn(instance, typeParams, packParams, ctx, "__mod");
}
TypeFamilyReductionResult<TypeId> concatFamilyFn(TypeId instance, NotNull<TypeFamilyQueue> queue, const std::vector<TypeId>& typeParams,
TypeFamilyReductionResult<TypeId> concatFamilyFn(TypeId instance, const std::vector<TypeId>& typeParams,
const std::vector<TypePackId>& packParams, NotNull<TypeFamilyContext> ctx)
{
if (typeParams.size() != 2 || !packParams.empty())
@ -922,6 +942,10 @@ TypeFamilyReductionResult<TypeId> concatFamilyFn(TypeId instance, NotNull<TypeFa
TypeId lhsTy = follow(typeParams.at(0));
TypeId rhsTy = follow(typeParams.at(1));
// isPending of `lhsTy` or `rhsTy` would return true, even if it cycles. We want a different answer for that.
if (lhsTy == instance || rhsTy == instance)
return {ctx->builtins->neverType, false, {}, {}};
// check to see if both operand types are resolved enough, and wait to reduce if not
if (isPending(lhsTy, ctx->solver))
return {std::nullopt, false, {lhsTy}, {}};
@ -962,6 +986,9 @@ TypeFamilyReductionResult<TypeId> concatFamilyFn(TypeId instance, NotNull<TypeFa
if ((normLhsTy->isSubtypeOfString() || normLhsTy->isExactlyNumber()) && (normRhsTy->isSubtypeOfString() || normRhsTy->isExactlyNumber()))
return {ctx->builtins->stringType, false, {}, {}};
if (auto result = tryDistributeTypeFamilyApp(concatFamilyFn, instance, typeParams, packParams, ctx))
return *result;
// findMetatableEntry demands the ability to emit errors, so we must give it
// the necessary state to do that, even if we intend to just eat the errors.
ErrorVec dummy;
@ -1011,7 +1038,7 @@ TypeFamilyReductionResult<TypeId> concatFamilyFn(TypeId instance, NotNull<TypeFa
return {ctx->builtins->stringType, false, {}, {}};
}
TypeFamilyReductionResult<TypeId> andFamilyFn(TypeId instance, NotNull<TypeFamilyQueue> queue, const std::vector<TypeId>& typeParams,
TypeFamilyReductionResult<TypeId> andFamilyFn(TypeId instance, const std::vector<TypeId>& typeParams,
const std::vector<TypePackId>& packParams, NotNull<TypeFamilyContext> ctx)
{
if (typeParams.size() != 2 || !packParams.empty())
@ -1062,7 +1089,7 @@ TypeFamilyReductionResult<TypeId> andFamilyFn(TypeId instance, NotNull<TypeFamil
return {overallResult.result, false, std::move(blockedTypes), {}};
}
TypeFamilyReductionResult<TypeId> orFamilyFn(TypeId instance, NotNull<TypeFamilyQueue> queue, const std::vector<TypeId>& typeParams,
TypeFamilyReductionResult<TypeId> orFamilyFn(TypeId instance, const std::vector<TypeId>& typeParams,
const std::vector<TypePackId>& packParams, NotNull<TypeFamilyContext> ctx)
{
if (typeParams.size() != 2 || !packParams.empty())
@ -1113,7 +1140,7 @@ TypeFamilyReductionResult<TypeId> orFamilyFn(TypeId instance, NotNull<TypeFamily
return {overallResult.result, false, std::move(blockedTypes), {}};
}
static TypeFamilyReductionResult<TypeId> comparisonFamilyFn(TypeId instance, NotNull<TypeFamilyQueue> queue, const std::vector<TypeId>& typeParams,
static TypeFamilyReductionResult<TypeId> comparisonFamilyFn(TypeId instance, const std::vector<TypeId>& typeParams,
const std::vector<TypePackId>& packParams, NotNull<TypeFamilyContext> ctx, const std::string metamethod)
{
@ -1126,6 +1153,9 @@ static TypeFamilyReductionResult<TypeId> comparisonFamilyFn(TypeId instance, Not
TypeId lhsTy = follow(typeParams.at(0));
TypeId rhsTy = follow(typeParams.at(1));
if (lhsTy == instance || rhsTy == instance)
return {ctx->builtins->neverType, false, {}, {}};
if (isPending(lhsTy, ctx->solver))
return {std::nullopt, false, {lhsTy}, {}};
else if (isPending(rhsTy, ctx->solver))
@ -1207,6 +1237,9 @@ static TypeFamilyReductionResult<TypeId> comparisonFamilyFn(TypeId instance, Not
if (normLhsTy->isExactlyNumber() && normRhsTy->isExactlyNumber())
return {ctx->builtins->booleanType, false, {}, {}};
if (auto result = tryDistributeTypeFamilyApp(comparisonFamilyFn, instance, typeParams, packParams, ctx, metamethod))
return *result;
// findMetatableEntry demands the ability to emit errors, so we must give it
// the necessary state to do that, even if we intend to just eat the errors.
ErrorVec dummy;
@ -1246,7 +1279,7 @@ static TypeFamilyReductionResult<TypeId> comparisonFamilyFn(TypeId instance, Not
return {ctx->builtins->booleanType, false, {}, {}};
}
TypeFamilyReductionResult<TypeId> ltFamilyFn(TypeId instance, NotNull<TypeFamilyQueue> queue, const std::vector<TypeId>& typeParams,
TypeFamilyReductionResult<TypeId> ltFamilyFn(TypeId instance, const std::vector<TypeId>& typeParams,
const std::vector<TypePackId>& packParams, NotNull<TypeFamilyContext> ctx)
{
if (typeParams.size() != 2 || !packParams.empty())
@ -1255,10 +1288,10 @@ TypeFamilyReductionResult<TypeId> ltFamilyFn(TypeId instance, NotNull<TypeFamily
LUAU_ASSERT(false);
}
return comparisonFamilyFn(instance, queue, typeParams, packParams, ctx, "__lt");
return comparisonFamilyFn(instance, typeParams, packParams, ctx, "__lt");
}
TypeFamilyReductionResult<TypeId> leFamilyFn(TypeId instance, NotNull<TypeFamilyQueue> queue, const std::vector<TypeId>& typeParams,
TypeFamilyReductionResult<TypeId> leFamilyFn(TypeId instance, const std::vector<TypeId>& typeParams,
const std::vector<TypePackId>& packParams, NotNull<TypeFamilyContext> ctx)
{
if (typeParams.size() != 2 || !packParams.empty())
@ -1267,10 +1300,10 @@ TypeFamilyReductionResult<TypeId> leFamilyFn(TypeId instance, NotNull<TypeFamily
LUAU_ASSERT(false);
}
return comparisonFamilyFn(instance, queue, typeParams, packParams, ctx, "__le");
return comparisonFamilyFn(instance, typeParams, packParams, ctx, "__le");
}
TypeFamilyReductionResult<TypeId> eqFamilyFn(TypeId instance, NotNull<TypeFamilyQueue> queue, const std::vector<TypeId>& typeParams,
TypeFamilyReductionResult<TypeId> eqFamilyFn(TypeId instance, const std::vector<TypeId>& typeParams,
const std::vector<TypePackId>& packParams, NotNull<TypeFamilyContext> ctx)
{
if (typeParams.size() != 2 || !packParams.empty())
@ -1407,7 +1440,7 @@ struct FindRefinementBlockers : TypeOnceVisitor
};
TypeFamilyReductionResult<TypeId> refineFamilyFn(TypeId instance, NotNull<TypeFamilyQueue> queue, const std::vector<TypeId>& typeParams,
TypeFamilyReductionResult<TypeId> refineFamilyFn(TypeId instance, const std::vector<TypeId>& typeParams,
const std::vector<TypePackId>& packParams, NotNull<TypeFamilyContext> ctx)
{
if (typeParams.size() != 2 || !packParams.empty())
@ -1480,7 +1513,7 @@ TypeFamilyReductionResult<TypeId> refineFamilyFn(TypeId instance, NotNull<TypeFa
return {resultTy, false, {}, {}};
}
TypeFamilyReductionResult<TypeId> singletonFamilyFn(TypeId instance, NotNull<TypeFamilyQueue> queue, const std::vector<TypeId>& typeParams,
TypeFamilyReductionResult<TypeId> singletonFamilyFn(TypeId instance, const std::vector<TypeId>& typeParams,
const std::vector<TypePackId>& packParams, NotNull<TypeFamilyContext> ctx)
{
if (typeParams.size() != 1 || !packParams.empty())
@ -1517,7 +1550,7 @@ TypeFamilyReductionResult<TypeId> singletonFamilyFn(TypeId instance, NotNull<Typ
return {ctx->builtins->unknownType, false, {}, {}};
}
TypeFamilyReductionResult<TypeId> unionFamilyFn(TypeId instance, NotNull<TypeFamilyQueue> queue, const std::vector<TypeId>& typeParams,
TypeFamilyReductionResult<TypeId> unionFamilyFn(TypeId instance, const std::vector<TypeId>& typeParams,
const std::vector<TypePackId>& packParams, NotNull<TypeFamilyContext> ctx)
{
if (!packParams.empty())
@ -1578,7 +1611,7 @@ TypeFamilyReductionResult<TypeId> unionFamilyFn(TypeId instance, NotNull<TypeFam
}
TypeFamilyReductionResult<TypeId> intersectFamilyFn(TypeId instance, NotNull<TypeFamilyQueue> queue, const std::vector<TypeId>& typeParams,
TypeFamilyReductionResult<TypeId> intersectFamilyFn(TypeId instance, const std::vector<TypeId>& typeParams,
const std::vector<TypePackId>& packParams, NotNull<TypeFamilyContext> ctx)
{
if (!packParams.empty())
@ -1802,7 +1835,7 @@ TypeFamilyReductionResult<TypeId> keyofFamilyImpl(
return {ctx->arena->addType(UnionType{singletons}), false, {}, {}};
}
TypeFamilyReductionResult<TypeId> keyofFamilyFn(TypeId instance, NotNull<TypeFamilyQueue> queue, const std::vector<TypeId>& typeParams,
TypeFamilyReductionResult<TypeId> keyofFamilyFn(TypeId instance, const std::vector<TypeId>& typeParams,
const std::vector<TypePackId>& packParams, NotNull<TypeFamilyContext> ctx)
{
if (typeParams.size() != 1 || !packParams.empty())
@ -1814,7 +1847,7 @@ TypeFamilyReductionResult<TypeId> keyofFamilyFn(TypeId instance, NotNull<TypeFam
return keyofFamilyImpl(typeParams, packParams, ctx, /* isRaw */ false);
}
TypeFamilyReductionResult<TypeId> rawkeyofFamilyFn(TypeId instance, NotNull<TypeFamilyQueue> queue, const std::vector<TypeId>& typeParams,
TypeFamilyReductionResult<TypeId> rawkeyofFamilyFn(TypeId instance, const std::vector<TypeId>& typeParams,
const std::vector<TypePackId>& packParams, NotNull<TypeFamilyContext> ctx)
{
if (typeParams.size() != 1 || !packParams.empty())

View File

@ -38,6 +38,59 @@ bool occursCheck(TypeId needle, TypeId haystack)
return false;
}
// FIXME: Property is quite large.
//
// Returning it on the stack like this isn't great. We'd like to just return a
// const Property*, but we mint a property of type any if the subject type is
// any.
std::optional<Property> findTableProperty(NotNull<BuiltinTypes> builtinTypes, ErrorVec& errors, TypeId ty, const std::string& name, Location location)
{
if (get<AnyType>(ty))
return Property::rw(ty);
if (const TableType* tableType = getTableType(ty))
{
const auto& it = tableType->props.find(name);
if (it != tableType->props.end())
return it->second;
}
std::optional<TypeId> mtIndex = findMetatableEntry(builtinTypes, errors, ty, "__index", location);
int count = 0;
while (mtIndex)
{
TypeId index = follow(*mtIndex);
if (count >= 100)
return std::nullopt;
++count;
if (const auto& itt = getTableType(index))
{
const auto& fit = itt->props.find(name);
if (fit != itt->props.end())
return fit->second.type();
}
else if (const auto& itf = get<FunctionType>(index))
{
std::optional<TypeId> r = first(follow(itf->retTypes));
if (!r)
return builtinTypes->nilType;
else
return *r;
}
else if (get<AnyType>(index))
return builtinTypes->anyType;
else
errors.push_back(TypeError{location, GenericError{"__index should either be a function or table. Got " + toString(index)}});
mtIndex = findMetatableEntry(builtinTypes, errors, *mtIndex, "__index", location);
}
return std::nullopt;
}
std::optional<TypeId> findMetatableEntry(
NotNull<BuiltinTypes> builtinTypes, ErrorVec& errors, TypeId type, const std::string& entry, Location location)
{

View File

@ -256,12 +256,16 @@ void setupState(lua_State* L)
void setupArguments(lua_State* L, int argc, char** argv)
{
lua_checkstack(L, argc);
for (int i = 0; i < argc; ++i)
lua_pushstring(L, argv[i]);
}
std::string runCode(lua_State* L, const std::string& source)
{
lua_checkstack(L, LUA_MINSTACK);
std::string bytecode = Luau::compile(source, copts());
if (luau_load(L, "=stdin", bytecode.data(), bytecode.size(), 0) != 0)
@ -432,6 +436,8 @@ static void completeIndexer(lua_State* L, const std::string& editBuffer, const A
std::string_view lookup = editBuffer;
bool completeOnlyFunctions = false;
lua_checkstack(L, LUA_MINSTACK);
// Push the global variable table to begin the search
lua_pushvalue(L, LUA_GLOBALSINDEX);

View File

@ -92,7 +92,7 @@ struct HostIrHooks
// Guards should take a VM exit to 'pcpos'
HostVectorAccessHandler vectorAccess = nullptr;
// Handle namecalled performed on a vector value
// Handle namecall performed on a vector value
// 'sourceReg' (self argument) is guaranteed to be a vector
// All other arguments can be of any type
// Guards should take a VM exit to 'pcpos'
@ -103,6 +103,9 @@ struct CompilationOptions
{
unsigned int flags = 0;
HostIrHooks hooks;
// null-terminated array of userdata types names that might have custom lowering
const char* const* userdataTypes = nullptr;
};
struct CompilationStats
@ -163,6 +166,12 @@ void create(lua_State* L, SharedCodeGenContext* codeGenContext);
// Enable or disable native execution according to `enabled` argument
void setNativeExecutionEnabled(lua_State* L, bool enabled);
// Given a name, this function must return the index of the type which matches the type array used all CompilationOptions and AssemblyOptions
// If the type is unknown, 0xff has to be returned
using UserdataRemapperCallback = uint8_t(void* context, const char* name, size_t nameLength);
void setUserdataRemapper(lua_State* L, void* context, UserdataRemapperCallback cb);
using ModuleId = std::array<uint8_t, 16>;
// Builds target function and all inner functions

View File

@ -31,9 +31,11 @@ void toString(IrToStringContext& ctx, IrOp op);
void toString(std::string& result, IrConst constant);
const char* getBytecodeTypeName(uint8_t type);
const char* getBytecodeTypeName_DEPRECATED(uint8_t type);
const char* getBytecodeTypeName(uint8_t type, const char* const* userdataTypes);
void toString(std::string& result, const BytecodeTypes& bcTypes);
void toString_DEPRECATED(std::string& result, const BytecodeTypes& bcTypes);
void toString(std::string& result, const BytecodeTypes& bcTypes, const char* const* userdataTypes);
void toStringDetailed(
IrToStringContext& ctx, const IrBlock& block, uint32_t blockIdx, const IrInst& inst, uint32_t instIdx, IncludeUseInfo includeUseInfo);

View File

@ -241,6 +241,10 @@ IrValueKind getCmdValueKind(IrCmd cmd);
bool isGCO(uint8_t tag);
// Optional bit has to be cleared at call site, otherwise, this will return 'false' for 'userdata?'
bool isUserdataBytecodeType(uint8_t ty);
bool isCustomUserdataBytecodeType(uint8_t ty);
// Manually add or remove use of an operand
void addUse(IrFunction& function, IrOp op);
void removeUse(IrFunction& function, IrOp op);

View File

@ -14,8 +14,6 @@
LUAU_FASTFLAG(LuauCodegenDirectUserdataFlow)
LUAU_FASTFLAG(LuauLoadTypeInfo) // Because new VM typeinfo load changes the format used by Codegen, same flag is used
LUAU_FASTFLAGVARIABLE(LuauCodegenTypeInfo, false) // New analysis is flagged separately
LUAU_FASTFLAG(LuauTypeInfoLookupImprovement)
LUAU_FASTFLAGVARIABLE(LuauCodegenVectorMispredictFix, false)
LUAU_FASTFLAGVARIABLE(LuauCodegenAnalyzeHostVectorOps, false)
LUAU_FASTFLAGVARIABLE(LuauCodegenLoadTypeUpvalCheck, false)
@ -68,21 +66,13 @@ void loadBytecodeTypeInfo(IrFunction& function)
Proto* proto = function.proto;
if (FFlag::LuauTypeInfoLookupImprovement)
{
if (!proto)
return;
}
else
{
if (!proto || !proto->typeinfo)
return;
}
if (!proto)
return;
BytecodeTypeInfo& typeInfo = function.bcTypeInfo;
// If there is no typeinfo, we generate default values for arguments and upvalues
if (FFlag::LuauTypeInfoLookupImprovement && !proto->typeinfo)
if (!proto->typeinfo)
{
typeInfo.argumentTypes.resize(proto->numparams, LBC_TYPE_ANY);
typeInfo.upvalueTypes.resize(proto->nups, LBC_TYPE_ANY);
@ -150,8 +140,6 @@ void loadBytecodeTypeInfo(IrFunction& function)
static void prepareRegTypeInfoLookups(BytecodeTypeInfo& typeInfo)
{
CODEGEN_ASSERT(FFlag::LuauTypeInfoLookupImprovement);
// Sort by register first, then by end PC
std::sort(typeInfo.regTypes.begin(), typeInfo.regTypes.end(), [](const BytecodeRegTypeInfo& a, const BytecodeRegTypeInfo& b) {
if (a.reg != b.reg)
@ -186,39 +174,26 @@ static BytecodeRegTypeInfo* findRegType(BytecodeTypeInfo& info, uint8_t reg, int
{
CODEGEN_ASSERT(FFlag::LuauCodegenTypeInfo);
if (FFlag::LuauTypeInfoLookupImprovement)
{
auto b = info.regTypes.begin() + info.regTypeOffsets[reg];
auto e = info.regTypes.begin() + info.regTypeOffsets[reg + 1];
// Doen't have info
if (b == e)
return nullptr;
// No info after the last live range
if (pc >= (e - 1)->endpc)
return nullptr;
for (auto it = b; it != e; ++it)
{
CODEGEN_ASSERT(it->reg == reg);
if (pc >= it->startpc && pc < it->endpc)
return &*it;
}
auto b = info.regTypes.begin() + info.regTypeOffsets[reg];
auto e = info.regTypes.begin() + info.regTypeOffsets[reg + 1];
// Doen't have info
if (b == e)
return nullptr;
}
else
{
for (BytecodeRegTypeInfo& el : info.regTypes)
{
if (reg == el.reg && pc >= el.startpc && pc < el.endpc)
return &el;
}
// No info after the last live range
if (pc >= (e - 1)->endpc)
return nullptr;
for (auto it = b; it != e; ++it)
{
CODEGEN_ASSERT(it->reg == reg);
if (pc >= it->startpc && pc < it->endpc)
return &*it;
}
return nullptr;
}
static void refineRegType(BytecodeTypeInfo& info, uint8_t reg, int pc, uint8_t ty)
@ -233,7 +208,7 @@ static void refineRegType(BytecodeTypeInfo& info, uint8_t reg, int pc, uint8_t t
if (regType->type == LBC_TYPE_ANY)
regType->type = ty;
}
else if (FFlag::LuauTypeInfoLookupImprovement && reg < info.argumentTypes.size())
else if (reg < info.argumentTypes.size())
{
if (info.argumentTypes[reg] == LBC_TYPE_ANY)
info.argumentTypes[reg] = ty;
@ -627,8 +602,7 @@ void analyzeBytecodeTypes(IrFunction& function, const HostIrHooks& hostHooks)
BytecodeTypeInfo& bcTypeInfo = function.bcTypeInfo;
if (FFlag::LuauTypeInfoLookupImprovement)
prepareRegTypeInfoLookups(bcTypeInfo);
prepareRegTypeInfoLookups(bcTypeInfo);
// Setup our current knowledge of type tags based on arguments
uint8_t regTags[256];
@ -786,32 +760,22 @@ void analyzeBytecodeTypes(IrFunction& function, const HostIrHooks& hostHooks)
regTags[ra] = LBC_TYPE_ANY;
if (FFlag::LuauCodegenVectorMispredictFix)
if (bcType.a == LBC_TYPE_VECTOR)
{
if (bcType.a == LBC_TYPE_VECTOR)
TString* str = gco2ts(function.proto->k[kc].value.gc);
const char* field = getstr(str);
if (str->len == 1)
{
TString* str = gco2ts(function.proto->k[kc].value.gc);
const char* field = getstr(str);
// Same handling as LOP_GETTABLEKS block in lvmexecute.cpp - case-insensitive comparison with "X" / "Y" / "Z"
char ch = field[0] | ' ';
if (str->len == 1)
{
// Same handling as LOP_GETTABLEKS block in lvmexecute.cpp - case-insensitive comparison with "X" / "Y" / "Z"
char ch = field[0] | ' ';
if (ch == 'x' || ch == 'y' || ch == 'z')
regTags[ra] = LBC_TYPE_NUMBER;
}
if (FFlag::LuauCodegenAnalyzeHostVectorOps && regTags[ra] == LBC_TYPE_ANY && hostHooks.vectorAccessBytecodeType)
regTags[ra] = hostHooks.vectorAccessBytecodeType(field, str->len);
if (ch == 'x' || ch == 'y' || ch == 'z')
regTags[ra] = LBC_TYPE_NUMBER;
}
}
else
{
// Assuming that vector component is being indexed
// TODO: check what key is used
if (bcType.a == LBC_TYPE_VECTOR)
regTags[ra] = LBC_TYPE_NUMBER;
if (FFlag::LuauCodegenAnalyzeHostVectorOps && regTags[ra] == LBC_TYPE_ANY && hostHooks.vectorAccessBytecodeType)
regTags[ra] = hostHooks.vectorAccessBytecodeType(field, str->len);
}
bcType.result = regTags[ra];

View File

@ -13,7 +13,7 @@
#include "lapi.h"
LUAU_FASTFLAG(LuauCodegenTypeInfo)
LUAU_FASTFLAGVARIABLE(LuauCodegenIrTypeNames, false)
LUAU_FASTFLAG(LuauLoadUserdataInfo)
namespace Luau
{
@ -22,8 +22,6 @@ namespace CodeGen
static const LocVar* tryFindLocal(const Proto* proto, int reg, int pcpos)
{
CODEGEN_ASSERT(FFlag::LuauCodegenIrTypeNames);
for (int i = 0; i < proto->sizelocvars; i++)
{
const LocVar& local = proto->locvars[i];
@ -37,8 +35,6 @@ static const LocVar* tryFindLocal(const Proto* proto, int reg, int pcpos)
const char* tryFindLocalName(const Proto* proto, int reg, int pcpos)
{
CODEGEN_ASSERT(FFlag::LuauCodegenIrTypeNames);
const LocVar* var = tryFindLocal(proto, reg, pcpos);
if (var && var->varname)
@ -49,8 +45,6 @@ const char* tryFindLocalName(const Proto* proto, int reg, int pcpos)
const char* tryFindUpvalueName(const Proto* proto, int upval)
{
CODEGEN_ASSERT(FFlag::LuauCodegenIrTypeNames);
if (proto->upvalues)
{
CODEGEN_ASSERT(upval < proto->sizeupvalues);
@ -72,22 +66,10 @@ static void logFunctionHeader(AssemblyBuilder& build, Proto* proto)
for (int i = 0; i < proto->numparams; i++)
{
if (FFlag::LuauCodegenIrTypeNames)
{
if (const char* name = tryFindLocalName(proto, i, 0))
build.logAppend("%s%s", i == 0 ? "" : ", ", name);
else
build.logAppend("%s$arg%d", i == 0 ? "" : ", ", i);
}
if (const char* name = tryFindLocalName(proto, i, 0))
build.logAppend("%s%s", i == 0 ? "" : ", ", name);
else
{
LocVar* var = proto->locvars ? &proto->locvars[proto->sizelocvars - proto->numparams + i] : nullptr;
if (var && var->varname)
build.logAppend("%s%s", i == 0 ? "" : ", ", getstr(var->varname));
else
build.logAppend("%s$arg%d", i == 0 ? "" : ", ", i);
}
build.logAppend("%s$arg%d", i == 0 ? "" : ", ", i);
}
if (proto->numparams != 0 && proto->is_vararg)
@ -102,9 +84,10 @@ static void logFunctionHeader(AssemblyBuilder& build, Proto* proto)
}
template<typename AssemblyBuilder>
static void logFunctionTypes(AssemblyBuilder& build, const IrFunction& function)
static void logFunctionTypes_DEPRECATED(AssemblyBuilder& build, const IrFunction& function)
{
CODEGEN_ASSERT(FFlag::LuauCodegenTypeInfo);
CODEGEN_ASSERT(!FFlag::LuauLoadUserdataInfo);
const BytecodeTypeInfo& typeInfo = function.bcTypeInfo;
@ -112,20 +95,12 @@ static void logFunctionTypes(AssemblyBuilder& build, const IrFunction& function)
{
uint8_t ty = typeInfo.argumentTypes[i];
if (FFlag::LuauCodegenIrTypeNames)
if (ty != LBC_TYPE_ANY)
{
if (ty != LBC_TYPE_ANY)
{
if (const char* name = tryFindLocalName(function.proto, int(i), 0))
build.logAppend("; R%d: %s [argument '%s']\n", int(i), getBytecodeTypeName(ty), name);
else
build.logAppend("; R%d: %s [argument]\n", int(i), getBytecodeTypeName(ty));
}
}
else
{
if (ty != LBC_TYPE_ANY)
build.logAppend("; R%d: %s [argument]\n", int(i), getBytecodeTypeName(ty));
if (const char* name = tryFindLocalName(function.proto, int(i), 0))
build.logAppend("; R%d: %s [argument '%s']\n", int(i), getBytecodeTypeName_DEPRECATED(ty), name);
else
build.logAppend("; R%d: %s [argument]\n", int(i), getBytecodeTypeName_DEPRECATED(ty));
}
}
@ -133,38 +108,76 @@ static void logFunctionTypes(AssemblyBuilder& build, const IrFunction& function)
{
uint8_t ty = typeInfo.upvalueTypes[i];
if (FFlag::LuauCodegenIrTypeNames)
if (ty != LBC_TYPE_ANY)
{
if (ty != LBC_TYPE_ANY)
{
if (const char* name = tryFindUpvalueName(function.proto, int(i)))
build.logAppend("; U%d: %s ['%s']\n", int(i), getBytecodeTypeName(ty), name);
else
build.logAppend("; U%d: %s\n", int(i), getBytecodeTypeName(ty));
}
}
else
{
if (ty != LBC_TYPE_ANY)
build.logAppend("; U%d: %s\n", int(i), getBytecodeTypeName(ty));
if (const char* name = tryFindUpvalueName(function.proto, int(i)))
build.logAppend("; U%d: %s ['%s']\n", int(i), getBytecodeTypeName_DEPRECATED(ty), name);
else
build.logAppend("; U%d: %s\n", int(i), getBytecodeTypeName_DEPRECATED(ty));
}
}
for (const BytecodeRegTypeInfo& el : typeInfo.regTypes)
{
if (FFlag::LuauCodegenIrTypeNames)
{
// Using last active position as the PC because 'startpc' for type info is before local is initialized
if (const char* name = tryFindLocalName(function.proto, el.reg, el.endpc - 1))
build.logAppend("; R%d: %s from %d to %d [local '%s']\n", el.reg, getBytecodeTypeName(el.type), el.startpc, el.endpc, name);
else
build.logAppend("; R%d: %s from %d to %d\n", el.reg, getBytecodeTypeName(el.type), el.startpc, el.endpc);
}
// Using last active position as the PC because 'startpc' for type info is before local is initialized
if (const char* name = tryFindLocalName(function.proto, el.reg, el.endpc - 1))
build.logAppend("; R%d: %s from %d to %d [local '%s']\n", el.reg, getBytecodeTypeName_DEPRECATED(el.type), el.startpc, el.endpc, name);
else
build.logAppend("; R%d: %s from %d to %d\n", el.reg, getBytecodeTypeName_DEPRECATED(el.type), el.startpc, el.endpc);
}
}
template<typename AssemblyBuilder>
static void logFunctionTypes(AssemblyBuilder& build, const IrFunction& function, const char* const* userdataTypes)
{
CODEGEN_ASSERT(FFlag::LuauCodegenTypeInfo);
CODEGEN_ASSERT(FFlag::LuauLoadUserdataInfo);
const BytecodeTypeInfo& typeInfo = function.bcTypeInfo;
for (size_t i = 0; i < typeInfo.argumentTypes.size(); i++)
{
uint8_t ty = typeInfo.argumentTypes[i];
const char* type = getBytecodeTypeName(ty, userdataTypes);
const char* optional = (ty & LBC_TYPE_OPTIONAL_BIT) != 0 ? "?" : "";
if (ty != LBC_TYPE_ANY)
{
build.logAppend("; R%d: %s from %d to %d\n", el.reg, getBytecodeTypeName(el.type), el.startpc, el.endpc);
if (const char* name = tryFindLocalName(function.proto, int(i), 0))
build.logAppend("; R%d: %s%s [argument '%s']\n", int(i), type, optional, name);
else
build.logAppend("; R%d: %s%s [argument]\n", int(i), type, optional);
}
}
for (size_t i = 0; i < typeInfo.upvalueTypes.size(); i++)
{
uint8_t ty = typeInfo.upvalueTypes[i];
const char* type = getBytecodeTypeName(ty, userdataTypes);
const char* optional = (ty & LBC_TYPE_OPTIONAL_BIT) != 0 ? "?" : "";
if (ty != LBC_TYPE_ANY)
{
if (const char* name = tryFindUpvalueName(function.proto, int(i)))
build.logAppend("; U%d: %s%s ['%s']\n", int(i), type, optional, name);
else
build.logAppend("; U%d: %s%s\n", int(i), type, optional);
}
}
for (const BytecodeRegTypeInfo& el : typeInfo.regTypes)
{
const char* type = getBytecodeTypeName(el.type, userdataTypes);
const char* optional = (el.type & LBC_TYPE_OPTIONAL_BIT) != 0 ? "?" : "";
// Using last active position as the PC because 'startpc' for type info is before local is initialized
if (const char* name = tryFindLocalName(function.proto, el.reg, el.endpc - 1))
build.logAppend("; R%d: %s%s from %d to %d [local '%s']\n", el.reg, type, optional, el.startpc, el.endpc, name);
else
build.logAppend("; R%d: %s%s from %d to %d\n", el.reg, type, optional, el.startpc, el.endpc);
}
}
unsigned getInstructionCount(const Instruction* insns, const unsigned size)
@ -224,7 +237,12 @@ static std::string getAssemblyImpl(AssemblyBuilder& build, const TValue* func, A
logFunctionHeader(build, p);
if (FFlag::LuauCodegenTypeInfo && options.includeIrTypes)
logFunctionTypes(build, ir.function);
{
if (FFlag::LuauLoadUserdataInfo)
logFunctionTypes(build, ir.function, options.compilationOptions.userdataTypes);
else
logFunctionTypes_DEPRECATED(build, ir.function);
}
CodeGenCompilationResult result = CodeGenCompilationResult::Success;

View File

@ -612,5 +612,29 @@ void setNativeExecutionEnabled(lua_State* L, bool enabled)
L->global->ecb.enter = enabled ? onEnter : onEnterDisabled;
}
static uint8_t userdataRemapperWrap(lua_State* L, const char* str, size_t len)
{
if (BaseCodeGenContext* codegenCtx = getCodeGenContext(L))
{
uint8_t index = codegenCtx->userdataRemapper(codegenCtx->userdataRemappingContext, str, len);
if (index < (LBC_TYPE_TAGGED_USERDATA_END - LBC_TYPE_TAGGED_USERDATA_BASE))
return LBC_TYPE_TAGGED_USERDATA_BASE + index;
}
return LBC_TYPE_USERDATA;
}
void setUserdataRemapper(lua_State* L, void* context, UserdataRemapperCallback cb)
{
if (BaseCodeGenContext* codegenCtx = getCodeGenContext(L))
{
codegenCtx->userdataRemappingContext = context;
codegenCtx->userdataRemapper = cb;
L->global->ecb.gettypemapping = cb ? userdataRemapperWrap : nullptr;
}
}
} // namespace CodeGen
} // namespace Luau

View File

@ -50,6 +50,9 @@ public:
uint8_t* gateData = nullptr;
size_t gateDataSize = 0;
void* userdataRemappingContext = nullptr;
UserdataRemapperCallback* userdataRemapper = nullptr;
NativeContext context;
};

View File

@ -28,6 +28,7 @@ LUAU_FASTINT(CodegenHeuristicsInstructionLimit)
LUAU_FASTINT(CodegenHeuristicsBlockLimit)
LUAU_FASTINT(CodegenHeuristicsBlockInstructionLimit)
LUAU_FASTFLAG(LuauCodegenRemoveDeadStores5)
LUAU_FASTFLAG(LuauLoadUserdataInfo)
namespace Luau
{
@ -149,7 +150,11 @@ inline bool lowerImpl(AssemblyBuilder& build, IrLowering& lowering, IrFunction&
if (bcTypes.result != LBC_TYPE_ANY || bcTypes.a != LBC_TYPE_ANY || bcTypes.b != LBC_TYPE_ANY || bcTypes.c != LBC_TYPE_ANY)
{
toString(ctx.result, bcTypes);
if (FFlag::LuauLoadUserdataInfo)
toString(ctx.result, bcTypes, options.compilationOptions.userdataTypes);
else
toString_DEPRECATED(ctx.result, bcTypes);
build.logAppend("\n");
}
}

View File

@ -14,8 +14,8 @@
#include <string.h>
LUAU_FASTFLAG(LuauLoadTypeInfo) // Because new VM typeinfo load changes the format used by Codegen, same flag is used
LUAU_FASTFLAG(LuauTypeInfoLookupImprovement)
LUAU_FASTFLAG(LuauCodegenAnalyzeHostVectorOps)
LUAU_FASTFLAG(LuauLoadUserdataInfo)
namespace Luau
{
@ -119,20 +119,13 @@ static bool hasTypedParameters(const BytecodeTypeInfo& typeInfo)
{
CODEGEN_ASSERT(FFlag::LuauLoadTypeInfo);
if (FFlag::LuauTypeInfoLookupImprovement)
for (auto el : typeInfo.argumentTypes)
{
for (auto el : typeInfo.argumentTypes)
{
if (el != LBC_TYPE_ANY)
return true;
}
if (el != LBC_TYPE_ANY)
return true;
}
return false;
}
else
{
return !typeInfo.argumentTypes.empty();
}
return false;
}
static void buildArgumentTypeChecks(IrBuilder& build)
@ -197,6 +190,19 @@ static void buildArgumentTypeChecks(IrBuilder& build)
case LBC_TYPE_BUFFER:
build.inst(IrCmd::CHECK_TAG, load, build.constTag(LUA_TBUFFER), build.vmExit(kVmExitEntryGuardPc));
break;
default:
if (FFlag::LuauLoadUserdataInfo)
{
if (tag >= LBC_TYPE_TAGGED_USERDATA_BASE && tag < LBC_TYPE_TAGGED_USERDATA_END)
{
build.inst(IrCmd::CHECK_TAG, load, build.constTag(LUA_TUSERDATA), build.vmExit(kVmExitEntryGuardPc));
}
else
{
CODEGEN_ASSERT(!"unknown argument type tag");
}
}
break;
}
if (optional)

View File

@ -7,6 +7,8 @@
#include <stdarg.h>
LUAU_FASTFLAG(LuauLoadUserdataInfo)
namespace Luau
{
namespace CodeGen
@ -480,8 +482,10 @@ void toString(std::string& result, IrConst constant)
}
}
const char* getBytecodeTypeName(uint8_t type)
const char* getBytecodeTypeName_DEPRECATED(uint8_t type)
{
CODEGEN_ASSERT(!FFlag::LuauLoadUserdataInfo);
switch (type & ~LBC_TYPE_OPTIONAL_BIT)
{
case LBC_TYPE_NIL:
@ -512,13 +516,78 @@ const char* getBytecodeTypeName(uint8_t type)
return nullptr;
}
void toString(std::string& result, const BytecodeTypes& bcTypes)
const char* getBytecodeTypeName(uint8_t type, const char* const* userdataTypes)
{
CODEGEN_ASSERT(FFlag::LuauLoadUserdataInfo);
// Optional bit should be handled externally
type = type & ~LBC_TYPE_OPTIONAL_BIT;
if (type >= LBC_TYPE_TAGGED_USERDATA_BASE && type < LBC_TYPE_TAGGED_USERDATA_END)
{
if (userdataTypes)
return userdataTypes[type - LBC_TYPE_TAGGED_USERDATA_BASE];
return "userdata";
}
switch (type)
{
case LBC_TYPE_NIL:
return "nil";
case LBC_TYPE_BOOLEAN:
return "boolean";
case LBC_TYPE_NUMBER:
return "number";
case LBC_TYPE_STRING:
return "string";
case LBC_TYPE_TABLE:
return "table";
case LBC_TYPE_FUNCTION:
return "function";
case LBC_TYPE_THREAD:
return "thread";
case LBC_TYPE_USERDATA:
return "userdata";
case LBC_TYPE_VECTOR:
return "vector";
case LBC_TYPE_BUFFER:
return "buffer";
case LBC_TYPE_ANY:
return "any";
}
CODEGEN_ASSERT(!"Unhandled type in getBytecodeTypeName");
return nullptr;
}
void toString_DEPRECATED(std::string& result, const BytecodeTypes& bcTypes)
{
CODEGEN_ASSERT(!FFlag::LuauLoadUserdataInfo);
if (bcTypes.c != LBC_TYPE_ANY)
append(result, "%s <- %s, %s, %s", getBytecodeTypeName(bcTypes.result), getBytecodeTypeName(bcTypes.a), getBytecodeTypeName(bcTypes.b),
getBytecodeTypeName(bcTypes.c));
append(result, "%s <- %s, %s, %s", getBytecodeTypeName_DEPRECATED(bcTypes.result), getBytecodeTypeName_DEPRECATED(bcTypes.a),
getBytecodeTypeName_DEPRECATED(bcTypes.b), getBytecodeTypeName_DEPRECATED(bcTypes.c));
else
append(result, "%s <- %s, %s", getBytecodeTypeName(bcTypes.result), getBytecodeTypeName(bcTypes.a), getBytecodeTypeName(bcTypes.b));
append(result, "%s <- %s, %s", getBytecodeTypeName_DEPRECATED(bcTypes.result), getBytecodeTypeName_DEPRECATED(bcTypes.a),
getBytecodeTypeName_DEPRECATED(bcTypes.b));
}
void toString(std::string& result, const BytecodeTypes& bcTypes, const char* const* userdataTypes)
{
CODEGEN_ASSERT(FFlag::LuauLoadUserdataInfo);
append(result, "%s%s", getBytecodeTypeName(bcTypes.result, userdataTypes), (bcTypes.result & LBC_TYPE_OPTIONAL_BIT) != 0 ? "?" : "");
append(result, " <- ");
append(result, "%s%s", getBytecodeTypeName(bcTypes.a, userdataTypes), (bcTypes.a & LBC_TYPE_OPTIONAL_BIT) != 0 ? "?" : "");
append(result, ", ");
append(result, "%s%s", getBytecodeTypeName(bcTypes.b, userdataTypes), (bcTypes.b & LBC_TYPE_OPTIONAL_BIT) != 0 ? "?" : "");
if (bcTypes.c != LBC_TYPE_ANY)
{
append(result, ", ");
append(result, "%s%s", getBytecodeTypeName(bcTypes.c, userdataTypes), (bcTypes.c & LBC_TYPE_OPTIONAL_BIT) != 0 ? "?" : "");
}
}
static void appendBlockSet(IrToStringContext& ctx, BlockIteratorWrapper blocks)

View File

@ -14,7 +14,6 @@
#include "ltm.h"
LUAU_FASTFLAGVARIABLE(LuauCodegenDirectUserdataFlow, false)
LUAU_FASTFLAGVARIABLE(LuauCodegenFixVectorFields, false)
LUAU_FASTFLAG(LuauCodegenAnalyzeHostVectorOps)
namespace Luau
@ -1200,19 +1199,19 @@ void translateInstGetTableKS(IrBuilder& build, const Instruction* pc, int pcpos)
TString* str = gco2ts(build.function.proto->k[aux].value.gc);
const char* field = getstr(str);
if ((!FFlag::LuauCodegenFixVectorFields || str->len == 1) && (*field == 'X' || *field == 'x'))
if (str->len == 1 && (*field == 'X' || *field == 'x'))
{
IrOp value = build.inst(IrCmd::LOAD_FLOAT, build.vmReg(rb), build.constInt(0));
build.inst(IrCmd::STORE_DOUBLE, build.vmReg(ra), value);
build.inst(IrCmd::STORE_TAG, build.vmReg(ra), build.constTag(LUA_TNUMBER));
}
else if ((!FFlag::LuauCodegenFixVectorFields || str->len == 1) && (*field == 'Y' || *field == 'y'))
else if (str->len == 1 && (*field == 'Y' || *field == 'y'))
{
IrOp value = build.inst(IrCmd::LOAD_FLOAT, build.vmReg(rb), build.constInt(4));
build.inst(IrCmd::STORE_DOUBLE, build.vmReg(ra), value);
build.inst(IrCmd::STORE_TAG, build.vmReg(ra), build.constTag(LUA_TNUMBER));
}
else if ((!FFlag::LuauCodegenFixVectorFields || str->len == 1) && (*field == 'Z' || *field == 'z'))
else if (str->len == 1 && (*field == 'Z' || *field == 'z'))
{
IrOp value = build.inst(IrCmd::LOAD_FLOAT, build.vmReg(rb), build.constInt(8));
build.inst(IrCmd::STORE_DOUBLE, build.vmReg(ra), value);

View File

@ -252,6 +252,16 @@ bool isGCO(uint8_t tag)
return tag >= LUA_TSTRING;
}
bool isUserdataBytecodeType(uint8_t ty)
{
return ty == LBC_TYPE_USERDATA || isCustomUserdataBytecodeType(ty);
}
bool isCustomUserdataBytecodeType(uint8_t ty)
{
return ty >= LBC_TYPE_TAGGED_USERDATA_BASE && ty < LBC_TYPE_TAGGED_USERDATA_END;
}
void kill(IrFunction& function, IrInst& inst)
{
CODEGEN_ASSERT(inst.useCount == 0);

View File

@ -18,7 +18,7 @@ LUAU_FASTINTVARIABLE(LuauCodeGenMinLinearBlockPath, 3)
LUAU_FASTINTVARIABLE(LuauCodeGenReuseSlotLimit, 64)
LUAU_FASTFLAGVARIABLE(DebugLuauAbortingChecks, false)
LUAU_FASTFLAG(LuauCodegenRemoveDeadStores5)
LUAU_FASTFLAGVARIABLE(LuauCodegenLoadPropCheckRegLinkInTv, false)
LUAU_FASTFLAGVARIABLE(LuauCodegenFixSplitStoreConstMismatch, false)
namespace Luau
{
@ -739,7 +739,7 @@ static void constPropInInst(ConstPropState& state, IrBuilder& build, IrFunction&
// If we know the tag, we can try extracting the value from a register used by LOAD_TVALUE
// To do that, we have to ensure that the register link of the source value is still valid
if (tag != 0xff && (!FFlag::LuauCodegenLoadPropCheckRegLinkInTv || state.tryGetRegLink(inst.b) != nullptr))
if (tag != 0xff && state.tryGetRegLink(inst.b) != nullptr)
{
if (IrInst* arg = function.asInstOp(inst.b); arg && arg->cmd == IrCmd::LOAD_TVALUE && arg->a.kind == IrOpKind::VmReg)
{
@ -750,18 +750,48 @@ static void constPropInInst(ConstPropState& state, IrBuilder& build, IrFunction&
}
}
// If we have constant tag and value, replace TValue store with tag/value pair store
if (tag != 0xff && value.kind != IrOpKind::None && (tag == LUA_TBOOLEAN || tag == LUA_TNUMBER || isGCO(tag)))
if (FFlag::LuauCodegenFixSplitStoreConstMismatch)
{
replace(function, block, index, {IrCmd::STORE_SPLIT_TVALUE, inst.a, build.constTag(tag), value, inst.c});
// If we have constant tag and value, replace TValue store with tag/value pair store
bool canSplitTvalueStore = false;
// Value can be propagated to future loads of the same register
if (inst.a.kind == IrOpKind::VmReg && activeLoadValue != kInvalidInstIdx)
state.valueMap[state.versionedVmRegLoad(activeLoadCmd, inst.a)] = activeLoadValue;
if (tag == LUA_TBOOLEAN &&
(value.kind == IrOpKind::Inst || (value.kind == IrOpKind::Constant && function.constOp(value).kind == IrConstKind::Int)))
canSplitTvalueStore = true;
else if (tag == LUA_TNUMBER &&
(value.kind == IrOpKind::Inst || (value.kind == IrOpKind::Constant && function.constOp(value).kind == IrConstKind::Double)))
canSplitTvalueStore = true;
else if (tag != 0xff && isGCO(tag) && value.kind == IrOpKind::Inst)
canSplitTvalueStore = true;
if (canSplitTvalueStore)
{
replace(function, block, index, {IrCmd::STORE_SPLIT_TVALUE, inst.a, build.constTag(tag), value, inst.c});
// Value can be propagated to future loads of the same register
if (inst.a.kind == IrOpKind::VmReg && activeLoadValue != kInvalidInstIdx)
state.valueMap[state.versionedVmRegLoad(activeLoadCmd, inst.a)] = activeLoadValue;
}
else if (inst.a.kind == IrOpKind::VmReg)
{
state.forwardVmRegStoreToLoad(inst, IrCmd::LOAD_TVALUE);
}
}
else if (inst.a.kind == IrOpKind::VmReg)
else
{
state.forwardVmRegStoreToLoad(inst, IrCmd::LOAD_TVALUE);
// If we have constant tag and value, replace TValue store with tag/value pair store
if (tag != 0xff && value.kind != IrOpKind::None && (tag == LUA_TBOOLEAN || tag == LUA_TNUMBER || isGCO(tag)))
{
replace(function, block, index, {IrCmd::STORE_SPLIT_TVALUE, inst.a, build.constTag(tag), value, inst.c});
// Value can be propagated to future loads of the same register
if (inst.a.kind == IrOpKind::VmReg && activeLoadValue != kInvalidInstIdx)
state.valueMap[state.versionedVmRegLoad(activeLoadCmd, inst.a)] = activeLoadValue;
}
else if (inst.a.kind == IrOpKind::VmReg)
{
state.forwardVmRegStoreToLoad(inst, IrCmd::LOAD_TVALUE);
}
}
}
break;

View File

@ -438,7 +438,9 @@ enum LuauBytecodeTag
LBC_VERSION_TARGET = 5,
// Type encoding version
LBC_TYPE_VERSION_DEPRECATED = 1,
LBC_TYPE_VERSION = 2,
LBC_TYPE_VERSION_MIN = 1,
LBC_TYPE_VERSION_MAX = 3,
LBC_TYPE_VERSION_TARGET = 3,
// Types of constant table entries
LBC_CONSTANT_NIL = 0,
LBC_CONSTANT_BOOLEAN,
@ -465,6 +467,10 @@ enum LuauBytecodeType
LBC_TYPE_BUFFER,
LBC_TYPE_ANY = 15,
LBC_TYPE_TAGGED_USERDATA_BASE = 64,
LBC_TYPE_TAGGED_USERDATA_END = 64 + 32,
LBC_TYPE_OPTIONAL_BIT = 1 << 7,
LBC_TYPE_INVALID = 256,

View File

@ -79,6 +79,9 @@ public:
void pushLocalTypeInfo(LuauBytecodeType type, uint8_t reg, uint32_t startpc, uint32_t endpc);
void pushUpvalTypeInfo(LuauBytecodeType type);
uint32_t addUserdataType(const char* name);
void useUserdataType(uint32_t index);
void setDebugFunctionName(StringRef name);
void setDebugFunctionLineDefined(int line);
void setDebugLine(int line);
@ -229,6 +232,13 @@ private:
LuauBytecodeType type;
};
struct UserdataType
{
std::string name;
uint32_t nameRef = 0;
bool used = false;
};
struct Jump
{
uint32_t source;
@ -277,6 +287,8 @@ private:
std::vector<TypedLocal> typedLocals;
std::vector<TypedUpval> typedUpvals;
std::vector<UserdataType> userdataTypes;
DenseHashMap<StringRef, unsigned int, StringRefHash> stringTable;
std::vector<StringRef> debugStrings;
@ -308,6 +320,8 @@ private:
int32_t addConstant(const ConstantKey& key, const Constant& value);
unsigned int addStringTableEntry(StringRef value);
const char* tryGetUserdataTypeName(LuauBytecodeType type) const;
};
} // namespace Luau

View File

@ -46,6 +46,9 @@ struct CompileOptions
// null-terminated array of globals that are mutable; disables the import optimization for fields accessed through these
const char* const* mutableGlobals = nullptr;
// null-terminated array of userdata types that will be included in the type information
const char* const* userdataTypes = nullptr;
};
class CompileError : public std::exception

View File

@ -42,6 +42,9 @@ struct lua_CompileOptions
// null-terminated array of globals that are mutable; disables the import optimization for fields accessed through these
const char* const* mutableGlobals;
// null-terminated array of userdata types that will be included in the type information
const char* const* userdataTypes = nullptr;
};
// compile source to bytecode; when source compilation fails, the resulting bytecode contains the encoded error. use free() to destroy

View File

@ -7,9 +7,8 @@
#include <algorithm>
#include <string.h>
LUAU_FASTFLAGVARIABLE(LuauCompileNoJumpLineRetarget, false)
LUAU_FASTFLAG(LuauCompileRepeatUntilSkippedLocals)
LUAU_FASTFLAGVARIABLE(LuauCompileTypeInfo, false)
LUAU_FASTFLAG(LuauCompileUserdataInfo)
namespace Luau
{
@ -335,6 +334,18 @@ unsigned int BytecodeBuilder::addStringTableEntry(StringRef value)
return index;
}
const char* BytecodeBuilder::tryGetUserdataTypeName(LuauBytecodeType type) const
{
LUAU_ASSERT(FFlag::LuauCompileUserdataInfo);
unsigned index = unsigned((type & ~LBC_TYPE_OPTIONAL_BIT) - LBC_TYPE_TAGGED_USERDATA_BASE);
if (index < userdataTypes.size())
return userdataTypes[index].name.c_str();
return nullptr;
}
int32_t BytecodeBuilder::addConstantNil()
{
Constant c = {Constant::Type_Nil};
@ -567,6 +578,25 @@ void BytecodeBuilder::pushUpvalTypeInfo(LuauBytecodeType type)
typedUpvals.push_back(upval);
}
uint32_t BytecodeBuilder::addUserdataType(const char* name)
{
LUAU_ASSERT(FFlag::LuauCompileUserdataInfo);
UserdataType ty;
ty.name = name;
userdataTypes.push_back(std::move(ty));
return uint32_t(userdataTypes.size() - 1);
}
void BytecodeBuilder::useUserdataType(uint32_t index)
{
LUAU_ASSERT(FFlag::LuauCompileUserdataInfo);
userdataTypes[index].used = true;
}
void BytecodeBuilder::setDebugFunctionName(StringRef name)
{
unsigned int index = addStringTableEntry(name);
@ -648,6 +678,15 @@ void BytecodeBuilder::finalize()
{
LUAU_ASSERT(bytecode.empty());
if (FFlag::LuauCompileUserdataInfo)
{
for (auto& ty : userdataTypes)
{
if (ty.used)
ty.nameRef = addStringTableEntry(StringRef({ty.name.c_str(), ty.name.length()}));
}
}
// preallocate space for bytecode blob
size_t capacity = 16;
@ -666,10 +705,24 @@ void BytecodeBuilder::finalize()
bytecode = char(version);
uint8_t typesversion = getTypeEncodingVersion();
LUAU_ASSERT(typesversion >= LBC_TYPE_VERSION_MIN && typesversion <= LBC_TYPE_VERSION_MAX);
writeByte(bytecode, typesversion);
writeStringTable(bytecode);
if (FFlag::LuauCompileTypeInfo && FFlag::LuauCompileUserdataInfo)
{
// Write the mapping between used type name indices and their name
for (uint32_t i = 0; i < uint32_t(userdataTypes.size()); i++)
{
writeByte(bytecode, i + 1);
writeVarInt(bytecode, userdataTypes[i].nameRef);
}
// 0 marks the end of the mapping
writeByte(bytecode, 0);
}
writeVarInt(bytecode, uint32_t(functions.size()));
for (const Function& func : functions)
@ -1036,11 +1089,6 @@ void BytecodeBuilder::foldJumps()
if (LUAU_INSN_OP(jumpInsn) == LOP_JUMP && LUAU_INSN_OP(targetInsn) == LOP_RETURN)
{
insns[jumpLabel] = targetInsn;
if (!FFlag::LuauCompileNoJumpLineRetarget)
{
lines[jumpLabel] = lines[targetLabel];
}
}
else if (int16_t(offset) == offset)
{
@ -1198,7 +1246,10 @@ uint8_t BytecodeBuilder::getVersion()
uint8_t BytecodeBuilder::getTypeEncodingVersion()
{
return FFlag::LuauCompileTypeInfo ? LBC_TYPE_VERSION : LBC_TYPE_VERSION_DEPRECATED;
if (FFlag::LuauCompileTypeInfo && FFlag::LuauCompileUserdataInfo)
return LBC_TYPE_VERSION_TARGET;
return FFlag::LuauCompileTypeInfo ? 2 : LBC_TYPE_VERSION_DEPRECATED;
}
#ifdef LUAU_ASSERTENABLED
@ -2275,7 +2326,7 @@ std::string BytecodeBuilder::dumpCurrentFunction(std::vector<int>& dumpinstoffs)
{
const DebugLocal& l = debugLocals[i];
if (FFlag::LuauCompileRepeatUntilSkippedLocals && l.startpc == l.endpc)
if (l.startpc == l.endpc)
{
LUAU_ASSERT(l.startpc < lines.size());
@ -2301,35 +2352,74 @@ std::string BytecodeBuilder::dumpCurrentFunction(std::vector<int>& dumpinstoffs)
{
const std::string& typeinfo = functions.back().typeinfo;
// Arguments start from third byte in function typeinfo string
for (uint8_t i = 2; i < typeinfo.size(); ++i)
if (FFlag::LuauCompileUserdataInfo)
{
uint8_t et = typeinfo[i];
// Arguments start from third byte in function typeinfo string
for (uint8_t i = 2; i < typeinfo.size(); ++i)
{
uint8_t et = typeinfo[i];
const char* base = getBaseTypeString(et);
const char* optional = (et & LBC_TYPE_OPTIONAL_BIT) ? "?" : "";
const char* userdata = tryGetUserdataTypeName(LuauBytecodeType(et));
const char* name = userdata ? userdata : getBaseTypeString(et);
const char* optional = (et & LBC_TYPE_OPTIONAL_BIT) ? "?" : "";
formatAppend(result, "R%d: %s%s [argument]\n", i - 2, base, optional);
formatAppend(result, "R%d: %s%s [argument]\n", i - 2, name, optional);
}
for (size_t i = 0; i < typedUpvals.size(); ++i)
{
const TypedUpval& l = typedUpvals[i];
const char* userdata = tryGetUserdataTypeName(l.type);
const char* name = userdata ? userdata : getBaseTypeString(l.type);
const char* optional = (l.type & LBC_TYPE_OPTIONAL_BIT) ? "?" : "";
formatAppend(result, "U%d: %s%s\n", int(i), name, optional);
}
for (size_t i = 0; i < typedLocals.size(); ++i)
{
const TypedLocal& l = typedLocals[i];
const char* userdata = tryGetUserdataTypeName(l.type);
const char* name = userdata ? userdata : getBaseTypeString(l.type);
const char* optional = (l.type & LBC_TYPE_OPTIONAL_BIT) ? "?" : "";
formatAppend(result, "R%d: %s%s from %d to %d\n", l.reg, name, optional, l.startpc, l.endpc);
}
}
for (size_t i = 0; i < typedUpvals.size(); ++i)
else
{
const TypedUpval& l = typedUpvals[i];
// Arguments start from third byte in function typeinfo string
for (uint8_t i = 2; i < typeinfo.size(); ++i)
{
uint8_t et = typeinfo[i];
const char* base = getBaseTypeString(l.type);
const char* optional = (l.type & LBC_TYPE_OPTIONAL_BIT) ? "?" : "";
const char* base = getBaseTypeString(et);
const char* optional = (et & LBC_TYPE_OPTIONAL_BIT) ? "?" : "";
formatAppend(result, "U%d: %s%s\n", int(i), base, optional);
}
formatAppend(result, "R%d: %s%s [argument]\n", i - 2, base, optional);
}
for (size_t i = 0; i < typedLocals.size(); ++i)
{
const TypedLocal& l = typedLocals[i];
for (size_t i = 0; i < typedUpvals.size(); ++i)
{
const TypedUpval& l = typedUpvals[i];
const char* base = getBaseTypeString(l.type);
const char* optional = (l.type & LBC_TYPE_OPTIONAL_BIT) ? "?" : "";
const char* base = getBaseTypeString(l.type);
const char* optional = (l.type & LBC_TYPE_OPTIONAL_BIT) ? "?" : "";
formatAppend(result, "R%d: %s%s from %d to %d\n", l.reg, base, optional, l.startpc, l.endpc);
formatAppend(result, "U%d: %s%s\n", int(i), base, optional);
}
for (size_t i = 0; i < typedLocals.size(); ++i)
{
const TypedLocal& l = typedLocals[i];
const char* base = getBaseTypeString(l.type);
const char* optional = (l.type & LBC_TYPE_OPTIONAL_BIT) ? "?" : "";
formatAppend(result, "R%d: %s%s from %d to %d\n", l.reg, base, optional, l.startpc, l.endpc);
}
}
}
}

View File

@ -26,10 +26,9 @@ LUAU_FASTINTVARIABLE(LuauCompileInlineThreshold, 25)
LUAU_FASTINTVARIABLE(LuauCompileInlineThresholdMaxBoost, 300)
LUAU_FASTINTVARIABLE(LuauCompileInlineDepth, 5)
LUAU_FASTFLAGVARIABLE(LuauCompileRepeatUntilSkippedLocals, false)
LUAU_FASTFLAG(LuauCompileTypeInfo)
LUAU_FASTFLAGVARIABLE(LuauTypeInfoLookupImprovement, false)
LUAU_FASTFLAGVARIABLE(LuauCompileTempTypeInfo, false)
LUAU_FASTFLAGVARIABLE(LuauCompileUserdataInfo, false)
namespace Luau
{
@ -107,6 +106,7 @@ struct Compiler
, locstants(nullptr)
, tableShapes(nullptr)
, builtins(nullptr)
, userdataTypes(AstName())
, functionTypes(nullptr)
, localTypes(nullptr)
, exprTypes(nullptr)
@ -677,10 +677,7 @@ struct Compiler
// if the argument is a local that isn't mutated, we will simply reuse the existing register
if (int reg = le ? getExprLocalReg(le) : -1; reg >= 0 && (!lv || !lv->written))
{
if (FFlag::LuauTypeInfoLookupImprovement)
args.push_back({var, uint8_t(reg), {Constant::Type_Unknown}, kDefaultAllocPc});
else
args.push_back({var, uint8_t(reg)});
args.push_back({var, uint8_t(reg), {Constant::Type_Unknown}, kDefaultAllocPc});
}
else
{
@ -2771,16 +2768,14 @@ struct Compiler
{
validateContinueUntil(loops.back().continueUsed, stat->condition, body, i + 1);
continueValidated = true;
if (FFlag::LuauCompileRepeatUntilSkippedLocals)
conditionLocals = localStack.size();
conditionLocals = localStack.size();
}
}
// if continue was used, some locals might not have had their initialization completed
// the lifetime of these locals has to end before the condition is executed
// because referencing skipped locals is not possible from the condition, this earlier closure doesn't affect upvalues
if (FFlag::LuauCompileRepeatUntilSkippedLocals && continueValidated)
if (continueValidated)
{
// if continueValidated is set, it means we have visited at least one body node and size > 0
setDebugLineEnd(body->body.data[body->body.size - 1]);
@ -4094,6 +4089,7 @@ struct Compiler
DenseHashMap<AstLocal*, Constant> locstants;
DenseHashMap<AstExprTable*, TableShape> tableShapes;
DenseHashMap<AstExprCall*, int> builtins;
DenseHashMap<AstName, uint8_t> userdataTypes;
DenseHashMap<AstExprFunction*, std::string> functionTypes;
DenseHashMap<AstLocal*, LuauBytecodeType> localTypes;
DenseHashMap<AstExpr*, LuauBytecodeType> exprTypes;
@ -4190,18 +4186,34 @@ void compileOrThrow(BytecodeBuilder& bytecode, const ParseResult& parseResult, c
Compiler::FunctionVisitor functionVisitor(&compiler, functions);
root->visit(&functionVisitor);
if (FFlag::LuauCompileUserdataInfo)
{
if (const char* const* ptr = options.userdataTypes)
{
for (; *ptr; ++ptr)
{
// Type will only resolve to an AstName if it is actually mentioned in the source
if (AstName name = names.get(*ptr); name.value)
compiler.userdataTypes[name] = bytecode.addUserdataType(name.value);
}
if (uintptr_t(ptr - options.userdataTypes) > (LBC_TYPE_TAGGED_USERDATA_END - LBC_TYPE_TAGGED_USERDATA_BASE))
CompileError::raise(root->location, "Exceeded userdata type limit in the compilation options");
}
}
// computes type information for all functions based on type annotations
if (FFlag::LuauCompileTypeInfo)
{
if (options.typeInfoLevel >= 1)
buildTypeMap(compiler.functionTypes, compiler.localTypes, compiler.exprTypes, root, options.vectorType, compiler.builtinTypes,
compiler.builtins, compiler.globals);
buildTypeMap(compiler.functionTypes, compiler.localTypes, compiler.exprTypes, root, options.vectorType, compiler.userdataTypes,
compiler.builtinTypes, compiler.builtins, compiler.globals, bytecode);
}
else
{
if (functionVisitor.hasTypes)
buildTypeMap(compiler.functionTypes, compiler.localTypes, compiler.exprTypes, root, options.vectorType, compiler.builtinTypes,
compiler.builtins, compiler.globals);
buildTypeMap(compiler.functionTypes, compiler.localTypes, compiler.exprTypes, root, options.vectorType, compiler.userdataTypes,
compiler.builtinTypes, compiler.builtins, compiler.globals, bytecode);
}
for (AstExprFunction* expr : functions)

View File

@ -5,6 +5,7 @@
LUAU_FASTFLAG(LuauCompileTypeInfo)
LUAU_FASTFLAG(LuauCompileTempTypeInfo)
LUAU_FASTFLAG(LuauCompileUserdataInfo)
namespace Luau
{
@ -39,7 +40,8 @@ static LuauBytecodeType getPrimitiveType(AstName name)
}
static LuauBytecodeType getType(const AstType* ty, const AstArray<AstGenericType>& generics,
const DenseHashMap<AstName, AstStatTypeAlias*>& typeAliases, bool resolveAliases, const char* vectorType)
const DenseHashMap<AstName, AstStatTypeAlias*>& typeAliases, bool resolveAliases, const char* vectorType,
const DenseHashMap<AstName, uint8_t>& userdataTypes, BytecodeBuilder& bytecode)
{
if (const AstTypeReference* ref = ty->as<AstTypeReference>())
{
@ -50,7 +52,7 @@ static LuauBytecodeType getType(const AstType* ty, const AstArray<AstGenericType
{
// note: we only resolve aliases to the depth of 1 to avoid dealing with recursive aliases
if (resolveAliases)
return getType((*alias)->type, (*alias)->generics, typeAliases, /* resolveAliases= */ false, vectorType);
return getType((*alias)->type, (*alias)->generics, typeAliases, /* resolveAliases= */ false, vectorType, userdataTypes, bytecode);
else
return LBC_TYPE_ANY;
}
@ -64,6 +66,15 @@ static LuauBytecodeType getType(const AstType* ty, const AstArray<AstGenericType
if (LuauBytecodeType prim = getPrimitiveType(ref->name); prim != LBC_TYPE_INVALID)
return prim;
if (FFlag::LuauCompileUserdataInfo)
{
if (const uint8_t* userdataIndex = userdataTypes.find(ref->name))
{
bytecode.useUserdataType(*userdataIndex);
return LuauBytecodeType(LBC_TYPE_TAGGED_USERDATA_BASE + *userdataIndex);
}
}
// not primitive or alias or generic => host-provided, we assume userdata for now
return LBC_TYPE_USERDATA;
}
@ -82,7 +93,7 @@ static LuauBytecodeType getType(const AstType* ty, const AstArray<AstGenericType
for (AstType* ty : un->types)
{
LuauBytecodeType et = getType(ty, generics, typeAliases, resolveAliases, vectorType);
LuauBytecodeType et = getType(ty, generics, typeAliases, resolveAliases, vectorType, userdataTypes, bytecode);
if (et == LBC_TYPE_NIL)
{
@ -113,7 +124,8 @@ static LuauBytecodeType getType(const AstType* ty, const AstArray<AstGenericType
return LBC_TYPE_ANY;
}
static std::string getFunctionType(const AstExprFunction* func, const DenseHashMap<AstName, AstStatTypeAlias*>& typeAliases, const char* vectorType)
static std::string getFunctionType(const AstExprFunction* func, const DenseHashMap<AstName, AstStatTypeAlias*>& typeAliases, const char* vectorType,
const DenseHashMap<AstName, uint8_t>& userdataTypes, BytecodeBuilder& bytecode)
{
bool self = func->self != 0;
@ -130,7 +142,8 @@ static std::string getFunctionType(const AstExprFunction* func, const DenseHashM
for (AstLocal* arg : func->args)
{
LuauBytecodeType ty =
arg->annotation ? getType(arg->annotation, func->generics, typeAliases, /* resolveAliases= */ true, vectorType) : LBC_TYPE_ANY;
arg->annotation ? getType(arg->annotation, func->generics, typeAliases, /* resolveAliases= */ true, vectorType, userdataTypes, bytecode)
: LBC_TYPE_ANY;
if (ty != LBC_TYPE_ANY)
haveNonAnyParam = true;
@ -161,9 +174,11 @@ struct TypeMapVisitor : AstVisitor
DenseHashMap<AstLocal*, LuauBytecodeType>& localTypes;
DenseHashMap<AstExpr*, LuauBytecodeType>& exprTypes;
const char* vectorType;
const DenseHashMap<AstName, uint8_t>& userdataTypes;
const BuiltinTypes& builtinTypes;
const DenseHashMap<AstExprCall*, int>& builtinCalls;
const DenseHashMap<AstName, Compile::Global>& globals;
BytecodeBuilder& bytecode;
DenseHashMap<AstName, AstStatTypeAlias*> typeAliases;
std::vector<std::pair<AstName, AstStatTypeAlias*>> typeAliasStack;
@ -171,15 +186,18 @@ struct TypeMapVisitor : AstVisitor
DenseHashMap<AstExpr*, const AstType*> resolvedExprs;
TypeMapVisitor(DenseHashMap<AstExprFunction*, std::string>& functionTypes, DenseHashMap<AstLocal*, LuauBytecodeType>& localTypes,
DenseHashMap<AstExpr*, LuauBytecodeType>& exprTypes, const char* vectorType, const BuiltinTypes& builtinTypes,
const DenseHashMap<AstExprCall*, int>& builtinCalls, const DenseHashMap<AstName, Compile::Global>& globals)
DenseHashMap<AstExpr*, LuauBytecodeType>& exprTypes, const char* vectorType, const DenseHashMap<AstName, uint8_t>& userdataTypes,
const BuiltinTypes& builtinTypes, const DenseHashMap<AstExprCall*, int>& builtinCalls, const DenseHashMap<AstName, Compile::Global>& globals,
BytecodeBuilder& bytecode)
: functionTypes(functionTypes)
, localTypes(localTypes)
, exprTypes(exprTypes)
, vectorType(vectorType)
, userdataTypes(userdataTypes)
, builtinTypes(builtinTypes)
, builtinCalls(builtinCalls)
, globals(globals)
, bytecode(bytecode)
, typeAliases(AstName())
, resolvedLocals(nullptr)
, resolvedExprs(nullptr)
@ -250,7 +268,7 @@ struct TypeMapVisitor : AstVisitor
resolvedExprs[expr] = ty;
LuauBytecodeType bty = getType(ty, {}, typeAliases, /* resolveAliases= */ true, vectorType);
LuauBytecodeType bty = getType(ty, {}, typeAliases, /* resolveAliases= */ true, vectorType, userdataTypes, bytecode);
exprTypes[expr] = bty;
return bty;
}
@ -263,7 +281,7 @@ struct TypeMapVisitor : AstVisitor
resolvedLocals[local] = ty;
LuauBytecodeType bty = getType(ty, {}, typeAliases, /* resolveAliases= */ true, vectorType);
LuauBytecodeType bty = getType(ty, {}, typeAliases, /* resolveAliases= */ true, vectorType, userdataTypes, bytecode);
if (bty != LBC_TYPE_ANY)
localTypes[local] = bty;
@ -354,7 +372,7 @@ struct TypeMapVisitor : AstVisitor
bool visit(AstExprFunction* node) override
{
std::string type = getFunctionType(node, typeAliases, vectorType);
std::string type = getFunctionType(node, typeAliases, vectorType, userdataTypes, bytecode);
if (!type.empty())
functionTypes[node] = std::move(type);
@ -393,7 +411,7 @@ struct TypeMapVisitor : AstVisitor
if (AstType* annotation = local->annotation)
{
LuauBytecodeType ty = getType(annotation, {}, typeAliases, /* resolveAliases= */ true, vectorType);
LuauBytecodeType ty = getType(annotation, {}, typeAliases, /* resolveAliases= */ true, vectorType, userdataTypes, bytecode);
if (ty != LBC_TYPE_ANY)
localTypes[local] = ty;
@ -754,10 +772,11 @@ struct TypeMapVisitor : AstVisitor
};
void buildTypeMap(DenseHashMap<AstExprFunction*, std::string>& functionTypes, DenseHashMap<AstLocal*, LuauBytecodeType>& localTypes,
DenseHashMap<AstExpr*, LuauBytecodeType>& exprTypes, AstNode* root, const char* vectorType, const BuiltinTypes& builtinTypes,
const DenseHashMap<AstExprCall*, int>& builtinCalls, const DenseHashMap<AstName, Compile::Global>& globals)
DenseHashMap<AstExpr*, LuauBytecodeType>& exprTypes, AstNode* root, const char* vectorType, const DenseHashMap<AstName, uint8_t>& userdataTypes,
const BuiltinTypes& builtinTypes, const DenseHashMap<AstExprCall*, int>& builtinCalls, const DenseHashMap<AstName, Compile::Global>& globals,
BytecodeBuilder& bytecode)
{
TypeMapVisitor visitor(functionTypes, localTypes, exprTypes, vectorType, builtinTypes, builtinCalls, globals);
TypeMapVisitor visitor(functionTypes, localTypes, exprTypes, vectorType, userdataTypes, builtinTypes, builtinCalls, globals, bytecode);
root->visit(&visitor);
}

View File

@ -10,6 +10,7 @@
namespace Luau
{
class BytecodeBuilder;
struct BuiltinTypes
{
@ -26,7 +27,8 @@ struct BuiltinTypes
};
void buildTypeMap(DenseHashMap<AstExprFunction*, std::string>& functionTypes, DenseHashMap<AstLocal*, LuauBytecodeType>& localTypes,
DenseHashMap<AstExpr*, LuauBytecodeType>& exprTypes, AstNode* root, const char* vectorType, const BuiltinTypes& builtinTypes,
const DenseHashMap<AstExprCall*, int>& builtinCalls, const DenseHashMap<AstName, Compile::Global>& globals);
DenseHashMap<AstExpr*, LuauBytecodeType>& exprTypes, AstNode* root, const char* vectorType, const DenseHashMap<AstName, uint8_t>& userdataTypes,
const BuiltinTypes& builtinTypes, const DenseHashMap<AstExprCall*, int>& builtinCalls, const DenseHashMap<AstName, Compile::Global>& globals,
BytecodeBuilder& bytecode);
} // namespace Luau

View File

@ -156,6 +156,7 @@ struct lua_ExecutionCallbacks
int (*enter)(lua_State* L, Proto* proto); // called when function is about to start/resume (when execdata is present), return 0 to exit VM
void (*disable)(lua_State* L, Proto* proto); // called when function has to be switched from native to bytecode in the debugger
size_t (*getmemorysize)(lua_State* L, Proto* proto); // called to request the size of memory associated with native part of the Proto
uint8_t (*gettypemapping)(lua_State* L, const char* str, size_t len); // called to get the userdata type index
};
/*

View File

@ -11,7 +11,6 @@
#include "ldebug.h"
#include "lvm.h"
LUAU_DYNAMIC_FASTFLAGVARIABLE(LuauFastCrossTableMove, false)
LUAU_DYNAMIC_FASTFLAGVARIABLE(LuauFastTableMaxn, false)
LUAU_DYNAMIC_FASTFLAGVARIABLE(LuauFasterConcat, false)
@ -145,68 +144,6 @@ static void moveelements(lua_State* L, int srct, int dstt, int f, int e, int t)
luaC_barrierfast(L, dst);
}
else if (DFFlag::LuauFastCrossTableMove && dst != src)
{
// compute the array slice we have to copy over
int slicestart = f < 1 ? 0 : (f > src->sizearray ? src->sizearray : f - 1);
int sliceend = e < 1 ? 0 : (e > src->sizearray ? src->sizearray : e);
LUAU_ASSERT(slicestart <= sliceend);
int slicecount = sliceend - slicestart;
if (slicecount > 0)
{
// array slice starting from INT_MIN is impossible, so we don't have to worry about int overflow
int dstslicestart = f < 1 ? -f + 1 : 0;
// copy over the slice
for (int i = 0; i < slicecount; ++i)
{
lua_rawgeti(L, srct, slicestart + i + 1);
lua_rawseti(L, dstt, dstslicestart + t + i);
}
}
// copy the remaining elements that could be in the hash part
int hashpartsize = sizenode(src);
// select the strategy with the least amount of steps
if (n <= hashpartsize)
{
for (int i = 0; i < n; ++i)
{
// skip array slice elements that were already copied over
if (cast_to(unsigned int, f + i - 1) < cast_to(unsigned int, src->sizearray))
continue;
lua_rawgeti(L, srct, f + i);
lua_rawseti(L, dstt, t + i);
}
}
else
{
// source and destination tables are different, so we can iterate over source hash part directly
int i = hashpartsize;
while (i--)
{
LuaNode* node = gnode(src, i);
if (ttisnumber(gkey(node)))
{
double n = nvalue(gkey(node));
int k;
luai_num2int(k, n);
if (luai_numeq(cast_num(k), n) && k >= f && k <= e)
{
lua_rawgeti(L, srct, k);
lua_rawseti(L, dstt, t - f + k);
}
}
}
}
}
else
{
if (t > e || t <= f || dst != src)

View File

@ -14,6 +14,7 @@
#include <string.h>
LUAU_FASTFLAG(LuauLoadTypeInfo)
LUAU_FASTFLAGVARIABLE(LuauLoadUserdataInfo, false)
// TODO: RAII deallocation doesn't work for longjmp builds if a memory error happens
template<typename T>
@ -187,6 +188,65 @@ static void resolveImportSafe(lua_State* L, Table* env, TValue* k, uint32_t id)
}
}
static void remapUserdataTypes(char* data, size_t size, uint8_t* userdataRemapping, uint32_t count)
{
LUAU_ASSERT(FFlag::LuauLoadUserdataInfo);
size_t offset = 0;
uint32_t typeSize = readVarInt(data, size, offset);
uint32_t upvalCount = readVarInt(data, size, offset);
uint32_t localCount = readVarInt(data, size, offset);
if (typeSize != 0)
{
uint8_t* types = (uint8_t*)data + offset;
// Skip two bytes of function type introduction
for (uint32_t i = 2; i < typeSize; i++)
{
uint32_t index = uint32_t(types[i] - LBC_TYPE_TAGGED_USERDATA_BASE);
if (index < count)
types[i] = userdataRemapping[index];
}
offset += typeSize;
}
if (upvalCount != 0)
{
uint8_t* types = (uint8_t*)data + offset;
for (uint32_t i = 0; i < upvalCount; i++)
{
uint32_t index = uint32_t(types[i] - LBC_TYPE_TAGGED_USERDATA_BASE);
if (index < count)
types[i] = userdataRemapping[index];
}
offset += upvalCount;
}
if (localCount != 0)
{
for (uint32_t i = 0; i < localCount; i++)
{
uint32_t index = uint32_t(data[offset] - LBC_TYPE_TAGGED_USERDATA_BASE);
if (index < count)
data[offset] = userdataRemapping[index];
offset += 2;
readVarInt(data, size, offset);
readVarInt(data, size, offset);
}
}
LUAU_ASSERT(offset == size);
}
int luau_load(lua_State* L, const char* chunkname, const char* data, size_t size, int env)
{
size_t offset = 0;
@ -227,6 +287,18 @@ int luau_load(lua_State* L, const char* chunkname, const char* data, size_t size
if (version >= 4)
{
typesversion = read<uint8_t>(data, size, offset);
if (FFlag::LuauLoadUserdataInfo)
{
if (typesversion < LBC_TYPE_VERSION_MIN || typesversion > LBC_TYPE_VERSION_MAX)
{
char chunkbuf[LUA_IDSIZE];
const char* chunkid = luaO_chunkid(chunkbuf, sizeof(chunkbuf), chunkname, strlen(chunkname));
lua_pushfstring(L, "%s: bytecode type version mismatch (expected [%d..%d], got %d)", chunkid, LBC_TYPE_VERSION_MIN,
LBC_TYPE_VERSION_MAX, typesversion);
return 1;
}
}
}
// string table
@ -241,6 +313,31 @@ int luau_load(lua_State* L, const char* chunkname, const char* data, size_t size
offset += length;
}
// userdata type remapping table
// for unknown userdata types, the entry will remap to common 'userdata' type
const uint32_t userdataTypeLimit = LBC_TYPE_TAGGED_USERDATA_END - LBC_TYPE_TAGGED_USERDATA_BASE;
uint8_t userdataRemapping[userdataTypeLimit];
if (FFlag::LuauLoadUserdataInfo && typesversion == 3)
{
memset(userdataRemapping, LBC_TYPE_USERDATA, userdataTypeLimit);
uint8_t index = read<uint8_t>(data, size, offset);
while (index != 0)
{
TString* name = readString(strings, data, size, offset);
if (uint32_t(index - 1) < userdataTypeLimit)
{
if (auto cb = L->global->ecb.gettypemapping)
userdataRemapping[index - 1] = cb(L, getstr(name), name->len);
}
index = read<uint8_t>(data, size, offset);
}
}
// proto table
unsigned int protoCount = readVarInt(data, size, offset);
TempBuffer<Proto*> protos(L, protoCount);
@ -299,7 +396,7 @@ int luau_load(lua_State* L, const char* chunkname, const char* data, size_t size
offset += typesize;
}
else if (typesversion == 2)
else if (typesversion == 2 || (FFlag::LuauLoadUserdataInfo && typesversion == 3))
{
uint32_t typesize = readVarInt(data, size, offset);
@ -311,6 +408,11 @@ int luau_load(lua_State* L, const char* chunkname, const char* data, size_t size
p->sizetypeinfo = typesize;
memcpy(p->typeinfo, types, typesize);
offset += typesize;
if (FFlag::LuauLoadUserdataInfo && typesversion == 3)
{
remapUserdataTypes((char*)(uint8_t*)p->typeinfo, p->sizetypeinfo, userdataRemapping, userdataTypeLimit);
}
}
}
}

View File

@ -22,8 +22,9 @@ LUAU_FASTINT(LuauCompileLoopUnrollThreshold)
LUAU_FASTINT(LuauCompileLoopUnrollThresholdMaxBoost)
LUAU_FASTINT(LuauRecursionLimit)
LUAU_FASTFLAG(LuauCompileNoJumpLineRetarget)
LUAU_FASTFLAG(LuauCompileRepeatUntilSkippedLocals)
LUAU_FASTFLAG(LuauCompileTypeInfo)
LUAU_FASTFLAG(LuauCompileTempTypeInfo)
LUAU_FASTFLAG(LuauCompileUserdataInfo)
using namespace Luau;
@ -2106,8 +2107,6 @@ RETURN R0 0
TEST_CASE("LoopContinueEarlyCleanup")
{
ScopedFastFlag luauCompileRepeatUntilSkippedLocals{FFlag::LuauCompileRepeatUntilSkippedLocals, true};
// locals after a potential 'continue' are not accessible inside the condition and can be closed at the end of a block
CHECK_EQ("\n" + compileFunction(R"(
local y
@ -2788,8 +2787,6 @@ end
TEST_CASE("DebugLineInfoWhile")
{
ScopedFastFlag luauCompileNoJumpLineRetarget{FFlag::LuauCompileNoJumpLineRetarget, true};
Luau::BytecodeBuilder bcb;
bcb.setDumpFlags(Luau::BytecodeBuilder::Dump_Code | Luau::BytecodeBuilder::Dump_Lines);
Luau::compileOrThrow(bcb, R"(
@ -3136,8 +3133,6 @@ local 8: reg 3, start pc 35 line 21, end pc 35 line 21
TEST_CASE("DebugLocals2")
{
ScopedFastFlag luauCompileRepeatUntilSkippedLocals{FFlag::LuauCompileRepeatUntilSkippedLocals, true};
const char* source = R"(
function foo(x)
repeat
@ -3167,9 +3162,6 @@ local 2: reg 0, start pc 0 line 4, end pc 2 line 6
TEST_CASE("DebugLocals3")
{
ScopedFastFlag luauCompileRepeatUntilSkippedLocals{FFlag::LuauCompileRepeatUntilSkippedLocals, true};
ScopedFastFlag luauCompileNoJumpLineRetarget{FFlag::LuauCompileNoJumpLineRetarget, true};
const char* source = R"(
function foo(x)
repeat
@ -3203,6 +3195,7 @@ local 4: reg 0, start pc 0 line 4, end pc 5 line 8
8: RETURN R0 0
)");
}
TEST_CASE("DebugRemarks")
{
Luau::BytecodeBuilder bcb;
@ -3230,6 +3223,80 @@ RETURN R0 0
)");
}
TEST_CASE("DebugTypes")
{
ScopedFastFlag luauCompileTypeInfo{FFlag::LuauCompileTypeInfo, true};
ScopedFastFlag luauCompileTempTypeInfo{FFlag::LuauCompileTempTypeInfo, true};
ScopedFastFlag luauCompileUserdataInfo{FFlag::LuauCompileUserdataInfo, true};
const char* source = R"(
local up: number = 2
function foo(e: vector, f: mat3, g: sequence)
local h = e * e
for i=1,3 do
print(i)
end
print(e * f)
print(g)
print(h)
up += a
return a
end
)";
Luau::BytecodeBuilder bcb;
bcb.setDumpFlags(Luau::BytecodeBuilder::Dump_Code | Luau::BytecodeBuilder::Dump_Types);
bcb.setDumpSource(source);
Luau::CompileOptions options;
options.vectorCtor = "vector";
options.vectorType = "vector";
options.typeInfoLevel = 1;
static const char* kUserdataCompileTypes[] = {"vec2", "color", "mat3", nullptr};
options.userdataTypes = kUserdataCompileTypes;
Luau::compileOrThrow(bcb, source, options);
CHECK_EQ("\n" + bcb.dumpFunction(0), R"(
R0: vector [argument]
R1: mat3 [argument]
R2: userdata [argument]
U0: number
R6: any from 1 to 9
R3: vector from 0 to 30
MUL R3 R0 R0
LOADN R6 1
LOADN R4 3
LOADN R5 1
FORNPREP R4 L1
L0: GETIMPORT R7 1 [print]
MOVE R8 R6
CALL R7 1 0
FORNLOOP R4 L0
L1: GETIMPORT R4 1 [print]
MUL R5 R0 R1
CALL R4 1 0
GETIMPORT R4 1 [print]
MOVE R5 R2
CALL R4 1 0
GETIMPORT R4 1 [print]
MOVE R5 R3
CALL R4 1 0
GETUPVAL R4 0
GETIMPORT R5 3 [a]
ADD R4 R4 R5
SETUPVAL R4 0
GETIMPORT R4 3 [a]
RETURN R4 1
)");
}
TEST_CASE("SourceRemarks")
{
const char* source = R"(
@ -4158,8 +4225,6 @@ RETURN R0 0
TEST_CASE("Coverage")
{
ScopedFastFlag luauCompileNoJumpLineRetarget{FFlag::LuauCompileNoJumpLineRetarget, true};
// basic statement coverage
CHECK_EQ("\n" + compileFunction0Coverage(R"(
print(1)

View File

@ -33,8 +33,7 @@ void luaC_validate(lua_State* L);
LUAU_FASTFLAG(DebugLuauAbortingChecks)
LUAU_FASTINT(CodegenHeuristicsInstructionLimit)
LUAU_FASTFLAG(LuauCompileRepeatUntilSkippedLocals)
LUAU_DYNAMIC_FASTFLAG(LuauFastCrossTableMove)
LUAU_FASTFLAG(LuauCodegenFixSplitStoreConstMismatch)
static lua_CompileOptions defaultOptions()
{
@ -443,8 +442,6 @@ TEST_CASE("Sort")
TEST_CASE("Move")
{
ScopedFastFlag luauFastCrossTableMove{DFFlag::LuauFastCrossTableMove, true};
runConformance("move.lua");
}
@ -717,8 +714,6 @@ TEST_CASE("Debugger")
static bool singlestep = false;
static int stephits = 0;
ScopedFastFlag luauCompileRepeatUntilSkippedLocals{FFlag::LuauCompileRepeatUntilSkippedLocals, true};
SUBCASE("")
{
singlestep = false;
@ -2140,6 +2135,8 @@ TEST_CASE("Native")
if (!codegen || !luau_codegen_supported())
return;
ScopedFastFlag luauCodegenFixSplitStoreConstMismatch{FFlag::LuauCodegenFixSplitStoreConstMismatch, true};
SUBCASE("Checked")
{
FFlag::DebugLuauAbortingChecks.value = true;

View File

@ -3,6 +3,8 @@
#include "Luau/IrBuilder.h"
static const char* kUserdataRunTypes[] = {"extra", "color", "vec2", "mat3", nullptr};
inline uint8_t vectorAccessBytecodeType(const char* member, size_t memberLength)
{
using namespace Luau::CodeGen;

View File

@ -14,7 +14,7 @@
LUAU_FASTFLAG(LuauCodegenRemoveDeadStores5)
LUAU_FASTFLAG(DebugLuauAbortingChecks)
LUAU_FASTFLAG(LuauCodegenLoadPropCheckRegLinkInTv)
LUAU_FASTFLAG(LuauCodegenFixSplitStoreConstMismatch)
using namespace Luau::CodeGen;
@ -2658,6 +2658,60 @@ bb_0:
)");
}
TEST_CASE_FIXTURE(IrBuilderFixture, "DoNotProduceInvalidSplitStore1")
{
ScopedFastFlag luauCodegenFixSplitStoreConstMismatch{FFlag::LuauCodegenFixSplitStoreConstMismatch, true};
IrOp entry = build.block(IrBlockKind::Internal);
build.beginBlock(entry);
build.inst(IrCmd::STORE_INT, build.vmReg(0), build.constInt(1));
build.inst(IrCmd::CHECK_TAG, build.vmReg(0), build.constTag(ttable), build.vmExit(1));
build.inst(IrCmd::STORE_TVALUE, build.vmReg(1), build.inst(IrCmd::LOAD_TVALUE, build.vmReg(0)));
build.inst(IrCmd::RETURN, build.vmReg(1), build.constInt(1));
updateUseCounts(build.function);
computeCfgInfo(build.function);
constPropInBlockChains(build, true);
CHECK("\n" + toString(build.function, IncludeUseInfo::No) == R"(
bb_0:
STORE_INT R0, 1i
CHECK_TAG R0, ttable, exit(1)
%2 = LOAD_TVALUE R0
STORE_TVALUE R1, %2
RETURN R1, 1i
)");
}
TEST_CASE_FIXTURE(IrBuilderFixture, "DoNotProduceInvalidSplitStore2")
{
ScopedFastFlag luauCodegenFixSplitStoreConstMismatch{FFlag::LuauCodegenFixSplitStoreConstMismatch, true};
IrOp entry = build.block(IrBlockKind::Internal);
build.beginBlock(entry);
build.inst(IrCmd::STORE_INT, build.vmReg(0), build.constInt(1));
build.inst(IrCmd::CHECK_TAG, build.vmReg(0), build.constTag(tnumber), build.vmExit(1));
build.inst(IrCmd::STORE_TVALUE, build.vmReg(1), build.inst(IrCmd::LOAD_TVALUE, build.vmReg(0)));
build.inst(IrCmd::RETURN, build.vmReg(1), build.constInt(1));
updateUseCounts(build.function);
computeCfgInfo(build.function);
constPropInBlockChains(build, true);
CHECK("\n" + toString(build.function, IncludeUseInfo::No) == R"(
bb_0:
STORE_INT R0, 1i
CHECK_TAG R0, tnumber, exit(1)
%2 = LOAD_TVALUE R0
STORE_TVALUE R1, %2
RETURN R1, 1i
)");
}
TEST_SUITE_END();
TEST_SUITE_BEGIN("Analysis");
@ -3475,8 +3529,6 @@ bb_1:
TEST_CASE_FIXTURE(IrBuilderFixture, "TaggedValuePropagationIntoTvalueChecksRegisterVersion")
{
ScopedFastFlag luauCodegenLoadPropCheckRegLinkInTv{FFlag::LuauCodegenLoadPropCheckRegLinkInTv, true};
IrOp entry = build.block(IrBlockKind::Internal);
build.beginBlock(entry);

View File

@ -13,18 +13,17 @@
#include "ConformanceIrHooks.h"
#include <memory>
#include <string_view>
LUAU_FASTFLAG(LuauCodegenRemoveDeadStores5)
LUAU_FASTFLAG(LuauCodegenDirectUserdataFlow)
LUAU_FASTFLAG(LuauCompileTypeInfo)
LUAU_FASTFLAG(LuauLoadTypeInfo)
LUAU_FASTFLAG(LuauCodegenTypeInfo)
LUAU_FASTFLAG(LuauTypeInfoLookupImprovement)
LUAU_FASTFLAG(LuauCodegenIrTypeNames)
LUAU_FASTFLAG(LuauCompileTempTypeInfo)
LUAU_FASTFLAG(LuauCodegenFixVectorFields)
LUAU_FASTFLAG(LuauCodegenVectorMispredictFix)
LUAU_FASTFLAG(LuauCodegenAnalyzeHostVectorOps)
LUAU_FASTFLAG(LuauCompileUserdataInfo)
LUAU_FASTFLAG(LuauLoadUserdataInfo)
static std::string getCodegenAssembly(const char* source, bool includeIrTypes = false, int debugLevel = 1)
{
@ -64,6 +63,9 @@ static std::string getCodegenAssembly(const char* source, bool includeIrTypes =
copts.vectorCtor = "vector";
copts.vectorType = "vector";
static const char* kUserdataCompileTypes[] = {"vec2", "color", "mat3", nullptr};
copts.userdataTypes = kUserdataCompileTypes;
Luau::BytecodeBuilder bcb;
Luau::compileOrThrow(bcb, result, names, copts);
@ -71,6 +73,33 @@ static std::string getCodegenAssembly(const char* source, bool includeIrTypes =
std::unique_ptr<lua_State, void (*)(lua_State*)> globalState(luaL_newstate(), lua_close);
lua_State* L = globalState.get();
// Runtime mapping is specifically created to NOT match the compilation mapping
options.compilationOptions.userdataTypes = kUserdataRunTypes;
if (Luau::CodeGen::isSupported())
{
// Type remapper requires the codegen runtime
Luau::CodeGen::create(L);
Luau::CodeGen::setUserdataRemapper(L, kUserdataRunTypes, [](void* context, const char* str, size_t len) -> uint8_t {
const char** types = (const char**)context;
uint8_t index = 0;
std::string_view sv{str, len};
for (; *types; ++types)
{
if (sv == *types)
return index;
index++;
}
return 0xff;
});
}
if (luau_load(L, "name", bytecode.data(), bytecode.size(), 0) == 0)
return Luau::CodeGen::getAssembly(L, -1, options, nullptr);
@ -480,8 +509,6 @@ bb_bytecode_1:
TEST_CASE("VectorRandomProp")
{
ScopedFastFlag luauCodegenRemoveDeadStores{FFlag::LuauCodegenRemoveDeadStores5, true};
ScopedFastFlag luauCodegenFixVectorFields{FFlag::LuauCodegenFixVectorFields, true};
ScopedFastFlag luauCodegenVectorMispredictFix{FFlag::LuauCodegenVectorMispredictFix, true};
CHECK_EQ("\n" + getCodegenAssembly(R"(
local function foo(a: vector)
@ -524,7 +551,6 @@ bb_6:
TEST_CASE("VectorCustomAccess")
{
ScopedFastFlag luauCodegenRemoveDeadStores{FFlag::LuauCodegenRemoveDeadStores5, true};
ScopedFastFlag luauCodegenVectorMispredictFix{FFlag::LuauCodegenVectorMispredictFix, true};
ScopedFastFlag luauCodegenAnalyzeHostVectorOps{FFlag::LuauCodegenAnalyzeHostVectorOps, true};
CHECK_EQ("\n" + getCodegenAssembly(R"(
@ -600,7 +626,6 @@ bb_bytecode_1:
TEST_CASE("VectorCustomAccessChain")
{
ScopedFastFlag luauCodegenRemoveDeadStores{FFlag::LuauCodegenRemoveDeadStores5, true};
ScopedFastFlag luauCodegenVectorMispredictFix{FFlag::LuauCodegenVectorMispredictFix, true};
ScopedFastFlag LuauCodegenDirectUserdataFlow{FFlag::LuauCodegenDirectUserdataFlow, true};
ScopedFastFlag luauCodegenAnalyzeHostVectorOps{FFlag::LuauCodegenAnalyzeHostVectorOps, true};
@ -655,7 +680,6 @@ bb_bytecode_1:
TEST_CASE("VectorCustomNamecallChain")
{
ScopedFastFlag luauCodegenRemoveDeadStores{FFlag::LuauCodegenRemoveDeadStores5, true};
ScopedFastFlag luauCodegenVectorMispredictFix{FFlag::LuauCodegenVectorMispredictFix, true};
ScopedFastFlag LuauCodegenDirectUserdataFlow{FFlag::LuauCodegenDirectUserdataFlow, true};
ScopedFastFlag luauCodegenAnalyzeHostVectorOps{FFlag::LuauCodegenAnalyzeHostVectorOps, true};
@ -717,8 +741,8 @@ bb_bytecode_1:
TEST_CASE("VectorCustomNamecallChain2")
{
ScopedFastFlag sffs[]{{FFlag::LuauLoadTypeInfo, true}, {FFlag::LuauCompileTypeInfo, true}, {FFlag::LuauCodegenTypeInfo, true},
{FFlag::LuauCodegenRemoveDeadStores5, true}, {FFlag::LuauTypeInfoLookupImprovement, true}, {FFlag::LuauCompileTempTypeInfo, true},
{FFlag::LuauCodegenVectorMispredictFix, true}, {FFlag::LuauCodegenDirectUserdataFlow, true}, {FFlag::LuauCodegenAnalyzeHostVectorOps, true}};
{FFlag::LuauCodegenRemoveDeadStores5, true}, {FFlag::LuauCompileTempTypeInfo, true}, {FFlag::LuauCodegenDirectUserdataFlow, true},
{FFlag::LuauCodegenAnalyzeHostVectorOps, true}};
CHECK_EQ("\n" + getCodegenAssembly(R"(
type Vertex = {n: vector, b: vector}
@ -1048,7 +1072,7 @@ bb_bytecode_1:
TEST_CASE("LoadAndMoveTypePropagation")
{
ScopedFastFlag sffs[]{{FFlag::LuauLoadTypeInfo, true}, {FFlag::LuauCompileTypeInfo, true}, {FFlag::LuauCodegenTypeInfo, true},
{FFlag::LuauCodegenRemoveDeadStores5, true}, {FFlag::LuauTypeInfoLookupImprovement, true}};
{FFlag::LuauCodegenRemoveDeadStores5, true}};
CHECK_EQ("\n" + getCodegenAssembly(R"(
local function getsum(n)
@ -1116,7 +1140,7 @@ bb_bytecode_4:
TEST_CASE("ArgumentTypeRefinement")
{
ScopedFastFlag sffs[]{{FFlag::LuauLoadTypeInfo, true}, {FFlag::LuauCompileTypeInfo, true}, {FFlag::LuauCodegenTypeInfo, true},
{FFlag::LuauCodegenRemoveDeadStores5, true}, {FFlag::LuauTypeInfoLookupImprovement, true}};
{FFlag::LuauCodegenRemoveDeadStores5, true}};
CHECK_EQ("\n" + getCodegenAssembly(R"(
local function getsum(x, y)
@ -1155,7 +1179,7 @@ bb_bytecode_0:
TEST_CASE("InlineFunctionType")
{
ScopedFastFlag sffs[]{{FFlag::LuauLoadTypeInfo, true}, {FFlag::LuauCompileTypeInfo, true}, {FFlag::LuauCodegenTypeInfo, true},
{FFlag::LuauCodegenRemoveDeadStores5, true}, {FFlag::LuauTypeInfoLookupImprovement, true}};
{FFlag::LuauCodegenRemoveDeadStores5, true}};
CHECK_EQ("\n" + getCodegenAssembly(R"(
local function inl(v: vector, s: number)
@ -1204,8 +1228,7 @@ bb_bytecode_0:
TEST_CASE("ResolveTablePathTypes")
{
ScopedFastFlag sffs[]{{FFlag::LuauLoadTypeInfo, true}, {FFlag::LuauCompileTypeInfo, true}, {FFlag::LuauCodegenTypeInfo, true},
{FFlag::LuauCodegenRemoveDeadStores5, true}, {FFlag::LuauTypeInfoLookupImprovement, true}, {FFlag::LuauCodegenIrTypeNames, true},
{FFlag::LuauCompileTempTypeInfo, true}};
{FFlag::LuauCodegenRemoveDeadStores5, true}, {FFlag::LuauCompileTempTypeInfo, true}};
CHECK_EQ("\n" + getCodegenAssembly(R"(
type Vertex = {pos: vector, normal: vector}
@ -1260,8 +1283,7 @@ bb_6:
TEST_CASE("ResolvableSimpleMath")
{
ScopedFastFlag sffs[]{{FFlag::LuauLoadTypeInfo, true}, {FFlag::LuauCompileTypeInfo, true}, {FFlag::LuauCodegenTypeInfo, true},
{FFlag::LuauCodegenRemoveDeadStores5, true}, {FFlag::LuauTypeInfoLookupImprovement, true}, {FFlag::LuauCodegenIrTypeNames, true},
{FFlag::LuauCompileTempTypeInfo, true}};
{FFlag::LuauCodegenRemoveDeadStores5, true}, {FFlag::LuauCompileTempTypeInfo, true}};
CHECK_EQ("\n" + getCodegenHeader(R"(
type Vertex = { p: vector, uv: vector, n: vector, t: vector, b: vector, h: number }
@ -1318,8 +1340,8 @@ end
TEST_CASE("ResolveVectorNamecalls")
{
ScopedFastFlag sffs[]{{FFlag::LuauLoadTypeInfo, true}, {FFlag::LuauCompileTypeInfo, true}, {FFlag::LuauCodegenTypeInfo, true},
{FFlag::LuauCodegenRemoveDeadStores5, true}, {FFlag::LuauTypeInfoLookupImprovement, true}, {FFlag::LuauCodegenIrTypeNames, true},
{FFlag::LuauCompileTempTypeInfo, true}, {FFlag::LuauCodegenDirectUserdataFlow, true}, {FFlag::LuauCodegenAnalyzeHostVectorOps, true}};
{FFlag::LuauCodegenRemoveDeadStores5, true}, {FFlag::LuauCompileTempTypeInfo, true}, {FFlag::LuauCodegenDirectUserdataFlow, true},
{FFlag::LuauCodegenAnalyzeHostVectorOps, true}};
CHECK_EQ("\n" + getCodegenAssembly(R"(
type Vertex = {pos: vector, normal: vector}
@ -1384,8 +1406,7 @@ bb_6:
TEST_CASE("ImmediateTypeAnnotationHelp")
{
ScopedFastFlag sffs[]{{FFlag::LuauLoadTypeInfo, true}, {FFlag::LuauCompileTypeInfo, true}, {FFlag::LuauCodegenTypeInfo, true},
{FFlag::LuauCodegenRemoveDeadStores5, true}, {FFlag::LuauTypeInfoLookupImprovement, true}, {FFlag::LuauCodegenIrTypeNames, true},
{FFlag::LuauCompileTempTypeInfo, true}};
{FFlag::LuauCodegenRemoveDeadStores5, true}, {FFlag::LuauCompileTempTypeInfo, true}};
CHECK_EQ("\n" + getCodegenAssembly(R"(
local function foo(arr, i)
@ -1424,8 +1445,7 @@ bb_2:
TEST_CASE("UnaryTypeResolve")
{
ScopedFastFlag sffs[]{{FFlag::LuauLoadTypeInfo, true}, {FFlag::LuauCompileTypeInfo, true}, {FFlag::LuauCodegenTypeInfo, true},
{FFlag::LuauCodegenRemoveDeadStores5, true}, {FFlag::LuauTypeInfoLookupImprovement, true}, {FFlag::LuauCodegenIrTypeNames, true},
{FFlag::LuauCompileTempTypeInfo, true}};
{FFlag::LuauCodegenRemoveDeadStores5, true}, {FFlag::LuauCompileTempTypeInfo, true}};
CHECK_EQ("\n" + getCodegenHeader(R"(
local function foo(a, b: vector, c)
@ -1448,8 +1468,7 @@ end
TEST_CASE("ForInManualAnnotation")
{
ScopedFastFlag sffs[]{{FFlag::LuauLoadTypeInfo, true}, {FFlag::LuauCompileTypeInfo, true}, {FFlag::LuauCodegenTypeInfo, true},
{FFlag::LuauCodegenRemoveDeadStores5, true}, {FFlag::LuauTypeInfoLookupImprovement, true}, {FFlag::LuauCodegenIrTypeNames, true},
{FFlag::LuauCompileTempTypeInfo, true}};
{FFlag::LuauCodegenRemoveDeadStores5, true}, {FFlag::LuauCompileTempTypeInfo, true}};
CHECK_EQ("\n" + getCodegenAssembly(R"(
type Vertex = {pos: vector, normal: vector}
@ -1545,8 +1564,7 @@ bb_12:
TEST_CASE("ForInAutoAnnotationIpairs")
{
ScopedFastFlag sffs[]{{FFlag::LuauLoadTypeInfo, true}, {FFlag::LuauCompileTypeInfo, true}, {FFlag::LuauCodegenTypeInfo, true},
{FFlag::LuauCodegenRemoveDeadStores5, true}, {FFlag::LuauTypeInfoLookupImprovement, true}, {FFlag::LuauCodegenIrTypeNames, true},
{FFlag::LuauCompileTempTypeInfo, true}};
{FFlag::LuauCodegenRemoveDeadStores5, true}, {FFlag::LuauCompileTempTypeInfo, true}};
CHECK_EQ("\n" + getCodegenHeader(R"(
type Vertex = {pos: vector, normal: vector}
@ -1574,8 +1592,7 @@ end
TEST_CASE("ForInAutoAnnotationPairs")
{
ScopedFastFlag sffs[]{{FFlag::LuauLoadTypeInfo, true}, {FFlag::LuauCompileTypeInfo, true}, {FFlag::LuauCodegenTypeInfo, true},
{FFlag::LuauCodegenRemoveDeadStores5, true}, {FFlag::LuauTypeInfoLookupImprovement, true}, {FFlag::LuauCodegenIrTypeNames, true},
{FFlag::LuauCompileTempTypeInfo, true}};
{FFlag::LuauCodegenRemoveDeadStores5, true}, {FFlag::LuauCompileTempTypeInfo, true}};
CHECK_EQ("\n" + getCodegenHeader(R"(
type Vertex = {pos: vector, normal: vector}
@ -1603,8 +1620,7 @@ end
TEST_CASE("ForInAutoAnnotationGeneric")
{
ScopedFastFlag sffs[]{{FFlag::LuauLoadTypeInfo, true}, {FFlag::LuauCompileTypeInfo, true}, {FFlag::LuauCodegenTypeInfo, true},
{FFlag::LuauCodegenRemoveDeadStores5, true}, {FFlag::LuauTypeInfoLookupImprovement, true}, {FFlag::LuauCodegenIrTypeNames, true},
{FFlag::LuauCompileTempTypeInfo, true}};
{FFlag::LuauCodegenRemoveDeadStores5, true}, {FFlag::LuauCompileTempTypeInfo, true}};
CHECK_EQ("\n" + getCodegenHeader(R"(
type Vertex = {pos: vector, normal: vector}
@ -1629,4 +1645,49 @@ end
)");
}
// Temporary test, when we don't compile new typeinfo, but support loading it
TEST_CASE("CustomUserdataTypesTemp")
{
// This test requires runtime component to be present
if (!Luau::CodeGen::isSupported())
return;
ScopedFastFlag sffs[]{{FFlag::LuauLoadTypeInfo, true}, {FFlag::LuauCompileTypeInfo, true}, {FFlag::LuauCodegenTypeInfo, true},
{FFlag::LuauCodegenRemoveDeadStores5, true}, {FFlag::LuauCompileTempTypeInfo, true}, {FFlag::LuauCompileUserdataInfo, false},
{FFlag::LuauLoadUserdataInfo, true}};
CHECK_EQ("\n" + getCodegenHeader(R"(
local function foo(v: vec2, x: mat3)
return v.X * x
end
)"),
R"(
; function foo(v, x) line 2
; R0: userdata [argument 'v']
; R1: userdata [argument 'x']
)");
}
TEST_CASE("CustomUserdataTypes")
{
// This test requires runtime component to be present
if (!Luau::CodeGen::isSupported())
return;
ScopedFastFlag sffs[]{{FFlag::LuauLoadTypeInfo, true}, {FFlag::LuauCompileTypeInfo, true}, {FFlag::LuauCodegenTypeInfo, true},
{FFlag::LuauCodegenRemoveDeadStores5, true}, {FFlag::LuauCompileTempTypeInfo, true}, {FFlag::LuauCompileUserdataInfo, true},
{FFlag::LuauLoadUserdataInfo, true}};
CHECK_EQ("\n" + getCodegenHeader(R"(
local function foo(v: vec2, x: mat3)
return v.X * x
end
)"),
R"(
; function foo(v, x) line 2
; R0: vec2 [argument 'v']
; R1: mat3 [argument 'x']
)");
}
TEST_SUITE_END();

View File

@ -556,4 +556,22 @@ local E = require(script.Parent.A)
LUAU_REQUIRE_NO_ERRORS(result);
}
TEST_CASE_FIXTURE(NonStrictTypeCheckerFixture, "nonstrict_shouldnt_warn_on_valid_buffer_use")
{
loadDefinition(R"(
declare buffer: {
create: @checked (size: number) -> buffer,
readi8: @checked (b: buffer, offset: number) -> number,
writef64: @checked (b: buffer, offset: number, value: number) -> (),
}
)");
CheckResult result = checkNonStrict(R"(
local b = buffer.create(100)
buffer.writef64(b, 0, 5)
buffer.readi8(b, 0)
)");
LUAU_REQUIRE_NO_ERRORS(result);
}
TEST_SUITE_END();

View File

@ -420,4 +420,22 @@ print(NewProxyOne.HelloICauseACrash)
)");
}
TEST_CASE_FIXTURE(ReplFixture, "InteractiveStackReserve1")
{
// Reset stack reservation
lua_resume(L, nullptr, 0);
runCode(L, R"(
local t = {}
)");
}
TEST_CASE_FIXTURE(ReplFixture, "InteractiveStackReserve2")
{
// Reset stack reservation
lua_resume(L, nullptr, 0);
getCompletionSet("a");
}
TEST_SUITE_END();

View File

@ -915,6 +915,7 @@ TEST_IS_SUBTYPE(numberToNumberType, negate(builtinTypes->classType));
TEST_IS_NOT_SUBTYPE(numberToNumberType, negate(builtinTypes->functionType));
// Negated supertypes: Primitives and singletons
TEST_IS_NOT_SUBTYPE(builtinTypes->stringType, negate(builtinTypes->stringType));
TEST_IS_SUBTYPE(builtinTypes->stringType, negate(builtinTypes->numberType));
TEST_IS_SUBTYPE(str("foo"), meet(builtinTypes->stringType, negate(str("bar"))));
TEST_IS_NOT_SUBTYPE(builtinTypes->trueType, negate(builtinTypes->booleanType));

View File

@ -782,7 +782,10 @@ TEST_CASE_FIXTURE(Fixture, "toStringNamedFunction_map")
TypeId ty = requireType("map");
const FunctionType* ftv = get<FunctionType>(follow(ty));
CHECK_EQ("map<a, b>(arr: {a}, fn: (a) -> b): {b}", toStringNamedFunction("map", *ftv));
if (FFlag::DebugLuauDeferredConstraintResolution)
CHECK_EQ("map<a, b>(arr: {a}, fn: (a) -> (b, ...unknown)): {b}", toStringNamedFunction("map", *ftv));
else
CHECK_EQ("map<a, b>(arr: {a}, fn: (a) -> b): {b}", toStringNamedFunction("map", *ftv));
}
TEST_CASE_FIXTURE(Fixture, "toStringNamedFunction_generic_pack")

View File

@ -3,7 +3,6 @@
#include "Luau/ConstraintSolver.h"
#include "Luau/NotNull.h"
#include "Luau/TxnLog.h"
#include "Luau/Type.h"
#include "ClassFixture.h"
@ -14,6 +13,7 @@
using namespace Luau;
LUAU_FASTFLAG(DebugLuauDeferredConstraintResolution)
LUAU_DYNAMIC_FASTINT(LuauTypeFamilyApplicationCartesianProductLimit)
struct FamilyFixture : Fixture
{
@ -24,7 +24,7 @@ struct FamilyFixture : Fixture
{
swapFamily = TypeFamily{/* name */ "Swap",
/* reducer */
[](TypeId instance, NotNull<TypeFamilyQueue> queue, const std::vector<TypeId>& tys, const std::vector<TypePackId>& tps,
[](TypeId instance, const std::vector<TypeId>& tys, const std::vector<TypePackId>& tps,
NotNull<TypeFamilyContext> ctx) -> TypeFamilyReductionResult<TypeId> {
LUAU_ASSERT(tys.size() == 1);
TypeId param = follow(tys.at(0));
@ -716,4 +716,117 @@ _(setmetatable(_,{[...]=_,}))
)");
}
TEST_CASE_FIXTURE(BuiltinsFixture, "cyclic_concat_family_at_work")
{
if (!FFlag::DebugLuauDeferredConstraintResolution)
return;
CheckResult result = check(R"(
type T = concat<string | T, string>
)");
LUAU_REQUIRE_NO_ERRORS(result);
CHECK(toString(requireTypeAlias("T")) == "string");
}
TEST_CASE_FIXTURE(BuiltinsFixture, "exceeded_distributivity_limits")
{
if (!FFlag::DebugLuauDeferredConstraintResolution)
return;
ScopedFastInt sfi{DFInt::LuauTypeFamilyApplicationCartesianProductLimit, 10};
loadDefinition(R"(
declare class A
function __mul(self, rhs: unknown): A
end
declare class B
function __mul(self, rhs: unknown): B
end
declare class C
function __mul(self, rhs: unknown): C
end
declare class D
function __mul(self, rhs: unknown): D
end
)");
CheckResult result = check(R"(
type T = mul<A | B | C | D, A | B | C | D>
)");
LUAU_REQUIRE_ERROR_COUNT(1, result);
CHECK(get<UninhabitedTypeFamily>(result.errors[0]));
}
TEST_CASE_FIXTURE(BuiltinsFixture, "didnt_quite_exceed_distributivity_limits")
{
if (!FFlag::DebugLuauDeferredConstraintResolution)
return;
// We duplicate the test here because we want to make sure the test failed
// due to exceeding the limits specifically, rather than any possible reasons.
ScopedFastInt sfi{DFInt::LuauTypeFamilyApplicationCartesianProductLimit, 20};
loadDefinition(R"(
declare class A
function __mul(self, rhs: unknown): A
end
declare class B
function __mul(self, rhs: unknown): B
end
declare class C
function __mul(self, rhs: unknown): C
end
declare class D
function __mul(self, rhs: unknown): D
end
)");
CheckResult result = check(R"(
type T = mul<A | B | C | D, A | B | C | D>
)");
LUAU_REQUIRE_NO_ERRORS(result);
}
TEST_CASE_FIXTURE(BuiltinsFixture, "ensure_equivalence_with_distributivity")
{
if (!FFlag::DebugLuauDeferredConstraintResolution)
return;
loadDefinition(R"(
declare class A
function __mul(self, rhs: unknown): A
end
declare class B
function __mul(self, rhs: unknown): B
end
declare class C
function __mul(self, rhs: unknown): C
end
declare class D
function __mul(self, rhs: unknown): D
end
)");
CheckResult result = check(R"(
type T = mul<A | B, C | D>
type U = mul<A, C> | mul<A, D> | mul<B, C> | mul<B, D>
)");
LUAU_REQUIRE_NO_ERRORS(result);
CHECK(toString(requireTypeAlias("T")) == "A | B");
CHECK(toString(requireTypeAlias("U")) == "A | A | B | B");
}
TEST_SUITE_END();

View File

@ -32,15 +32,7 @@ TEST_CASE_FIXTURE(Fixture, "for_in_loop_iterator_returns_any")
LUAU_REQUIRE_NO_ERRORS(result);
if (FFlag::DebugLuauDeferredConstraintResolution)
{
// Bug: We do not simplify at the right time
CHECK_EQ("any?", toString(requireType("a")));
}
else
{
CHECK_EQ(builtinTypes->anyType, requireType("a"));
}
CHECK(builtinTypes->anyType == requireType("a"));
}
TEST_CASE_FIXTURE(Fixture, "for_in_loop_iterator_returns_any2")
@ -58,15 +50,7 @@ TEST_CASE_FIXTURE(Fixture, "for_in_loop_iterator_returns_any2")
LUAU_REQUIRE_NO_ERRORS(result);
if (FFlag::DebugLuauDeferredConstraintResolution)
{
// Bug: We do not simplify at the right time
CHECK_EQ("any?", toString(requireType("a")));
}
else
{
CHECK_EQ("any", toString(requireType("a")));
}
CHECK("any" == toString(requireType("a")));
}
TEST_CASE_FIXTURE(Fixture, "for_in_loop_iterator_is_any")
@ -82,15 +66,7 @@ TEST_CASE_FIXTURE(Fixture, "for_in_loop_iterator_is_any")
LUAU_REQUIRE_NO_ERRORS(result);
if (FFlag::DebugLuauDeferredConstraintResolution)
{
// Bug: We do not simplify at the right time
CHECK_EQ("any?", toString(requireType("a")));
}
else
{
CHECK_EQ("any", toString(requireType("a")));
}
CHECK("any" == toString(requireType("a")));
}
TEST_CASE_FIXTURE(Fixture, "for_in_loop_iterator_is_any2")
@ -104,17 +80,7 @@ TEST_CASE_FIXTURE(Fixture, "for_in_loop_iterator_is_any2")
end
)");
LUAU_REQUIRE_NO_ERRORS(result);
if (FFlag::DebugLuauDeferredConstraintResolution)
{
// Bug: We do not simplify at the right time
CHECK_EQ("any?", toString(requireType("a")));
}
else
{
CHECK_EQ("any", toString(requireType("a")));
}
CHECK("any" == toString(requireType("a")));
}
TEST_CASE_FIXTURE(Fixture, "for_in_loop_iterator_is_any_pack")
@ -130,15 +96,7 @@ TEST_CASE_FIXTURE(Fixture, "for_in_loop_iterator_is_any_pack")
LUAU_REQUIRE_NO_ERRORS(result);
if (FFlag::DebugLuauDeferredConstraintResolution)
{
// Bug: We do not simplify at the right time
CHECK_EQ("any?", toString(requireType("a")));
}
else
{
CHECK_EQ("any", toString(requireType("a")));
}
CHECK("any" == toString(requireType("a")));
}
TEST_CASE_FIXTURE(Fixture, "for_in_loop_iterator_is_error")

View File

@ -1582,7 +1582,7 @@ TEST_CASE_FIXTURE(Fixture, "inferred_higher_order_functions_are_quantified_at_th
if (!result.errors.empty())
{
for (const auto& e : result.errors)
printf("%s %s: %s\n", e.moduleName.c_str(), toString(e.location).c_str(), toString(e).c_str());
MESSAGE(e.moduleName << " " << toString(e.location) << ": " << toString(e));
}
}

View File

@ -433,9 +433,53 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "varlist_declared_by_for_in_loop_should_be_fr
end
)");
if (FFlag::DebugLuauDeferredConstraintResolution)
{
LUAU_REQUIRE_ERROR_COUNT(1, result);
auto err = get<TypeMismatch>(result.errors[0]);
CHECK(err != nullptr);
}
else
{
LUAU_REQUIRE_NO_ERRORS(result);
}
}
TEST_CASE_FIXTURE(BuiltinsFixture, "iter_constraint_before_loop_body")
{
CheckResult result = check(R"(
local T = {
fields = {},
}
function f()
for u, v in pairs(T.fields) do
T.fields[u] = nil
end
end
)");
LUAU_REQUIRE_NO_ERRORS(result);
}
TEST_CASE_FIXTURE(BuiltinsFixture, "rbxl_place_file_crash_for_wrong_constraints")
{
CheckResult result = check(R"(
local VehicleParameters = {
-- These are default values in the case the package structure is broken
StrutSpringStiffnessFront = 28000,
}
local function updateFromConfiguration()
for property, value in pairs(VehicleParameters) do
VehicleParameters[property] = value
end
end
)");
LUAU_REQUIRE_NO_ERRORS(result);
}
TEST_CASE_FIXTURE(BuiltinsFixture, "properly_infer_iteratee_is_a_free_table")
{
// In this case, we cannot know the element type of the table {}. It could be anything.

View File

@ -732,7 +732,11 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "and_binexps_dont_unify")
end
)");
LUAU_REQUIRE_NO_ERRORS(result);
// This infers a type for `t` of `{unknown}`, and so it makes sense that `t[1].test` would error.
if (FFlag::DebugLuauDeferredConstraintResolution)
LUAU_REQUIRE_ERROR_COUNT(1, result);
else
LUAU_REQUIRE_NO_ERRORS(result);
}
TEST_CASE_FIXTURE(Fixture, "error_on_invalid_operand_types_to_relational_operators")

View File

@ -101,4 +101,14 @@ TEST_CASE("singleton_types")
CHECK(result.errors.empty());
}
TEST_CASE_FIXTURE(BuiltinsFixture, "property_of_buffers")
{
CheckResult result = check(R"(
local b = buffer.create(100)
print(b.foo)
)");
LUAU_REQUIRE_ERROR_COUNT(1, result);
}
TEST_SUITE_END();

View File

@ -44,6 +44,20 @@ TEST_CASE_FIXTURE(Fixture, "string_singletons")
LUAU_REQUIRE_NO_ERRORS(result);
}
TEST_CASE_FIXTURE(Fixture, "string_singleton_function_call")
{
if (!FFlag::DebugLuauDeferredConstraintResolution)
return;
CheckResult result = check(R"(
local x = "a"
function f(x: "a") end
f(x)
)");
LUAU_REQUIRE_NO_ERRORS(result);
}
TEST_CASE_FIXTURE(Fixture, "bool_singletons_mismatch")
{
CheckResult result = check(R"(

View File

@ -2462,10 +2462,7 @@ local x: {number} | number | string
local y = #x
)");
if (FFlag::DebugLuauDeferredConstraintResolution)
LUAU_REQUIRE_ERROR_COUNT(2, result);
else
LUAU_REQUIRE_ERROR_COUNT(1, result);
LUAU_REQUIRE_ERROR_COUNT(1, result);
}
TEST_CASE_FIXTURE(BuiltinsFixture, "dont_hang_when_trying_to_look_up_in_cyclic_metatable_index")
@ -2973,7 +2970,7 @@ c = b
const TableType* ttv = get<TableType>(*ty);
REQUIRE(ttv);
CHECK(ttv->instantiatedTypeParams.empty());
CHECK(0 == ttv->instantiatedTypeParams.size());
}
TEST_CASE_FIXTURE(Fixture, "table_indexing_error_location")
@ -4355,19 +4352,6 @@ TEST_CASE_FIXTURE(Fixture, "mymovie_read_write_tables_bug_2")
LUAU_REQUIRE_ERRORS(result);
}
TEST_CASE_FIXTURE(Fixture, "setindexer_always_transmute")
{
ScopedFastFlag sff{FFlag::DebugLuauDeferredConstraintResolution, true};
CheckResult result = check(R"(
function f(x)
(5)[5] = x
end
)");
CHECK_EQ("(*error-type*) -> ()", toString(requireType("f")));
}
TEST_CASE_FIXTURE(BuiltinsFixture, "instantiated_metatable_frozen_table_clone_mutation")
{
ScopedFastFlag luauMetatableInstantiationCloneCheck{FFlag::LuauMetatableInstantiationCloneCheck, true};
@ -4412,6 +4396,21 @@ TEST_CASE_FIXTURE(Fixture, "setprop_on_a_mutating_local_in_both_loops_and_functi
LUAU_REQUIRE_ERRORS(result);
}
TEST_CASE_FIXTURE(Fixture, "cant_index_this")
{
CheckResult result = check(R"(
local a: number = 9
a[18] = "tomfoolery"
)");
LUAU_REQUIRE_ERROR_COUNT(1, result);
NotATable* notATable = get<NotATable>(result.errors[0]);
REQUIRE(notATable);
CHECK("number" == toString(notATable->ty));
}
TEST_CASE_FIXTURE(Fixture, "setindexer_multiple_tables_intersection")
{
ScopedFastFlag sff{FFlag::DebugLuauDeferredConstraintResolution, true};
@ -4423,8 +4422,8 @@ TEST_CASE_FIXTURE(Fixture, "setindexer_multiple_tables_intersection")
end
)");
LUAU_REQUIRE_NO_ERRORS(result);
CHECK("({ [string]: number } & { [thread]: boolean }, boolean | number) -> ()" == toString(requireType("f")));
LUAU_REQUIRE_ERROR_COUNT(2, result);
CHECK("({ [string]: number } & { [thread]: boolean }, never) -> ()" == toString(requireType("f")));
}
TEST_CASE_FIXTURE(Fixture, "insert_a_and_f_of_a_into_table_res_in_a_loop")

View File

@ -406,6 +406,7 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "prototyped_recursive_functions_but_has_futur
)");
LUAU_REQUIRE_ERROR_COUNT(1, result);
CHECK("((() -> ()) | number)?" == toString(requireType("f")));
}

View File

@ -606,13 +606,7 @@ TEST_CASE_FIXTURE(Fixture, "indexing_into_a_cyclic_union_doesnt_crash")
end
)");
// The old solver has a bug: It doesn't consider this goofy thing to be a
// table. It's not really important. What's important is that we don't
// crash, hang, or ICE.
if (FFlag::DebugLuauDeferredConstraintResolution)
LUAU_REQUIRE_NO_ERRORS(result);
else
LUAU_REQUIRE_ERROR_COUNT(1, result);
LUAU_REQUIRE_ERROR_COUNT(1, result);
}
TEST_CASE_FIXTURE(BuiltinsFixture, "table_union_write_indirect")

View File

@ -65,30 +65,6 @@ do
a = table.move({[minI] = 100}, minI, minI, maxI)
eqT(a, {[minI] = 100, [maxI] = 100})
-- moving small amount of elements (array/hash) using a wide range
a = {}
table.move({1, 2, 3, 4, 5}, -100000000, 100000000, -100000000, a)
eqT(a, {1, 2, 3, 4, 5})
a = {}
table.move({1, 2}, -100000000, 100000000, 0, a)
eqT(a, {[100000001] = 1, [100000002] = 2})
-- hash part copy
a = {}
table.move({[-1000000] = 1, [-100] = 2, [100] = 3, [100000] = 4}, -100000000, 100000000, 0, a)
eqT(a, {[99000000] = 1, [99999900] = 2, [100000100] = 3, [100100000] = 4})
-- precise hash part bounds
a = {}
table.move({[-100000000 - 1] = -1, [-100000000] = 1, [-100] = 2, [100] = 3, [100000000] = 4, [100000000 + 1] = -1}, -100000000, 100000000, 0, a)
eqT(a, {[0] = 1, [99999900] = 2, [100000100] = 3, [200000000] = 4})
-- no integer undeflow in corner hash part case
a = {}
table.move({[minI] = 100, [-100] = 2}, minI, minI + 100000000, minI, a)
eqT(a, {[minI] = 100})
-- hash part skips array slice
a = {}
table.move({[-1] = 1, [0] = 2, [1] = 3, [2] = 4}, -1, 3, 1, a)
@ -97,6 +73,19 @@ do
a = {}
table.move({[-1] = 1, [0] = 2, [1] = 3, [2] = 4, [10] = 5, [100] = 6, [1000] = 7}, -1, 3, 1, a)
eqT(a, {[1] = 1, [2] = 2, [3] = 3, [4] = 4})
-- moving ranges containing nil values into tables with values
a = {1, 2, 3, 4, 5}
table.move({10}, 1, 3, 2, a)
eqT(a, {1, 10, nil, nil, 5})
a = {1, 2, 3, 4, 5}
table.move({10}, -1, 1, 2, a)
eqT(a, {1, nil, nil, 10, 5})
a = {[-1000] = 1, [1000] = 2, [1] = 3}
table.move({10}, -1000, 1000, -1000, a)
eqT(a, {10})
end
checkerror("too many", table.move, {}, 0, maxI, 1)

View File

@ -208,6 +208,35 @@ end
assert(pcall(fuzzfail21) == false)
local function fuzzfail22(...)
local _ = {false,},true,...,l0
while _ do
_ = true,{unpack(0,_),},l0
_.n126 = nil
_ = {not _,_=not _,n0=_,_,n0=not _,},_ < _
return _ > _
end
return `""`
end
assert(pcall(fuzzfail22) == false)
local function fuzzfail23(...)
local _ = {false,},_,...,l0
while _ do
_ = true,{unpack(_),},l0
_ = {{[_]=nil,_=not _,_,true,_=nil,},not _,not _,_,bxor=- _,}
do end
break
end
do end
local _ = _,true
do end
local _ = _,true
end
assert(pcall(fuzzfail23) == false)
local function arraySizeInv1()
local t = {1, 2, nil, nil, nil, nil, nil, nil, nil, true}

View File

@ -71,7 +71,6 @@ GenericsTests.no_stack_overflow_from_quantifying
GenericsTests.properties_can_be_instantiated_polytypes
GenericsTests.quantify_functions_even_if_they_have_an_explicit_generic
GenericsTests.self_recursive_instantiated_param
IntersectionTypes.CLI-44817
IntersectionTypes.error_detailed_intersection_all
IntersectionTypes.error_detailed_intersection_part
IntersectionTypes.intersect_bool_and_false
@ -134,11 +133,8 @@ RefinementTest.call_an_incompatible_function_after_using_typeguard
RefinementTest.dataflow_analysis_can_tell_refinements_when_its_appropriate_to_refine_into_nil_or_never
RefinementTest.discriminate_from_isa_of_x
RefinementTest.discriminate_from_truthiness_of_x
RefinementTest.free_type_is_equal_to_an_lvalue
RefinementTest.globals_can_be_narrowed_too
RefinementTest.isa_type_refinement_must_be_known_ahead_of_time
RefinementTest.luau_polyfill_isindexkey_refine_conjunction
RefinementTest.luau_polyfill_isindexkey_refine_conjunction_variant
RefinementTest.not_t_or_some_prop_of_t
RefinementTest.refine_a_param_that_got_resolved_during_constraint_solving_stage
RefinementTest.refine_a_property_of_some_global
@ -157,7 +153,6 @@ TableTests.a_free_shape_can_turn_into_a_scalar_if_it_is_compatible
TableTests.a_free_shape_cannot_turn_into_a_scalar_if_it_is_not_compatible
TableTests.any_when_indexing_into_an_unsealed_table_with_no_indexer_in_nonstrict_mode
TableTests.array_factory_function
TableTests.cannot_augment_sealed_table
TableTests.casting_tables_with_props_into_table_with_indexer2
TableTests.casting_tables_with_props_into_table_with_indexer3
TableTests.casting_unsealed_tables_with_props_into_table_with_indexer
@ -181,20 +176,18 @@ TableTests.generalize_table_argument
TableTests.generic_table_instantiation_potential_regression
TableTests.indexer_on_sealed_table_must_unify_with_free_table
TableTests.indexers_get_quantified_too
TableTests.inequality_operators_imply_exactly_matching_types
TableTests.infer_array
TableTests.infer_indexer_from_array_like_table
TableTests.infer_indexer_from_its_variable_type_and_unifiable
TableTests.inferred_return_type_of_free_table
TableTests.invariant_table_properties_means_instantiating_tables_in_assignment_is_unsound
TableTests.invariant_table_properties_means_instantiating_tables_in_call_is_unsound
TableTests.length_operator_union
TableTests.less_exponential_blowup_please
TableTests.meta_add
TableTests.meta_add_inferred
TableTests.metatable_mismatch_should_fail
TableTests.missing_metatable_for_sealed_tables_do_not_get_inferred
TableTests.mixed_tables_with_implicit_numbered_keys
TableTests.nil_assign_doesnt_hit_indexer
TableTests.ok_to_provide_a_subtype_during_construction
TableTests.ok_to_set_nil_even_on_non_lvalue_base_expr
TableTests.okay_to_add_property_to_unsealed_tables_by_assignment
@ -202,7 +195,6 @@ TableTests.okay_to_add_property_to_unsealed_tables_by_function_call
TableTests.only_ascribe_synthetic_names_at_module_scope
TableTests.open_table_unification_2
TableTests.parameter_was_set_an_indexer_and_bounded_by_another_parameter
TableTests.parameter_was_set_an_indexer_and_bounded_by_string
TableTests.pass_a_union_of_tables_to_a_function_that_requires_a_table
TableTests.pass_a_union_of_tables_to_a_function_that_requires_a_table_2
TableTests.persistent_sealed_table_is_immutable
@ -210,7 +202,6 @@ TableTests.quantify_even_that_table_was_never_exported_at_all
TableTests.quantify_metatables_of_metatables_of_table
TableTests.reasonable_error_when_adding_a_nonexistent_property_to_an_array_like_table
TableTests.recursive_metatable_type_call
TableTests.refined_thing_can_be_an_array
TableTests.right_table_missing_key2
TableTests.scalar_is_a_subtype_of_a_compatible_polymorphic_shape_type
TableTests.scalar_is_not_a_subtype_of_a_compatible_polymorphic_shape_type
@ -228,12 +219,10 @@ TableTests.table_subtyping_with_extra_props_dont_report_multiple_errors
TableTests.table_subtyping_with_missing_props_dont_report_multiple_errors2
TableTests.table_unification_4
TableTests.table_unifies_into_map
TableTests.table_writes_introduce_write_properties
TableTests.type_mismatch_on_massive_table_is_cut_short
TableTests.used_colon_instead_of_dot
TableTests.used_dot_instead_of_colon
TableTests.when_augmenting_an_unsealed_table_with_an_indexer_apply_the_correct_scope_to_the_indexer_type
TableTests.wrong_assign_does_hit_indexer
ToDot.function
ToString.exhaustive_toString_of_cyclic_table
ToString.free_types
@ -274,6 +263,7 @@ TypeInfer.cli_50041_committing_txnlog_in_apollo_client_error
TypeInfer.dont_ice_when_failing_the_occurs_check
TypeInfer.dont_report_type_errors_within_an_AstExprError
TypeInfer.dont_report_type_errors_within_an_AstStatError
TypeInfer.follow_on_new_types_in_substitution
TypeInfer.globals
TypeInfer.globals2
TypeInfer.infer_through_group_expr
@ -285,9 +275,10 @@ TypeInfer.type_infer_recursion_limit_no_ice
TypeInfer.type_infer_recursion_limit_normalizer
TypeInfer.unify_nearly_identical_recursive_types
TypeInferAnyError.can_subscript_any
TypeInferAnyError.for_in_loop_iterator_is_error
TypeInferAnyError.for_in_loop_iterator_is_error2
TypeInferAnyError.replace_every_free_type_when_unifying_a_complex_function_with_any
TypeInferAnyError.for_in_loop_iterator_is_any
TypeInferAnyError.for_in_loop_iterator_is_any2
TypeInferAnyError.for_in_loop_iterator_is_any_pack
TypeInferAnyError.for_in_loop_iterator_returns_any2
TypeInferClasses.callable_classes
TypeInferClasses.cannot_unify_class_instance_with_primitive
TypeInferClasses.class_type_mismatch_with_name_conflict
@ -317,10 +308,8 @@ TypeInferFunctions.function_does_not_return_enough_values
TypeInferFunctions.function_exprs_are_generalized_at_signature_scope_not_enclosing
TypeInferFunctions.function_is_supertype_of_concrete_functions
TypeInferFunctions.function_statement_sealed_table_assignment_through_indexer
TypeInferFunctions.fuzzer_missing_follow_in_ast_stat_fun
TypeInferFunctions.generic_packs_are_not_variadic
TypeInferFunctions.higher_order_function_2
TypeInferFunctions.higher_order_function_3
TypeInferFunctions.higher_order_function_4
TypeInferFunctions.improved_function_arg_mismatch_error_nonstrict
TypeInferFunctions.improved_function_arg_mismatch_errors
@ -338,7 +327,6 @@ TypeInferFunctions.occurs_check_failure_in_function_return_type
TypeInferFunctions.other_things_are_not_related_to_function
TypeInferFunctions.param_1_and_2_both_takes_the_same_generic_but_their_arguments_are_incompatible
TypeInferFunctions.param_1_and_2_both_takes_the_same_generic_but_their_arguments_are_incompatible_2
TypeInferFunctions.regex_benchmark_string_format_minimization
TypeInferFunctions.report_exiting_without_return_nonstrict
TypeInferFunctions.return_type_by_overload
TypeInferFunctions.tf_suggest_return_type
@ -370,9 +358,7 @@ TypeInferLoops.loop_iter_trailing_nil
TypeInferLoops.loop_typecheck_crash_on_empty_optional
TypeInferLoops.properly_infer_iteratee_is_a_free_table
TypeInferLoops.repeat_loop
TypeInferLoops.varlist_declared_by_for_in_loop_should_be_free
TypeInferLoops.while_loop
TypeInferModules.do_not_modify_imported_types_5
TypeInferModules.require
TypeInferOOP.dont_suggest_using_colon_rather_than_dot_if_it_wont_help_2
TypeInferOOP.dont_suggest_using_colon_rather_than_dot_if_not_defined_with_colon
@ -396,7 +382,6 @@ TypeInferOperators.typecheck_unary_len_error
TypeInferOperators.typecheck_unary_minus_error
TypeInferOperators.UnknownGlobalCompoundAssign
TypeInferPrimitives.CheckMethodsOfNumber
TypeInferPrimitives.string_index
TypeInferUnknownNever.assign_to_local_which_is_never
TypeInferUnknownNever.index_on_union_of_tables_for_properties_that_is_never
TypeInferUnknownNever.index_on_union_of_tables_for_properties_that_is_sorta_never
@ -414,6 +399,7 @@ TypeSingletons.error_detailed_tagged_union_mismatch_string
TypeSingletons.overloaded_function_call_with_singletons_mismatch
TypeSingletons.return_type_of_f_is_not_widened
TypeSingletons.singletons_stick_around_under_assignment
TypeSingletons.string_singleton_function_call
TypeSingletons.table_properties_type_error_escapes
TypeSingletons.widen_the_supertype_if_it_is_free_and_subtype_has_singleton
TypeStatesTest.typestates_preserve_error_suppression_properties
@ -423,7 +409,6 @@ UnionTypes.generic_function_with_optional_arg
UnionTypes.index_on_a_union_type_with_missing_property
UnionTypes.less_greedy_unification_with_union_types
UnionTypes.optional_arguments_table
UnionTypes.optional_length_error
UnionTypes.optional_union_functions
UnionTypes.optional_union_members
UnionTypes.optional_union_methods