Merge branch 'upstream' into merge

This commit is contained in:
Andy Friesen 2023-11-17 10:18:37 -08:00
commit 2d4a544709
58 changed files with 1736 additions and 373 deletions

View File

@ -190,6 +190,11 @@ struct UnpackConstraint
{ {
TypePackId resultPack; TypePackId resultPack;
TypePackId sourcePack; 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 ~ refine type mode discriminant // resultType ~ refine type mode discriminant

View File

@ -78,11 +78,13 @@ struct ConstraintGenerator
TypeIds types; TypeIds types;
}; };
// During constraint generation, we only populate the Scope::bindings // Some locals have multiple type states. We wish for Scope::bindings to
// property for annotated symbols. Unannotated symbols must be handled in a // map each local name onto the union of every type that the local can have
// postprocessing step because we have not yet allocated the types that will // over its lifetime, so we use this map to accumulate the set of types it
// be assigned to those unannotated symbols, so we queue them up here. // might have.
std::map<Symbol, InferredBinding> inferredBindings; //
// See the functions recordInferredBinding and fillInInferredBindings.
DenseHashMap<Symbol, InferredBinding> inferredBindings{{}};
// Constraints that go straight to the solver. // Constraints that go straight to the solver.
std::vector<ConstraintPtr> constraints; std::vector<ConstraintPtr> constraints;
@ -245,8 +247,6 @@ private:
std::optional<TypeId> checkLValue(const ScopePtr& scope, AstExprIndexExpr* indexExpr, TypeId assignedTy); std::optional<TypeId> checkLValue(const ScopePtr& scope, AstExprIndexExpr* indexExpr, TypeId assignedTy);
TypeId updateProperty(const ScopePtr& scope, AstExpr* expr, TypeId assignedTy); TypeId updateProperty(const ScopePtr& scope, AstExpr* expr, TypeId assignedTy);
void updateLValueType(AstExpr* lvalue, TypeId ty);
struct FunctionSignature struct FunctionSignature
{ {
// The type of the function. // The type of the function.
@ -336,6 +336,10 @@ private:
*/ */
void prepopulateGlobalScope(const ScopePtr& globalScope, AstStatBlock* program); void prepopulateGlobalScope(const ScopePtr& globalScope, AstStatBlock* program);
// Record the fact that a particular local has a particular type in at least
// one of its states.
void recordInferredBinding(AstLocal* local, TypeId ty);
void fillInInferredBindings(const ScopePtr& globalScope, AstStatBlock* block); void fillInInferredBindings(const ScopePtr& globalScope, AstStatBlock* block);
/** Given a function type annotation, return a vector describing the expected types of the calls to the function /** Given a function type annotation, return a vector describing the expected types of the calls to the function

View File

@ -77,8 +77,11 @@ struct DfgScope
DfgScope* parent; DfgScope* parent;
bool isLoopScope; bool isLoopScope;
DenseHashMap<Symbol, const Def*> bindings{Symbol{}}; using Bindings = DenseHashMap<Symbol, const Def*>;
DenseHashMap<const Def*, std::unordered_map<std::string, const Def*>> props{nullptr}; using Props = DenseHashMap<const Def*, std::unordered_map<std::string, const Def*>>;
Bindings bindings{Symbol{}};
Props props{nullptr};
std::optional<DefId> lookup(Symbol symbol) const; std::optional<DefId> lookup(Symbol symbol) const;
std::optional<DefId> lookup(DefId def, const std::string& key) const; std::optional<DefId> lookup(DefId def, const std::string& key) const;
@ -115,7 +118,13 @@ private:
std::vector<std::unique_ptr<DfgScope>> scopes; std::vector<std::unique_ptr<DfgScope>> scopes;
DfgScope* childScope(DfgScope* scope, bool isLoopScope = false); DfgScope* childScope(DfgScope* scope, bool isLoopScope = false);
void join(DfgScope* parent, DfgScope* a, DfgScope* b);
void join(DfgScope* p, DfgScope* a, DfgScope* b);
void joinBindings(DfgScope::Bindings& p, const DfgScope::Bindings& a, const DfgScope::Bindings& b);
void joinProps(DfgScope::Props& p, const DfgScope::Props& a, const DfgScope::Props& b);
DefId lookup(DfgScope* scope, Symbol symbol);
DefId lookup(DfgScope* scope, DefId def, const std::string& key);
ControlFlow visit(DfgScope* scope, AstStatBlock* b); ControlFlow visit(DfgScope* scope, AstStatBlock* b);
ControlFlow visitBlockWithoutChildScope(DfgScope* scope, AstStatBlock* b); ControlFlow visitBlockWithoutChildScope(DfgScope* scope, AstStatBlock* b);

View File

@ -80,6 +80,7 @@ struct DefArena
DefId freshCell(bool subscripted = false); DefId freshCell(bool subscripted = false);
DefId phi(DefId a, DefId b); DefId phi(DefId a, DefId b);
DefId phi(const std::vector<DefId>& defs);
}; };
} // namespace Luau } // namespace Luau

View File

@ -56,6 +56,7 @@ struct Scope
void addBuiltinTypeBinding(const Name& name, const TypeFun& tyFun); void addBuiltinTypeBinding(const Name& name, const TypeFun& tyFun);
std::optional<TypeId> lookup(Symbol sym) const; std::optional<TypeId> lookup(Symbol sym) const;
std::optional<TypeId> lookupUnrefinedType(DefId def) const;
std::optional<TypeId> lookup(DefId def) const; std::optional<TypeId> lookup(DefId def) const;
std::optional<std::pair<TypeId, Scope*>> lookupEx(DefId def); std::optional<std::pair<TypeId, Scope*>> lookupEx(DefId def);
std::optional<std::pair<Binding*, Scope*>> lookupEx(Symbol sym); std::optional<std::pair<Binding*, Scope*>> lookupEx(Symbol sym);

View File

@ -15,10 +15,14 @@ template<typename T, typename Hash = SetHashDefault<T>>
class Set class Set
{ {
private: private:
DenseHashMap<T, bool, Hash> mapping; using Impl = DenseHashMap<T, bool, Hash>;
Impl mapping;
size_t entryCount = 0; size_t entryCount = 0;
public: public:
class const_iterator;
using iterator = const_iterator;
Set(const T& empty_key) Set(const T& empty_key)
: mapping{empty_key} : mapping{empty_key}
{ {
@ -83,6 +87,16 @@ public:
return count(element) != 0; return count(element) != 0;
} }
const_iterator begin() const
{
return const_iterator(mapping.begin(), mapping.end());
}
const_iterator end() const
{
return const_iterator(mapping.end(), mapping.end());
}
bool operator==(const Set<T>& there) const bool operator==(const Set<T>& there) const
{ {
// if the sets are unequal sizes, then they cannot possibly be equal. // if the sets are unequal sizes, then they cannot possibly be equal.
@ -100,6 +114,58 @@ public:
// otherwise, we've proven the two equal! // otherwise, we've proven the two equal!
return true; return true;
} }
class const_iterator
{
public:
const_iterator(typename Impl::const_iterator impl, typename Impl::const_iterator end)
: impl(impl)
, end(end)
{}
const T& operator*() const
{
return impl->first;
}
const T* operator->() const
{
return &impl->first;
}
bool operator==(const const_iterator& other) const
{
return impl == other.impl;
}
bool operator!=(const const_iterator& other) const
{
return impl != other.impl;
}
const_iterator& operator++()
{
do
{
impl++;
} while (impl != end && impl->second == false);
// keep iterating past pairs where the value is `false`
return *this;
}
const_iterator operator++(int)
{
const_iterator res = *this;
++*this;
return res;
}
private:
typename Impl::const_iterator impl;
typename Impl::const_iterator end;
};
}; };
} // namespace Luau } // namespace Luau

View File

@ -27,10 +27,19 @@ struct TypeArena;
struct Scope; struct Scope;
struct TableIndexer; struct TableIndexer;
enum class SubtypingVariance
{
// Used for an empty key. Should never appear in actual code.
Invalid,
Covariant,
Invariant,
};
struct SubtypingReasoning struct SubtypingReasoning
{ {
Path subPath; Path subPath;
Path superPath; Path superPath;
SubtypingVariance variance = SubtypingVariance::Covariant;
bool operator==(const SubtypingReasoning& other) const; bool operator==(const SubtypingReasoning& other) const;
}; };
@ -49,7 +58,8 @@ struct SubtypingResult
/// The reason for isSubtype to be false. May not be present even if /// The reason for isSubtype to be false. May not be present even if
/// isSubtype is false, depending on the input types. /// isSubtype is false, depending on the input types.
DenseHashSet<SubtypingReasoning, SubtypingReasoningHash> reasoning{SubtypingReasoning{}}; DenseHashSet<SubtypingReasoning, SubtypingReasoningHash> reasoning{
SubtypingReasoning{TypePath::kEmpty, TypePath::kEmpty, SubtypingVariance::Invalid}};
SubtypingResult& andAlso(const SubtypingResult& other); SubtypingResult& andAlso(const SubtypingResult& other);
SubtypingResult& orElse(const SubtypingResult& other); SubtypingResult& orElse(const SubtypingResult& other);
@ -59,6 +69,7 @@ struct SubtypingResult
SubtypingResult& withBothPath(TypePath::Path path); SubtypingResult& withBothPath(TypePath::Path path);
SubtypingResult& withSubPath(TypePath::Path path); SubtypingResult& withSubPath(TypePath::Path path);
SubtypingResult& withSuperPath(TypePath::Path path); SubtypingResult& withSuperPath(TypePath::Path path);
SubtypingResult& withVariance(SubtypingVariance variance);
// Only negates the `isSubtype`. // Only negates the `isSubtype`.
static SubtypingResult negate(const SubtypingResult& result); static SubtypingResult negate(const SubtypingResult& result);

View File

@ -86,6 +86,24 @@ struct FreeType
TypeId upperBound = nullptr; TypeId upperBound = nullptr;
}; };
/** A type that tracks the domain of a local variable.
*
* We consider each local's domain to be the union of all types assigned to it.
* We accomplish this with LocalType. Each time we dispatch an assignment to a
* local, we accumulate this union and decrement blockCount.
*
* When blockCount reaches 0, we can consider the LocalType to be "fully baked"
* and replace it with the union we've built.
*/
struct LocalType
{
TypeId domain;
int blockCount = 0;
// Used for debugging
std::string name;
};
struct GenericType struct GenericType
{ {
// By default, generics are global, with a synthetic name // By default, generics are global, with a synthetic name
@ -623,7 +641,7 @@ struct NegationType
using ErrorType = Unifiable::Error; using ErrorType = Unifiable::Error;
using TypeVariant = using TypeVariant =
Unifiable::Variant<TypeId, FreeType, GenericType, PrimitiveType, BlockedType, PendingExpansionType, SingletonType, FunctionType, TableType, Unifiable::Variant<TypeId, FreeType, LocalType, GenericType, PrimitiveType, BlockedType, PendingExpansionType, SingletonType, FunctionType, TableType,
MetatableType, ClassType, AnyType, UnionType, IntersectionType, LazyType, UnknownType, NeverType, NegationType, TypeFamilyInstanceType>; MetatableType, ClassType, AnyType, UnionType, IntersectionType, LazyType, UnknownType, NeverType, NegationType, TypeFamilyInstanceType>;
struct Type final struct Type final

View File

@ -97,6 +97,10 @@ struct GenericTypeVisitor
{ {
return visit(ty); return visit(ty);
} }
virtual bool visit(TypeId ty, const LocalType& ftv)
{
return visit(ty);
}
virtual bool visit(TypeId ty, const GenericType& gtv) virtual bool visit(TypeId ty, const GenericType& gtv)
{ {
return visit(ty); return visit(ty);
@ -241,6 +245,11 @@ struct GenericTypeVisitor
else else
visit(ty, *ftv); visit(ty, *ftv);
} }
else if (auto lt = get<LocalType>(ty))
{
if (visit(ty, *lt))
traverse(lt->domain);
}
else if (auto gtv = get<GenericType>(ty)) else if (auto gtv = get<GenericType>(ty))
visit(ty, *gtv); visit(ty, *gtv);
else if (auto etv = get<ErrorType>(ty)) else if (auto etv = get<ErrorType>(ty))

View File

@ -261,6 +261,11 @@ private:
t->upperBound = shallowClone(t->upperBound); t->upperBound = shallowClone(t->upperBound);
} }
void cloneChildren(LocalType* t)
{
t->domain = shallowClone(t->domain);
}
void cloneChildren(GenericType* t) void cloneChildren(GenericType* t)
{ {
// TOOD: clone upper bounds. // TOOD: clone upper bounds.
@ -504,6 +509,7 @@ struct TypeCloner
void defaultClone(const T& t); void defaultClone(const T& t);
void operator()(const FreeType& t); void operator()(const FreeType& t);
void operator()(const LocalType& t);
void operator()(const GenericType& t); void operator()(const GenericType& t);
void operator()(const BoundType& t); void operator()(const BoundType& t);
void operator()(const ErrorType& t); void operator()(const ErrorType& t);
@ -631,6 +637,11 @@ void TypeCloner::operator()(const FreeType& t)
defaultClone(t); defaultClone(t);
} }
void TypeCloner::operator()(const LocalType& t)
{
defaultClone(t);
}
void TypeCloner::operator()(const GenericType& t) void TypeCloner::operator()(const GenericType& t)
{ {
defaultClone(t); defaultClone(t);

View File

@ -205,33 +205,6 @@ ScopePtr ConstraintGenerator::childScope(AstNode* node, const ScopePtr& parent)
return scope; return scope;
} }
static std::vector<DefId> flatten(const Phi* phi)
{
std::vector<DefId> result;
std::deque<DefId> queue{phi->operands.begin(), phi->operands.end()};
DenseHashSet<const Def*> seen{nullptr};
while (!queue.empty())
{
DefId next = queue.front();
queue.pop_front();
// Phi nodes should never be cyclic.
LUAU_ASSERT(!seen.find(next));
if (seen.find(next))
continue;
seen.insert(next);
if (get<Cell>(next))
result.push_back(next);
else if (auto phi = get<Phi>(next))
queue.insert(queue.end(), phi->operands.begin(), phi->operands.end());
}
return result;
}
std::optional<TypeId> ConstraintGenerator::lookup(Scope* scope, DefId def) std::optional<TypeId> ConstraintGenerator::lookup(Scope* scope, DefId def)
{ {
if (get<Cell>(def)) if (get<Cell>(def))
@ -243,7 +216,7 @@ std::optional<TypeId> ConstraintGenerator::lookup(Scope* scope, DefId def)
TypeId res = builtinTypes->neverType; TypeId res = builtinTypes->neverType;
for (DefId operand : flatten(phi)) for (DefId operand : phi->operands)
{ {
// `scope->lookup(operand)` may return nothing because it could be a phi node of globals, but one of // `scope->lookup(operand)` may return nothing because it could be a phi node of globals, but one of
// the operand of that global has never been assigned a type, and so it should be an error. // the operand of that global has never been assigned a type, and so it should be an error.
@ -621,8 +594,12 @@ ControlFlow ConstraintGenerator::visit(const ScopePtr& scope, AstStat* stat)
ControlFlow ConstraintGenerator::visit(const ScopePtr& scope, AstStatLocal* statLocal) ControlFlow ConstraintGenerator::visit(const ScopePtr& scope, AstStatLocal* statLocal)
{ {
std::vector<std::optional<TypeId>> varTypes; std::vector<TypeId> annotatedTypes;
varTypes.reserve(statLocal->vars.size); annotatedTypes.reserve(statLocal->vars.size);
bool hasAnnotation = false;
std::vector<std::optional<TypeId>> expectedTypes;
expectedTypes.reserve(statLocal->vars.size);
std::vector<TypeId> assignees; std::vector<TypeId> assignees;
assignees.reserve(statLocal->vars.size); assignees.reserve(statLocal->vars.size);
@ -635,7 +612,8 @@ ControlFlow ConstraintGenerator::visit(const ScopePtr& scope, AstStatLocal* stat
{ {
const Location location = local->location; const Location location = local->location;
TypeId assignee = arena->addType(BlockedType{}); TypeId assignee = arena->addType(LocalType{builtinTypes->neverType, /* blockCount */ 1, local->name.value});
assignees.push_back(assignee); assignees.push_back(assignee);
if (!firstValueType) if (!firstValueType)
@ -643,16 +621,21 @@ ControlFlow ConstraintGenerator::visit(const ScopePtr& scope, AstStatLocal* stat
if (local->annotation) if (local->annotation)
{ {
hasAnnotation = true;
TypeId annotationTy = resolveType(scope, local->annotation, /* inTypeArguments */ false); TypeId annotationTy = resolveType(scope, local->annotation, /* inTypeArguments */ false);
varTypes.push_back(annotationTy); annotatedTypes.push_back(annotationTy);
expectedTypes.push_back(annotationTy);
addConstraint(scope, local->location, SubtypeConstraint{assignee, annotationTy});
scope->bindings[local] = Binding{annotationTy, location}; scope->bindings[local] = Binding{annotationTy, location};
} }
else else
{ {
varTypes.push_back(std::nullopt); // annotatedTypes must contain one type per local. If a particular
// local has no annotation at, assume the most conservative thing.
annotatedTypes.push_back(builtinTypes->unknownType);
expectedTypes.push_back(std::nullopt);
scope->bindings[local] = Binding{builtinTypes->unknownType, location};
inferredBindings[local] = {scope.get(), location, {assignee}}; inferredBindings[local] = {scope.get(), location, {assignee}};
} }
@ -661,8 +644,12 @@ ControlFlow ConstraintGenerator::visit(const ScopePtr& scope, AstStatLocal* stat
scope->lvalueTypes[def] = assignee; scope->lvalueTypes[def] = assignee;
} }
TypePackId resultPack = checkPack(scope, statLocal->values, varTypes).tp; TypePackId resultPack = checkPack(scope, statLocal->values, expectedTypes).tp;
addConstraint(scope, statLocal->location, UnpackConstraint{arena->addTypePack(std::move(assignees)), resultPack}); addConstraint(scope, statLocal->location, UnpackConstraint{arena->addTypePack(std::move(assignees)), resultPack, /*resultIsLValue*/ true});
// Types must flow between whatever annotations were provided and the rhs expression.
if (hasAnnotation)
addConstraint(scope, statLocal->location, PackSubtypeConstraint{resultPack, arena->addTypePack(std::move(annotatedTypes))});
if (statLocal->vars.size == 1 && statLocal->values.size == 1 && firstValueType && scope.get() == rootScope) if (statLocal->vars.size == 1 && statLocal->values.size == 1 && firstValueType && scope.get() == rootScope)
{ {
@ -1006,26 +993,22 @@ static void bindFreeType(TypeId a, TypeId b)
ControlFlow ConstraintGenerator::visit(const ScopePtr& scope, AstStatAssign* assign) ControlFlow ConstraintGenerator::visit(const ScopePtr& scope, AstStatAssign* assign)
{ {
std::vector<std::optional<TypeId>> expectedTypes;
expectedTypes.reserve(assign->vars.size);
std::vector<TypeId> assignees; std::vector<TypeId> assignees;
assignees.reserve(assign->vars.size); assignees.reserve(assign->vars.size);
for (AstExpr* lvalue : assign->vars) for (AstExpr* lvalue : assign->vars)
{ {
TypeId assignee = arena->addType(BlockedType{}); TypeId assignee = arena->addType(BlockedType{});
assignees.push_back(assignee);
checkLValue(scope, lvalue, assignee); checkLValue(scope, lvalue, assignee);
assignees.push_back(assignee);
DefId def = dfg->getDef(lvalue); DefId def = dfg->getDef(lvalue);
scope->lvalueTypes[def] = assignee; scope->lvalueTypes[def] = assignee;
updateLValueType(lvalue, assignee);
} }
TypePackId resultPack = checkPack(scope, assign->values, expectedTypes).tp; TypePackId resultPack = checkPack(scope, assign->values).tp;
addConstraint(scope, assign->location, UnpackConstraint{arena->addTypePack(std::move(assignees)), resultPack}); addConstraint(scope, assign->location, UnpackConstraint{arena->addTypePack(std::move(assignees)), resultPack, /*resultIsLValue*/ true});
return ControlFlow::None; return ControlFlow::None;
} }
@ -1545,8 +1528,7 @@ InferencePack ConstraintGenerator::checkPack(const ScopePtr& scope, AstExprCall*
scope->lvalueTypes[def] = resultTy; // TODO: typestates: track this as an assignment scope->lvalueTypes[def] = resultTy; // TODO: typestates: track this as an assignment
scope->rvalueRefinements[def] = resultTy; // TODO: typestates: track this as an assignment scope->rvalueRefinements[def] = resultTy; // TODO: typestates: track this as an assignment
if (auto it = inferredBindings.find(targetLocal->local); it != inferredBindings.end()) recordInferredBinding(targetLocal->local, resultTy);
it->second.types.insert(resultTy);
} }
return InferencePack{arena->addTypePack({resultTy}), {refinementArena.variadic(returnRefinements)}}; return InferencePack{arena->addTypePack({resultTy}), {refinementArena.variadic(returnRefinements)}};
@ -1723,8 +1705,8 @@ Inference ConstraintGenerator::check(const ScopePtr& scope, AstExprLocal* local)
if (maybeTy) if (maybeTy)
{ {
TypeId ty = follow(*maybeTy); TypeId ty = follow(*maybeTy);
if (auto it = inferredBindings.find(local->local); it != inferredBindings.end())
it->second.types.insert(ty); recordInferredBinding(local->local, ty);
return Inference{ty, refinementArena.proposition(key, builtinTypes->truthyType)}; return Inference{ty, refinementArena.proposition(key, builtinTypes->truthyType)};
} }
@ -2210,23 +2192,35 @@ std::optional<TypeId> ConstraintGenerator::checkLValue(const ScopePtr& scope, As
std::optional<TypeId> ConstraintGenerator::checkLValue(const ScopePtr& scope, AstExprLocal* local, TypeId assignedTy) std::optional<TypeId> ConstraintGenerator::checkLValue(const ScopePtr& scope, AstExprLocal* local, TypeId assignedTy)
{ {
/*
* The caller of this method uses the returned type to emit the proper
* SubtypeConstraint.
*
* At this point during constraint generation, the binding table is only
* populated by symbols that have type annotations.
*
* If this local has an interesting type annotation, it is important that we
* return that and constrain the assigned type.
*/
std::optional<TypeId> annotatedTy = scope->lookup(local->local); std::optional<TypeId> annotatedTy = scope->lookup(local->local);
LUAU_ASSERT(annotatedTy);
if (annotatedTy) if (annotatedTy)
addConstraint(scope, local->location, SubtypeConstraint{assignedTy, *annotatedTy}); addConstraint(scope, local->location, SubtypeConstraint{assignedTy, *annotatedTy});
else if (auto it = inferredBindings.find(local->local); it == inferredBindings.end())
ice->ice("Cannot find AstLocal* in either Scope::bindings or inferredBindings?");
return annotatedTy; const DefId defId = dfg->getDef(local);
std::optional<TypeId> ty = scope->lookupUnrefinedType(defId);
if (ty)
{
if (auto lt = getMutable<LocalType>(*ty))
++lt->blockCount;
}
else
{
ty = arena->addType(LocalType{builtinTypes->neverType, /* blockCount */ 1, local->local->name.value});
scope->lvalueTypes[defId] = *ty;
}
addConstraint(scope, local->location, UnpackConstraint{
arena->addTypePack({*ty}),
arena->addTypePack({assignedTy}),
/*resultIsLValue*/ true
});
recordInferredBinding(local->local, *ty);
return ty;
} }
std::optional<TypeId> ConstraintGenerator::checkLValue(const ScopePtr& scope, AstExprGlobal* global, TypeId assignedTy) std::optional<TypeId> ConstraintGenerator::checkLValue(const ScopePtr& scope, AstExprGlobal* global, TypeId assignedTy)
@ -2379,15 +2373,6 @@ TypeId ConstraintGenerator::updateProperty(const ScopePtr& scope, AstExpr* expr,
return assignedTy; return assignedTy;
} }
void ConstraintGenerator::updateLValueType(AstExpr* lvalue, TypeId ty)
{
if (auto local = lvalue->as<AstExprLocal>())
{
if (auto it = inferredBindings.find(local->local); it != inferredBindings.end())
it->second.types.insert(ty);
}
}
Inference ConstraintGenerator::check(const ScopePtr& scope, AstExprTable* expr, std::optional<TypeId> expectedType) Inference ConstraintGenerator::check(const ScopePtr& scope, AstExprTable* expr, std::optional<TypeId> expectedType)
{ {
const bool expectedTypeIsFree = expectedType && get<FreeType>(follow(*expectedType)); const bool expectedTypeIsFree = expectedType && get<FreeType>(follow(*expectedType));
@ -2611,13 +2596,7 @@ ConstraintGenerator::FunctionSignature ConstraintGenerator::checkFunctionSignatu
argTypes.push_back(argTy); argTypes.push_back(argTy);
argNames.emplace_back(FunctionArgument{local->name.value, local->location}); argNames.emplace_back(FunctionArgument{local->name.value, local->location});
if (local->annotation)
signatureScope->bindings[local] = Binding{argTy, local->location}; signatureScope->bindings[local] = Binding{argTy, local->location};
else
{
signatureScope->bindings[local] = Binding{builtinTypes->neverType, local->location};
inferredBindings[local] = {signatureScope.get(), {}};
}
DefId def = dfg->getDef(local); DefId def = dfg->getDef(local);
signatureScope->lvalueTypes[def] = argTy; signatureScope->lvalueTypes[def] = argTy;
@ -3125,6 +3104,12 @@ void ConstraintGenerator::prepopulateGlobalScope(const ScopePtr& globalScope, As
program->visit(&gp); program->visit(&gp);
} }
void ConstraintGenerator::recordInferredBinding(AstLocal* local, TypeId ty)
{
if (InferredBinding* ib = inferredBindings.find(local))
ib->types.insert(ty);
}
void ConstraintGenerator::fillInInferredBindings(const ScopePtr& globalScope, AstStatBlock* block) void ConstraintGenerator::fillInInferredBindings(const ScopePtr& globalScope, AstStatBlock* block)
{ {
for (const auto& [symbol, p] : inferredBindings) for (const auto& [symbol, p] : inferredBindings)

View File

@ -993,6 +993,27 @@ bool ConstraintSolver::tryDispatch(const FunctionCallConstraint& c, NotNull<cons
return block(c.fn, constraint); return block(c.fn, constraint);
} }
auto [argsHead, argsTail] = flatten(argsPack);
bool blocked = false;
for (TypeId t : argsHead)
{
if (isBlocked(t))
{
block(t, constraint);
blocked = true;
}
}
if (argsTail && isBlocked(*argsTail))
{
block(*argsTail, constraint);
blocked = true;
}
if (blocked)
return false;
auto collapse = [](const auto* t) -> std::optional<TypeId> { auto collapse = [](const auto* t) -> std::optional<TypeId> {
auto it = begin(t); auto it = begin(t);
auto endIt = end(t); auto endIt = end(t);
@ -1018,10 +1039,12 @@ bool ConstraintSolver::tryDispatch(const FunctionCallConstraint& c, NotNull<cons
// We don't support magic __call metamethods. // We don't support magic __call metamethods.
if (std::optional<TypeId> callMm = findMetatableEntry(builtinTypes, errors, fn, "__call", constraint->location)) if (std::optional<TypeId> callMm = findMetatableEntry(builtinTypes, errors, fn, "__call", constraint->location))
{ {
auto [head, tail] = flatten(c.argsPack); argsHead.insert(argsHead.begin(), fn);
head.insert(head.begin(), fn);
argsPack = arena->addTypePack(TypePack{std::move(head), tail}); if (argsTail && isBlocked(*argsTail))
return block(*argsTail, constraint);
argsPack = arena->addTypePack(TypePack{std::move(argsHead), argsTail});
fn = follow(*callMm); fn = follow(*callMm);
asMutable(c.result)->ty.emplace<FreeTypePack>(constraint->scope); asMutable(c.result)->ty.emplace<FreeTypePack>(constraint->scope);
} }
@ -1136,23 +1159,14 @@ bool ConstraintSolver::tryDispatch(const PrimitiveTypeConstraint& c, NotNull<con
bool ConstraintSolver::tryDispatch(const HasPropConstraint& c, NotNull<const Constraint> constraint) bool ConstraintSolver::tryDispatch(const HasPropConstraint& c, NotNull<const Constraint> constraint)
{ {
TypeId subjectType = follow(c.subjectType); const TypeId subjectType = follow(c.subjectType);
const TypeId resultType = follow(c.resultType);
LUAU_ASSERT(get<BlockedType>(c.resultType)); LUAU_ASSERT(get<BlockedType>(resultType));
if (isBlocked(subjectType) || get<PendingExpansionType>(subjectType)) if (isBlocked(subjectType) || get<PendingExpansionType>(subjectType))
return block(subjectType, constraint); return block(subjectType, constraint);
if (get<FreeType>(subjectType))
{
TableType& ttv = asMutable(subjectType)->ty.emplace<TableType>(TableState::Free, TypeLevel{}, constraint->scope);
ttv.props[c.prop] = Property{c.resultType};
TypeId res = freshType(arena, builtinTypes, constraint->scope);
asMutable(c.resultType)->ty.emplace<BoundType>(res);
unblock(c.resultType, constraint->location);
return true;
}
auto [blocked, result] = lookupTableProp(subjectType, c.prop, c.suppressSimplification); auto [blocked, result] = lookupTableProp(subjectType, c.prop, c.suppressSimplification);
if (!blocked.empty()) if (!blocked.empty())
{ {
@ -1162,8 +1176,8 @@ bool ConstraintSolver::tryDispatch(const HasPropConstraint& c, NotNull<const Con
return false; return false;
} }
bindBlockedType(c.resultType, result.value_or(builtinTypes->anyType), c.subjectType, constraint->location); bindBlockedType(resultType, result.value_or(builtinTypes->anyType), c.subjectType, constraint->location);
unblock(c.resultType, constraint->location); unblock(resultType, constraint->location);
return true; return true;
} }
@ -1438,32 +1452,57 @@ bool ConstraintSolver::tryDispatch(const UnpackConstraint& c, NotNull<const Cons
TypePack srcPack = extendTypePack(*arena, builtinTypes, sourcePack, size(resultPack)); TypePack srcPack = extendTypePack(*arena, builtinTypes, sourcePack, size(resultPack));
auto destIter = begin(resultPack); auto resultIter = begin(resultPack);
auto destEnd = end(resultPack); auto resultEnd = end(resultPack);
size_t i = 0; size_t i = 0;
while (destIter != destEnd) while (resultIter != resultEnd)
{ {
if (i >= srcPack.head.size()) if (i >= srcPack.head.size())
break; break;
TypeId srcTy = follow(srcPack.head[i]); TypeId srcTy = follow(srcPack.head[i]);
TypeId resultTy = follow(*resultIter);
if (isBlocked(*destIter)) if (resultTy)
{ {
if (follow(srcTy) == *destIter) if (auto lt = getMutable<LocalType>(resultTy); c.resultIsLValue && lt)
{ {
// Cyclic type dependency. (????) lt->domain = simplifyUnion(builtinTypes, arena, lt->domain, srcTy).result;
LUAU_ASSERT(lt->blockCount > 0);
--lt->blockCount;
LUAU_ASSERT(0 <= lt->blockCount);
if (0 == lt->blockCount)
asMutable(resultTy)->ty.emplace<BoundType>(lt->domain);
}
else if (get<BlockedType>(resultTy))
{
if (follow(srcTy) == resultTy)
{
// It is sometimes the case that we find that a blocked type
// is only blocked on itself. This doesn't actually
// constitute any meaningful constraint, so we replace it
// with a free type.
TypeId f = freshType(arena, builtinTypes, constraint->scope); TypeId f = freshType(arena, builtinTypes, constraint->scope);
asMutable(*destIter)->ty.emplace<BoundType>(f); asMutable(resultTy)->ty.emplace<BoundType>(f);
} }
else else
asMutable(*destIter)->ty.emplace<BoundType>(srcTy); asMutable(resultTy)->ty.emplace<BoundType>(srcTy);
unblock(*destIter, constraint->location);
} }
else else
unify(constraint->scope, constraint->location, *destIter, srcTy); {
LUAU_ASSERT(c.resultIsLValue);
unify(constraint->scope, constraint->location, resultTy, srcTy);
}
++destIter; unblock(resultTy, constraint->location);
}
else
unify(constraint->scope, constraint->location, resultTy, srcTy);
++resultIter;
++i; ++i;
} }
@ -1471,15 +1510,25 @@ bool ConstraintSolver::tryDispatch(const UnpackConstraint& c, NotNull<const Cons
// sourcePack is long enough to fill every value. Replace every remaining // sourcePack is long enough to fill every value. Replace every remaining
// result TypeId with `nil`. // result TypeId with `nil`.
while (destIter != destEnd) while (resultIter != resultEnd)
{ {
if (isBlocked(*destIter)) TypeId resultTy = follow(*resultIter);
if (auto lt = getMutable<LocalType>(resultTy); c.resultIsLValue && lt)
{ {
asMutable(*destIter)->ty.emplace<BoundType>(builtinTypes->nilType); lt->domain = simplifyUnion(builtinTypes, arena, lt->domain, builtinTypes->nilType).result;
unblock(*destIter, constraint->location); LUAU_ASSERT(0 <= lt->blockCount);
--lt->blockCount;
if (0 == lt->blockCount)
asMutable(resultTy)->ty.emplace<BoundType>(lt->domain);
}
else if (get<BlockedType>(*resultIter) || get<PendingExpansionType>(*resultIter))
{
asMutable(*resultIter)->ty.emplace<BoundType>(builtinTypes->nilType);
unblock(*resultIter, constraint->location);
} }
++destIter; ++resultIter;
} }
return true; return true;
@ -1999,14 +2048,23 @@ std::pair<std::vector<TypeId>, std::optional<TypeId>> ConstraintSolver::lookupTa
} }
else if (auto ft = get<FreeType>(subjectType)) else if (auto ft = get<FreeType>(subjectType))
{ {
Scope* scope = ft->scope; const TypeId upperBound = follow(ft->upperBound);
TableType* tt = &asMutable(subjectType)->ty.emplace<TableType>(); if (get<TableType>(upperBound))
tt->state = TableState::Free; return lookupTableProp(upperBound, propName, suppressSimplification, seen);
tt->scope = scope;
// TODO: The upper bound could be an intersection that contains suitable tables or classes.
NotNull<Scope> scope{ft->scope};
const TypeId newUpperBound = arena->addType(TableType{TableState::Free, TypeLevel{}, scope});
TableType* tt = getMutable<TableType>(newUpperBound);
LUAU_ASSERT(tt);
TypeId propType = freshType(arena, builtinTypes, scope); TypeId propType = freshType(arena, builtinTypes, scope);
tt->props[propName] = Property{propType}; tt->props[propName] = Property{propType};
unify(scope, Location{}, subjectType, newUpperBound);
return {{}, propType}; return {{}, propType};
} }
else if (auto utv = get<UnionType>(subjectType)) else if (auto utv = get<UnionType>(subjectType))
@ -2298,7 +2356,12 @@ void ConstraintSolver::unblock(const std::vector<TypePackId>& packs, Location lo
bool ConstraintSolver::isBlocked(TypeId ty) bool ConstraintSolver::isBlocked(TypeId ty)
{ {
return nullptr != get<BlockedType>(follow(ty)) || nullptr != get<PendingExpansionType>(follow(ty)); ty = follow(ty);
if (auto lt = get<LocalType>(ty))
return lt->blockCount > 0;
return nullptr != get<BlockedType>(ty) || nullptr != get<PendingExpansionType>(ty);
} }
bool ConstraintSolver::isBlocked(TypePackId tp) bool ConstraintSolver::isBlocked(TypePackId tp)

View File

@ -161,23 +161,107 @@ DfgScope* DataFlowGraphBuilder::childScope(DfgScope* scope, bool isLoopScope)
void DataFlowGraphBuilder::join(DfgScope* p, DfgScope* a, DfgScope* b) void DataFlowGraphBuilder::join(DfgScope* p, DfgScope* a, DfgScope* b)
{ {
// TODO TODO FIXME IMPLEMENT JOIN LOGIC FOR PROPERTIES joinBindings(p->bindings, a->bindings, b->bindings);
joinProps(p->props, a->props, b->props);
for (const auto& [sym, def1] : a->bindings)
{
if (auto def2 = b->bindings.find(sym))
p->bindings[sym] = defArena->phi(NotNull{def1}, NotNull{*def2});
else if (auto def2 = p->bindings.find(sym))
p->bindings[sym] = defArena->phi(NotNull{def1}, NotNull{*def2});
} }
for (const auto& [sym, def1] : b->bindings) void DataFlowGraphBuilder::joinBindings(DfgScope::Bindings& p, const DfgScope::Bindings& a, const DfgScope::Bindings& b)
{ {
if (a->bindings.find(sym)) for (const auto& [sym, def1] : a)
{
if (auto def2 = b.find(sym))
p[sym] = defArena->phi(NotNull{def1}, NotNull{*def2});
else if (auto def2 = p.find(sym))
p[sym] = defArena->phi(NotNull{def1}, NotNull{*def2});
}
for (const auto& [sym, def1] : b)
{
if (auto def2 = p.find(sym))
p[sym] = defArena->phi(NotNull{def1}, NotNull{*def2});
}
}
void DataFlowGraphBuilder::joinProps(DfgScope::Props& p, const DfgScope::Props& a, const DfgScope::Props& b)
{
auto phinodify = [this](auto& p, const auto& a, const auto& b) mutable {
for (const auto& [k, defA] : a)
{
if (auto it = b.find(k); it != b.end())
p[k] = defArena->phi(NotNull{it->second}, NotNull{defA});
else if (auto it = p.find(k); it != p.end())
p[k] = defArena->phi(NotNull{it->second}, NotNull{defA});
else
p[k] = defA;
}
for (const auto& [k, defB] : b)
{
if (auto it = a.find(k); it != a.end())
continue; continue;
else if (auto def2 = p->bindings.find(sym)) else if (auto it = p.find(k); it != p.end())
p->bindings[sym] = defArena->phi(NotNull{def1}, NotNull{*def2}); p[k] = defArena->phi(NotNull{it->second}, NotNull{defB});
else
p[k] = defB;
} }
};
for (const auto& [def, a1] : a)
{
p.try_insert(def, {});
if (auto a2 = b.find(def))
phinodify(p[def], a1, *a2);
else if (auto a2 = p.find(def))
phinodify(p[def], a1, *a2);
}
for (const auto& [def, a1] : b)
{
p.try_insert(def, {});
if (a.find(def))
continue;
else if (auto a2 = p.find(def))
phinodify(p[def], a1, *a2);
}
}
DefId DataFlowGraphBuilder::lookup(DfgScope* scope, Symbol symbol)
{
if (auto found = scope->lookup(symbol))
return *found;
else
{
DefId result = defArena->freshCell();
if (symbol.local)
scope->bindings[symbol] = result;
else
moduleScope->bindings[symbol] = result;
return result;
}
}
DefId DataFlowGraphBuilder::lookup(DfgScope* scope, DefId def, const std::string& key)
{
if (auto found = scope->lookup(def, key))
return *found;
else if (auto phi = get<Phi>(def))
{
std::vector<DefId> defs;
for (DefId operand : phi->operands)
defs.push_back(lookup(scope, operand, key));
DefId result = defArena->phi(defs);
scope->props[def][key] = result;
return result;
}
else if (get<Cell>(def))
{
DefId result = defArena->freshCell();
scope->props[def][key] = result;
return result;
}
else
handle->ice("Inexhaustive lookup cases in DataFlowGraphBuilder::lookup");
} }
ControlFlow DataFlowGraphBuilder::visit(DfgScope* scope, AstStatBlock* b) ControlFlow DataFlowGraphBuilder::visit(DfgScope* scope, AstStatBlock* b)
@ -585,6 +669,7 @@ DataFlowResult DataFlowGraphBuilder::visitExpr(DfgScope* scope, AstExprGroup* gr
DataFlowResult DataFlowGraphBuilder::visitExpr(DfgScope* scope, AstExprLocal* l) DataFlowResult DataFlowGraphBuilder::visitExpr(DfgScope* scope, AstExprLocal* l)
{ {
// DfgScope::lookup is intentional here: we want to be able to ice.
if (auto def = scope->lookup(l->local)) if (auto def = scope->lookup(l->local))
{ {
const RefinementKey* key = keyArena->leaf(*def); const RefinementKey* key = keyArena->leaf(*def);
@ -596,11 +681,7 @@ DataFlowResult DataFlowGraphBuilder::visitExpr(DfgScope* scope, AstExprLocal* l)
DataFlowResult DataFlowGraphBuilder::visitExpr(DfgScope* scope, AstExprGlobal* g) DataFlowResult DataFlowGraphBuilder::visitExpr(DfgScope* scope, AstExprGlobal* g)
{ {
if (auto def = scope->lookup(g->name)) DefId def = lookup(scope, g->name);
return {*def, keyArena->leaf(*def)};
DefId def = defArena->freshCell();
moduleScope->bindings[g->name] = def;
return {def, keyArena->leaf(def)}; return {def, keyArena->leaf(def)};
} }
@ -619,15 +700,10 @@ DataFlowResult DataFlowGraphBuilder::visitExpr(DfgScope* scope, AstExprIndexName
auto [parentDef, parentKey] = visitExpr(scope, i->expr); auto [parentDef, parentKey] = visitExpr(scope, i->expr);
std::string index = i->index.value; std::string index = i->index.value;
if (auto propDef = scope->lookup(parentDef, index))
return {*propDef, keyArena->node(parentKey, *propDef, index)}; DefId def = lookup(scope, parentDef, index);
else
{
DefId def = defArena->freshCell();
scope->props[parentDef][index] = def;
return {def, keyArena->node(parentKey, def, index)}; return {def, keyArena->node(parentKey, def, index)};
} }
}
DataFlowResult DataFlowGraphBuilder::visitExpr(DfgScope* scope, AstExprIndexExpr* i) DataFlowResult DataFlowGraphBuilder::visitExpr(DfgScope* scope, AstExprIndexExpr* i)
{ {
@ -637,15 +713,10 @@ DataFlowResult DataFlowGraphBuilder::visitExpr(DfgScope* scope, AstExprIndexExpr
if (auto string = i->index->as<AstExprConstantString>()) if (auto string = i->index->as<AstExprConstantString>())
{ {
std::string index{string->value.data, string->value.size}; std::string index{string->value.data, string->value.size};
if (auto propDef = scope->lookup(parentDef, index))
return {*propDef, keyArena->node(parentKey, *propDef, index)}; DefId def = lookup(scope, parentDef, index);
else
{
DefId def = defArena->freshCell();
scope->props[parentDef][index] = def;
return {def, keyArena->node(parentKey, def, index)}; return {def, keyArena->node(parentKey, def, index)};
} }
}
return {defArena->freshCell(/* subscripted= */true), nullptr}; return {defArena->freshCell(/* subscripted= */true), nullptr};
} }
@ -795,8 +866,8 @@ void DataFlowGraphBuilder::visitLValue(DfgScope* scope, AstExprGlobal* g, DefId
// We need to keep the previous def around for a compound assignment. // We need to keep the previous def around for a compound assignment.
if (isCompoundAssignment) if (isCompoundAssignment)
{ {
if (auto def = scope->lookup(g->name)) DefId def = lookup(scope, g->name);
graph.compoundAssignDefs[g] = *def; graph.compoundAssignDefs[g] = def;
} }
// In order to avoid alias tracking, we need to clip the reference to the parent def. // In order to avoid alias tracking, we need to clip the reference to the parent def.

View File

@ -1,8 +1,9 @@
// This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details // This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details
#include "Luau/Def.h" #include "Luau/Def.h"
#include "Luau/Common.h"
#include "Luau/DenseHash.h"
#include "Luau/Common.h"
#include <algorithm>
#include <deque> #include <deque>
namespace Luau namespace Luau
@ -13,26 +14,8 @@ bool containsSubscriptedDefinition(DefId def)
if (auto cell = get<Cell>(def)) if (auto cell = get<Cell>(def))
return cell->subscripted; return cell->subscripted;
else if (auto phi = get<Phi>(def)) else if (auto phi = get<Phi>(def))
{ return std::any_of(phi->operands.begin(), phi->operands.end(), containsSubscriptedDefinition);
std::deque<DefId> queue(begin(phi->operands), end(phi->operands)); else
DenseHashSet<const Def*> seen{nullptr};
while (!queue.empty())
{
DefId next = queue.front();
queue.pop_front();
LUAU_ASSERT(!seen.find(next));
if (seen.find(next))
continue;
seen.insert(next);
if (auto cell_ = get<Cell>(next); cell_ && cell_->subscripted)
return true;
else if (auto phi_ = get<Phi>(next))
queue.insert(queue.end(), phi_->operands.begin(), phi_->operands.end());
}
}
return false; return false;
} }
@ -41,12 +24,35 @@ DefId DefArena::freshCell(bool subscripted)
return NotNull{allocator.allocate(Def{Cell{subscripted}})}; return NotNull{allocator.allocate(Def{Cell{subscripted}})};
} }
static void collectOperands(DefId def, std::vector<DefId>& operands)
{
if (std::find(operands.begin(), operands.end(), def) != operands.end())
return;
else if (get<Cell>(def))
operands.push_back(def);
else if (auto phi = get<Phi>(def))
{
for (const Def* operand : phi->operands)
collectOperands(NotNull{operand}, operands);
}
}
DefId DefArena::phi(DefId a, DefId b) DefId DefArena::phi(DefId a, DefId b)
{ {
if (a == b) return phi({a, b});
return a; }
DefId DefArena::phi(const std::vector<DefId>& defs)
{
std::vector<DefId> operands;
for (DefId operand : defs)
collectOperands(operand, operands);
// There's no need to allocate a Phi node for a singleton set.
if (operands.size() == 1)
return operands[0];
else else
return NotNull{allocator.allocate(Def{Phi{{a, b}}})}; return NotNull{allocator.allocate(Def{Phi{std::move(operands)}})};
} }
} // namespace Luau } // namespace Luau

View File

@ -84,7 +84,7 @@ struct NonStrictContext
for (auto [def, rightTy] : right.context) for (auto [def, rightTy] : right.context)
{ {
if (!right.find(def).has_value()) if (!left.find(def).has_value())
disj.context[def] = rightTy; disj.context[def] = rightTy;
} }
@ -270,18 +270,24 @@ struct NonStrictTypeChecker
NonStrictContext visit(AstStatBlock* block) NonStrictContext visit(AstStatBlock* block)
{ {
auto StackPusher = pushStack(block); auto StackPusher = pushStack(block);
NonStrictContext ctx;
for (AstStat* statement : block->body) for (AstStat* statement : block->body)
visit(statement); ctx = NonStrictContext::disjunction(builtinTypes, NotNull{&arena}, ctx, visit(statement));
return {}; return ctx;
} }
NonStrictContext visit(AstStatIf* ifStatement) NonStrictContext visit(AstStatIf* ifStatement)
{ {
NonStrictContext condB = visit(ifStatement->condition); NonStrictContext condB = visit(ifStatement->condition);
NonStrictContext thenB = visit(ifStatement->thenbody); NonStrictContext branchContext;
NonStrictContext elseB = visit(ifStatement->elsebody); // If there is no else branch, don't bother generating warnings for the then branch - we can't prove there is an error
return NonStrictContext::disjunction( if (ifStatement->elsebody)
builtinTypes, NotNull{&arena}, condB, NonStrictContext::conjunction(builtinTypes, NotNull{&arena}, thenB, elseB)); {
NonStrictContext thenBody = visit(ifStatement->thenbody);
NonStrictContext elseBody = visit(ifStatement->elsebody);
branchContext = NonStrictContext::conjunction(builtinTypes, NotNull{&arena}, thenBody, elseBody);
}
return NonStrictContext::disjunction(builtinTypes, NotNull{&arena}, condB, branchContext);
} }
NonStrictContext visit(AstStatWhile* whileStatement) NonStrictContext visit(AstStatWhile* whileStatement)
@ -316,6 +322,8 @@ struct NonStrictTypeChecker
NonStrictContext visit(AstStatLocal* local) NonStrictContext visit(AstStatLocal* local)
{ {
for (AstExpr* rhs : local->values)
visit(rhs);
return {}; return {};
} }
@ -341,12 +349,12 @@ struct NonStrictTypeChecker
NonStrictContext visit(AstStatFunction* statFn) NonStrictContext visit(AstStatFunction* statFn)
{ {
return {}; return visit(statFn->func);
} }
NonStrictContext visit(AstStatLocalFunction* localFn) NonStrictContext visit(AstStatLocalFunction* localFn)
{ {
return {}; return visit(localFn->func);
} }
NonStrictContext visit(AstStatTypeAlias* typeAlias) NonStrictContext visit(AstStatTypeAlias* typeAlias)
@ -530,7 +538,7 @@ struct NonStrictTypeChecker
NonStrictContext visit(AstExprFunction* exprFn) NonStrictContext visit(AstExprFunction* exprFn)
{ {
auto pusher = pushStack(exprFn); auto pusher = pushStack(exprFn);
return {}; return visit(exprFn->body);
} }
NonStrictContext visit(AstExprTable* table) NonStrictContext visit(AstExprTable* table)
@ -589,10 +597,6 @@ struct NonStrictTypeChecker
SubtypingResult r = subtyping.isSubtype(actualType, *contextTy); SubtypingResult r = subtyping.isSubtype(actualType, *contextTy);
if (r.normalizationTooComplex) if (r.normalizationTooComplex)
reportError(NormalizationTooComplex{}, fragment->location); reportError(NormalizationTooComplex{}, fragment->location);
if (!r.isSubtype && !r.isErrorSuppressing)
reportError(TypeMismatch{actualType, *contextTy}, fragment->location);
if (r.isSubtype) if (r.isSubtype)
return {actualType}; return {actualType};
} }

View File

@ -1623,6 +1623,12 @@ bool Normalizer::unionNormalWithTy(NormalizedType& here, TypeId there, Set<TypeI
inter.tops = builtinTypes->unknownType; inter.tops = builtinTypes->unknownType;
here.tyvars.insert_or_assign(there, std::make_unique<NormalizedType>(std::move(inter))); here.tyvars.insert_or_assign(there, std::make_unique<NormalizedType>(std::move(inter)));
} }
else if (auto lt = get<LocalType>(there))
{
// FIXME? This is somewhat questionable.
// Maybe we should assert because this should never happen?
unionNormalWithTy(here, lt->domain, seenSetTypes, ignoreSmallerTyvars);
}
else if (get<FunctionType>(there)) else if (get<FunctionType>(there))
unionFunctionsWithFunction(here.functions, there); unionFunctionsWithFunction(here.functions, there);
else if (get<TableType>(there) || get<MetatableType>(there)) else if (get<TableType>(there) || get<MetatableType>(there))

View File

@ -72,6 +72,17 @@ std::optional<std::pair<Binding*, Scope*>> Scope::lookupEx(Symbol sym)
} }
} }
std::optional<TypeId> Scope::lookupUnrefinedType(DefId def) const
{
for (const Scope* current = this; current; current = current->parent.get())
{
if (auto ty = current->lvalueTypes.find(def))
return *ty;
}
return std::nullopt;
}
std::optional<TypeId> Scope::lookup(DefId def) const std::optional<TypeId> Scope::lookup(DefId def) const
{ {
for (const Scope* current = this; current; current = current->parent.get()) for (const Scope* current = this; current; current = current->parent.get())

View File

@ -19,8 +19,12 @@ static TypeId shallowClone(TypeId ty, TypeArena& dest, const TxnLog* log, bool a
auto go = [ty, &dest, alwaysClone](auto&& a) { auto go = [ty, &dest, alwaysClone](auto&& a) {
using T = std::decay_t<decltype(a)>; using T = std::decay_t<decltype(a)>;
// The pointer identities of free and local types is very important.
// We decline to copy them.
if constexpr (std::is_same_v<T, FreeType>) if constexpr (std::is_same_v<T, FreeType>)
return ty; return ty;
else if constexpr (std::is_same_v<T, LocalType>)
return ty;
else if constexpr (std::is_same_v<T, BoundType>) else if constexpr (std::is_same_v<T, BoundType>)
{ {
// This should never happen, but visit() cannot see it. // This should never happen, but visit() cannot see it.

View File

@ -47,12 +47,12 @@ struct VarianceFlipper
bool SubtypingReasoning::operator==(const SubtypingReasoning& other) const bool SubtypingReasoning::operator==(const SubtypingReasoning& other) const
{ {
return subPath == other.subPath && superPath == other.superPath; return subPath == other.subPath && superPath == other.superPath && variance == other.variance;
} }
size_t SubtypingReasoningHash::operator()(const SubtypingReasoning& r) const size_t SubtypingReasoningHash::operator()(const SubtypingReasoning& r) const
{ {
return TypePath::PathHash()(r.subPath) ^ (TypePath::PathHash()(r.superPath) << 1); return TypePath::PathHash()(r.subPath) ^ (TypePath::PathHash()(r.superPath) << 1) ^ (static_cast<size_t>(r.variance) << 1);
} }
SubtypingResult& SubtypingResult::andAlso(const SubtypingResult& other) SubtypingResult& SubtypingResult::andAlso(const SubtypingResult& other)
@ -162,6 +162,19 @@ SubtypingResult& SubtypingResult::withSuperPath(TypePath::Path path)
return *this; return *this;
} }
SubtypingResult& SubtypingResult::withVariance(SubtypingVariance variance)
{
if (reasoning.empty())
reasoning.insert(SubtypingReasoning{TypePath::kEmpty, TypePath::kEmpty, variance});
else
{
for (auto& r : reasoning)
r.variance = variance;
}
return *this;
}
SubtypingResult SubtypingResult::negate(const SubtypingResult& result) SubtypingResult SubtypingResult::negate(const SubtypingResult& result)
{ {
return SubtypingResult{ return SubtypingResult{
@ -671,7 +684,7 @@ SubtypingResult Subtyping::isContravariantWith(SubtypingEnvironment& env, SubTy&
template<typename SubTy, typename SuperTy> template<typename SubTy, typename SuperTy>
SubtypingResult Subtyping::isInvariantWith(SubtypingEnvironment& env, SubTy&& subTy, SuperTy&& superTy) SubtypingResult Subtyping::isInvariantWith(SubtypingEnvironment& env, SubTy&& subTy, SuperTy&& superTy)
{ {
return isCovariantWith(env, subTy, superTy).andAlso(isContravariantWith(env, subTy, superTy)); return isCovariantWith(env, subTy, superTy).andAlso(isContravariantWith(env, subTy, superTy)).withVariance(SubtypingVariance::Invariant);
} }
template<typename SubTy, typename SuperTy> template<typename SubTy, typename SuperTy>
@ -689,7 +702,7 @@ SubtypingResult Subtyping::isContravariantWith(SubtypingEnvironment& env, const
template<typename SubTy, typename SuperTy> template<typename SubTy, typename SuperTy>
SubtypingResult Subtyping::isInvariantWith(SubtypingEnvironment& env, const TryPair<const SubTy*, const SuperTy*>& pair) SubtypingResult Subtyping::isInvariantWith(SubtypingEnvironment& env, const TryPair<const SubTy*, const SuperTy*>& pair)
{ {
return isCovariantWith(env, pair).andAlso(isContravariantWith(pair)); return isCovariantWith(env, pair).andAlso(isContravariantWith(pair)).withVariance(SubtypingVariance::Invariant);
} }
/* /*
@ -1009,7 +1022,7 @@ SubtypingResult Subtyping::isCovariantWith(SubtypingEnvironment& env, const Meta
SubtypingResult Subtyping::isCovariantWith(SubtypingEnvironment& env, const MetatableType* subMt, const TableType* superTable) SubtypingResult Subtyping::isCovariantWith(SubtypingEnvironment& env, const MetatableType* subMt, const TableType* superTable)
{ {
if (auto subTable = get<TableType>(subMt->table)) if (auto subTable = get<TableType>(follow(subMt->table)))
{ {
// Metatables cannot erase properties from the table they're attached to, so // Metatables cannot erase properties from the table they're attached to, so
// the subtyping rule for this is just if the table component is a subtype // the subtyping rule for this is just if the table component is a subtype

View File

@ -261,6 +261,14 @@ void StateDot::visitChildren(TypeId ty, int index)
visitChild(t.upperBound, index, "[upperBound]"); visitChild(t.upperBound, index, "[upperBound]");
} }
} }
else if constexpr (std::is_same_v<T, LocalType>)
{
formatAppend(result, "LocalType");
finishNodeLabel(ty);
finishNode();
visitChild(t.domain, 1, "[domain]");
}
else if constexpr (std::is_same_v<T, AnyType>) else if constexpr (std::is_same_v<T, AnyType>)
{ {
formatAppend(result, "AnyType %d", index); formatAppend(result, "AnyType %d", index);

View File

@ -100,6 +100,16 @@ struct FindCyclicTypes final : TypeVisitor
return false; return false;
} }
bool visit(TypeId ty, const LocalType& lt) override
{
if (!visited.insert(ty).second)
return false;
traverse(lt.domain);
return false;
}
bool visit(TypeId ty, const TableType& ttv) override bool visit(TypeId ty, const TableType& ttv) override
{ {
if (!visited.insert(ty).second) if (!visited.insert(ty).second)
@ -500,6 +510,15 @@ struct TypeStringifier
} }
} }
void operator()(TypeId ty, const LocalType& lt)
{
state.emit("l-");
state.emit(lt.name);
state.emit("=[");
stringify(lt.domain);
state.emit("]");
}
void operator()(TypeId, const BoundType& btv) void operator()(TypeId, const BoundType& btv)
{ {
stringify(btv.boundTo); stringify(btv.boundTo);

View File

@ -329,10 +329,14 @@ public:
{ {
return Luau::visit(*this, bound.boundTo->ty); return Luau::visit(*this, bound.boundTo->ty);
} }
AstType* operator()(const FreeType& ftv) AstType* operator()(const FreeType& ft)
{ {
return allocator->alloc<AstTypeReference>(Location(), std::nullopt, AstName("free"), std::nullopt, Location()); return allocator->alloc<AstTypeReference>(Location(), std::nullopt, AstName("free"), std::nullopt, Location());
} }
AstType* operator()(const LocalType& lt)
{
return Luau::visit(*this, lt.domain->ty);
}
AstType* operator()(const UnionType& uv) AstType* operator()(const UnionType& uv)
{ {
AstArray<AstType*> unionTypes; AstArray<AstType*> unionTypes;

View File

@ -2464,13 +2464,16 @@ struct TypeChecker2
if (!get2<TypeId, TypeId>(*subLeaf, *superLeaf) && !get2<TypePackId, TypePackId>(*subLeaf, *superLeaf)) if (!get2<TypeId, TypeId>(*subLeaf, *superLeaf) && !get2<TypePackId, TypePackId>(*subLeaf, *superLeaf))
ice->ice("Subtyping test returned a reasoning where one path ends at a type and the other ends at a pack.", location); ice->ice("Subtyping test returned a reasoning where one path ends at a type and the other ends at a pack.", location);
std::string relation = "a subtype of";
if (reasoning.variance == SubtypingVariance::Invariant)
relation = "exactly";
std::string reason; std::string reason;
if (reasoning.subPath == reasoning.superPath) if (reasoning.subPath == reasoning.superPath)
reason = "at " + toString(reasoning.subPath) + ", " + toString(*subLeaf) + " is not a subtype of " + toString(*superLeaf); reason = "at " + toString(reasoning.subPath) + ", " + toString(*subLeaf) + " is not " + relation + " " + toString(*superLeaf);
else else
reason = "type " + toString(subTy) + toString(reasoning.subPath, /* prefixDot */ true) + " (" + toString(*subLeaf) + reason = "type " + toString(subTy) + toString(reasoning.subPath, /* prefixDot */ true) + " (" + toString(*subLeaf) + ") is not " +
") is not a subtype of " + toString(superTy) + toString(reasoning.superPath, /* prefixDot */ true) + " (" + relation + " " + toString(superTy) + toString(reasoning.superPath, /* prefixDot */ true) + " (" + toString(*superLeaf) + ")";
toString(*superLeaf) + ")";
reasons.push_back(reason); reasons.push_back(reason);
} }

295
CLI/Bytecode.cpp Normal file
View File

@ -0,0 +1,295 @@
// This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details
#include "lua.h"
#include "lualib.h"
#include "Luau/CodeGen.h"
#include "Luau/Compiler.h"
#include "Luau/BytecodeBuilder.h"
#include "Luau/Parser.h"
#include "Luau/BytecodeSummary.h"
#include "FileUtils.h"
#include "Flags.h"
#include <memory>
using Luau::CodeGen::FunctionBytecodeSummary;
struct GlobalOptions
{
int optimizationLevel = 1;
int debugLevel = 1;
} globalOptions;
static Luau::CompileOptions copts()
{
Luau::CompileOptions result = {};
result.optimizationLevel = globalOptions.optimizationLevel;
result.debugLevel = globalOptions.debugLevel;
return result;
}
static void displayHelp(const char* argv0)
{
printf("Usage: %s [options] [file list]\n", argv0);
printf("\n");
printf("Available options:\n");
printf(" -h, --help: Display this usage message.\n");
printf(" -O<n>: compile with optimization level n (default 1, n should be between 0 and 2).\n");
printf(" -g<n>: compile with debug level n (default 1, n should be between 0 and 2).\n");
printf(" --fflags=<fflags>: flags to be enabled.\n");
printf(" --summary-file=<filename>: file in which bytecode analysis summary will be recorded (default 'bytecode-summary.json').\n");
exit(0);
}
static bool parseArgs(int argc, char** argv, std::string& summaryFile)
{
for (int i = 1; i < argc; i++)
{
if (strcmp(argv[i], "-h") == 0 || strcmp(argv[i], "--help") == 0)
{
displayHelp(argv[0]);
}
else if (strncmp(argv[i], "-O", 2) == 0)
{
int level = atoi(argv[i] + 2);
if (level < 0 || level > 2)
{
fprintf(stderr, "Error: Optimization level must be between 0 and 2 inclusive.\n");
return false;
}
globalOptions.optimizationLevel = level;
}
else if (strncmp(argv[i], "-g", 2) == 0)
{
int level = atoi(argv[i] + 2);
if (level < 0 || level > 2)
{
fprintf(stderr, "Error: Debug level must be between 0 and 2 inclusive.\n");
return false;
}
globalOptions.debugLevel = level;
}
else if (strncmp(argv[i], "--summary-file=", 15) == 0)
{
summaryFile = argv[i] + 15;
if (summaryFile.size() == 0)
{
fprintf(stderr, "Error: filename missing for '--summary-file'.\n\n");
return false;
}
}
else if (strncmp(argv[i], "--fflags=", 9) == 0)
{
setLuauFlags(argv[i] + 9);
}
else if (argv[i][0] == '-')
{
fprintf(stderr, "Error: Unrecognized option '%s'.\n\n", argv[i]);
displayHelp(argv[0]);
}
}
return true;
}
static void report(const char* name, const Luau::Location& location, const char* type, const char* message)
{
fprintf(stderr, "%s(%d,%d): %s: %s\n", name, location.begin.line + 1, location.begin.column + 1, type, message);
}
static void reportError(const char* name, const Luau::ParseError& error)
{
report(name, error.getLocation(), "SyntaxError", error.what());
}
static void reportError(const char* name, const Luau::CompileError& error)
{
report(name, error.getLocation(), "CompileError", error.what());
}
static bool analyzeFile(const char* name, const unsigned nestingLimit, std::vector<FunctionBytecodeSummary>& summaries)
{
std::optional<std::string> source = readFile(name);
if (!source)
{
fprintf(stderr, "Error opening %s\n", name);
return false;
}
try
{
Luau::BytecodeBuilder bcb;
compileOrThrow(bcb, source.value(), copts());
const std::string& bytecode = bcb.getBytecode();
std::unique_ptr<lua_State, void (*)(lua_State*)> globalState(luaL_newstate(), lua_close);
lua_State* L = globalState.get();
if (luau_load(L, name, bytecode.data(), bytecode.size(), 0) == 0)
{
summaries = Luau::CodeGen::summarizeBytecode(L, -1, nestingLimit);
return true;
}
else
{
fprintf(stderr, "Error loading bytecode %s\n", name);
return false;
}
}
catch (Luau::ParseErrors& e)
{
for (auto& error : e.getErrors())
reportError(name, error);
return false;
}
catch (Luau::CompileError& e)
{
reportError(name, e);
return false;
}
return true;
}
static std::string escapeFilename(const std::string& filename)
{
std::string escaped;
escaped.reserve(filename.size());
for (const char ch : filename)
{
switch (ch)
{
case '\\':
escaped.push_back('/');
break;
case '"':
escaped.push_back('\\');
escaped.push_back(ch);
break;
default:
escaped.push_back(ch);
}
}
return escaped;
}
static void serializeFunctionSummary(const FunctionBytecodeSummary& summary, FILE* fp)
{
const unsigned nestingLimit = summary.getNestingLimit();
const unsigned opLimit = summary.getOpLimit();
fprintf(fp, " {\n");
fprintf(fp, " \"source\": \"%s\",\n", summary.getSource().c_str());
fprintf(fp, " \"name\": \"%s\",\n", summary.getName().c_str());
fprintf(fp, " \"line\": %d,\n", summary.getLine());
fprintf(fp, " \"nestingLimit\": %u,\n", nestingLimit);
fprintf(fp, " \"counts\": [");
for (unsigned nesting = 0; nesting <= nestingLimit; ++nesting)
{
fprintf(fp, "\n [");
for (unsigned i = 0; i < opLimit; ++i)
{
fprintf(fp, "%d", summary.getCount(nesting, uint8_t(i)));
if (i < opLimit - 1)
fprintf(fp, ", ");
}
fprintf(fp, "]");
if (nesting < nestingLimit)
fprintf(fp, ",");
}
fprintf(fp, "\n ]");
fprintf(fp, "\n }");
}
static void serializeScriptSummary(const std::string& file, const std::vector<FunctionBytecodeSummary>& scriptSummary, FILE* fp)
{
std::string escaped(escapeFilename(file));
const size_t functionCount = scriptSummary.size();
fprintf(fp, " \"%s\": [\n", escaped.c_str());
for (size_t i = 0; i < functionCount; ++i)
{
serializeFunctionSummary(scriptSummary[i], fp);
fprintf(fp, i == (functionCount - 1) ? "\n" : ",\n");
}
fprintf(fp, " ]");
}
static bool serializeSummaries(
const std::vector<std::string>& files, const std::vector<std::vector<FunctionBytecodeSummary>>& scriptSummaries, const std::string& summaryFile)
{
FILE* fp = fopen(summaryFile.c_str(), "w");
const size_t fileCount = files.size();
if (!fp)
{
fprintf(stderr, "Unable to open '%s'.\n", summaryFile.c_str());
return false;
}
fprintf(fp, "{\n");
for (size_t i = 0; i < fileCount; ++i)
{
serializeScriptSummary(files[i], scriptSummaries[i], fp);
fprintf(fp, i < (fileCount - 1) ? ",\n" : "\n");
}
fprintf(fp, "}");
fclose(fp);
return true;
}
static int assertionHandler(const char* expr, const char* file, int line, const char* function)
{
printf("%s(%d): ASSERTION FAILED: %s\n", file, line, expr);
return 1;
}
int main(int argc, char** argv)
{
Luau::assertHandler() = assertionHandler;
setLuauFlagsDefault();
std::string summaryFile("bytecode-summary.json");
unsigned nestingLimit = 0;
if (!parseArgs(argc, argv, summaryFile))
return 1;
const std::vector<std::string> files = getSourceFiles(argc, argv);
size_t fileCount = files.size();
std::vector<std::vector<FunctionBytecodeSummary>> scriptSummaries;
scriptSummaries.reserve(fileCount);
for (size_t i = 0; i < fileCount; ++i)
{
if (!analyzeFile(files[i].c_str(), nestingLimit, scriptSummaries[i]))
return 1;
}
if (!serializeSummaries(files, scriptSummaries, summaryFile))
return 1;
fprintf(stdout, "Bytecode summary written to '%s'\n", summaryFile.c_str());
return 0;
}

View File

@ -37,6 +37,7 @@ if(LUAU_BUILD_CLI)
add_executable(Luau.Ast.CLI) add_executable(Luau.Ast.CLI)
add_executable(Luau.Reduce.CLI) add_executable(Luau.Reduce.CLI)
add_executable(Luau.Compile.CLI) add_executable(Luau.Compile.CLI)
add_executable(Luau.Bytecode.CLI)
# This also adds target `name` on Linux/macOS and `name.exe` on Windows # This also adds target `name` on Linux/macOS and `name.exe` on Windows
set_target_properties(Luau.Repl.CLI PROPERTIES OUTPUT_NAME luau) set_target_properties(Luau.Repl.CLI PROPERTIES OUTPUT_NAME luau)
@ -44,6 +45,7 @@ if(LUAU_BUILD_CLI)
set_target_properties(Luau.Ast.CLI PROPERTIES OUTPUT_NAME luau-ast) set_target_properties(Luau.Ast.CLI PROPERTIES OUTPUT_NAME luau-ast)
set_target_properties(Luau.Reduce.CLI PROPERTIES OUTPUT_NAME luau-reduce) set_target_properties(Luau.Reduce.CLI PROPERTIES OUTPUT_NAME luau-reduce)
set_target_properties(Luau.Compile.CLI PROPERTIES OUTPUT_NAME luau-compile) set_target_properties(Luau.Compile.CLI PROPERTIES OUTPUT_NAME luau-compile)
set_target_properties(Luau.Bytecode.CLI PROPERTIES OUTPUT_NAME luau-bytecode)
endif() endif()
if(LUAU_BUILD_TESTS) if(LUAU_BUILD_TESTS)
@ -187,6 +189,7 @@ if(LUAU_BUILD_CLI)
target_compile_options(Luau.Analyze.CLI PRIVATE ${LUAU_OPTIONS}) target_compile_options(Luau.Analyze.CLI PRIVATE ${LUAU_OPTIONS})
target_compile_options(Luau.Ast.CLI PRIVATE ${LUAU_OPTIONS}) target_compile_options(Luau.Ast.CLI PRIVATE ${LUAU_OPTIONS})
target_compile_options(Luau.Compile.CLI PRIVATE ${LUAU_OPTIONS}) target_compile_options(Luau.Compile.CLI PRIVATE ${LUAU_OPTIONS})
target_compile_options(Luau.Bytecode.CLI PRIVATE ${LUAU_OPTIONS})
target_include_directories(Luau.Repl.CLI PRIVATE extern extern/isocline/include) target_include_directories(Luau.Repl.CLI PRIVATE extern extern/isocline/include)
@ -209,6 +212,8 @@ if(LUAU_BUILD_CLI)
target_link_libraries(Luau.Reduce.CLI PRIVATE Luau.Common Luau.Ast Luau.Analysis) target_link_libraries(Luau.Reduce.CLI PRIVATE Luau.Common Luau.Ast Luau.Analysis)
target_link_libraries(Luau.Compile.CLI PRIVATE Luau.Compiler Luau.VM Luau.CodeGen) target_link_libraries(Luau.Compile.CLI PRIVATE Luau.Compiler Luau.VM Luau.CodeGen)
target_link_libraries(Luau.Bytecode.CLI PRIVATE Luau.Compiler Luau.VM Luau.CodeGen)
endif() endif()
if(LUAU_BUILD_TESTS) if(LUAU_BUILD_TESTS)

View File

@ -0,0 +1,81 @@
// This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details
#pragma once
#include "Luau/Common.h"
#include "Luau/Bytecode.h"
#include <string>
#include <vector>
struct lua_State;
struct Proto;
namespace Luau
{
namespace CodeGen
{
class FunctionBytecodeSummary
{
public:
FunctionBytecodeSummary(std::string source, std::string name, const int line, unsigned nestingLimit);
const std::string& getSource() const
{
return source;
}
const std::string& getName() const
{
return name;
}
int getLine() const
{
return line;
}
const unsigned getNestingLimit() const
{
return nestingLimit;
}
const unsigned getOpLimit() const
{
return LOP__COUNT;
}
void incCount(unsigned nesting, uint8_t op)
{
LUAU_ASSERT(nesting <= getNestingLimit());
LUAU_ASSERT(op < getOpLimit());
++counts[nesting][op];
}
unsigned getCount(unsigned nesting, uint8_t op) const
{
LUAU_ASSERT(nesting <= getNestingLimit());
LUAU_ASSERT(op < getOpLimit());
return counts[nesting][op];
}
const std::vector<unsigned>& getCounts(unsigned nesting) const
{
LUAU_ASSERT(nesting <= getNestingLimit());
return counts[nesting];
}
static FunctionBytecodeSummary fromProto(Proto* proto, unsigned nestingLimit);
private:
std::string source;
std::string name;
int line;
unsigned nestingLimit;
std::vector<std::vector<unsigned>> counts;
};
std::vector<FunctionBytecodeSummary> summarizeBytecode(lua_State* L, int idx, unsigned nestingLimit);
} // namespace CodeGen
} // namespace Luau

View File

@ -6,6 +6,8 @@
#include <stdarg.h> #include <stdarg.h>
#include <stdio.h> #include <stdio.h>
LUAU_FASTFLAG(LuauCodeGenFixByteLower)
namespace Luau namespace Luau
{ {
namespace CodeGen namespace CodeGen
@ -1437,11 +1439,19 @@ void AssemblyBuilderX64::placeImm8(int32_t imm)
{ {
int8_t imm8 = int8_t(imm); int8_t imm8 = int8_t(imm);
if (FFlag::LuauCodeGenFixByteLower)
{
LUAU_ASSERT(imm8 == imm);
place(imm8);
}
else
{
if (imm8 == imm) if (imm8 == imm)
place(imm8); place(imm8);
else else
LUAU_ASSERT(!"Invalid immediate value"); LUAU_ASSERT(!"Invalid immediate value");
} }
}
void AssemblyBuilderX64::placeImm16(int16_t imm) void AssemblyBuilderX64::placeImm16(int16_t imm)
{ {

View File

@ -0,0 +1,71 @@
// This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details
#include "Luau/BytecodeSummary.h"
#include "CodeGenLower.h"
#include "lua.h"
#include "lapi.h"
#include "lobject.h"
#include "lstate.h"
namespace Luau
{
namespace CodeGen
{
FunctionBytecodeSummary::FunctionBytecodeSummary(std::string source, std::string name, const int line, unsigned nestingLimit)
: source(std::move(source))
, name(std::move(name))
, line(line)
, nestingLimit(nestingLimit)
{
counts.reserve(nestingLimit);
for (unsigned i = 0; i < 1 + nestingLimit; ++i)
{
counts.push_back(std::vector<unsigned>(getOpLimit(), 0));
}
}
FunctionBytecodeSummary FunctionBytecodeSummary::fromProto(Proto* proto, unsigned nestingLimit)
{
const char* source = getstr(proto->source);
source = (source[0] == '=' || source[0] == '@') ? source + 1 : "[string]";
const char* name = proto->debugname ? getstr(proto->debugname) : "";
int line = proto->linedefined;
FunctionBytecodeSummary summary(source, name, line, nestingLimit);
for (int i = 0; i < proto->sizecode; ++i)
{
Instruction insn = proto->code[i];
uint8_t op = LUAU_INSN_OP(insn);
summary.incCount(0, op);
}
return summary;
}
std::vector<FunctionBytecodeSummary> summarizeBytecode(lua_State* L, int idx, unsigned nestingLimit)
{
LUAU_ASSERT(lua_isLfunction(L, idx));
const TValue* func = luaA_toobject(L, idx);
Proto* root = clvalue(func)->l.p;
std::vector<Proto*> protos;
gatherFunctions(protos, root, CodeGen_ColdFunctions);
std::vector<FunctionBytecodeSummary> summaries;
summaries.reserve(protos.size());
for (Proto* proto : protos)
{
summaries.push_back(FunctionBytecodeSummary::fromProto(proto, nestingLimit));
}
return summaries;
}
} // namespace CodeGen
} // namespace Luau

View File

@ -405,7 +405,15 @@ void IrLoweringA64::lowerInst(IrInst& inst, uint32_t index, const IrBlock& next)
case IrCmd::STORE_POINTER: case IrCmd::STORE_POINTER:
{ {
AddressA64 addr = tempAddr(inst.a, offsetof(TValue, value)); AddressA64 addr = tempAddr(inst.a, offsetof(TValue, value));
if (inst.b.kind == IrOpKind::Constant)
{
LUAU_ASSERT(intOp(inst.b) == 0);
build.str(xzr, addr);
}
else
{
build.str(regOp(inst.b), addr); build.str(regOp(inst.b), addr);
}
break; break;
} }
case IrCmd::STORE_DOUBLE: case IrCmd::STORE_DOUBLE:

View File

@ -15,6 +15,8 @@
#include "lstate.h" #include "lstate.h"
#include "lgc.h" #include "lgc.h"
LUAU_FASTFLAG(LuauCodeGenFixByteLower)
namespace Luau namespace Luau
{ {
namespace CodeGen namespace CodeGen
@ -213,11 +215,24 @@ void IrLoweringX64::lowerInst(IrInst& inst, uint32_t index, const IrBlock& next)
LUAU_ASSERT(!"Unsupported instruction form"); LUAU_ASSERT(!"Unsupported instruction form");
break; break;
case IrCmd::STORE_POINTER: case IrCmd::STORE_POINTER:
if (inst.a.kind == IrOpKind::Inst) {
build.mov(qword[regOp(inst.a) + offsetof(TValue, value)], regOp(inst.b)); OperandX64 valueLhs = inst.a.kind == IrOpKind::Inst ? qword[regOp(inst.a) + offsetof(TValue, value)] : luauRegValue(vmRegOp(inst.a));
if (inst.b.kind == IrOpKind::Constant)
{
LUAU_ASSERT(intOp(inst.b) == 0);
build.mov(valueLhs, 0);
}
else if (inst.b.kind == IrOpKind::Inst)
{
build.mov(valueLhs, regOp(inst.b));
}
else else
build.mov(luauRegValue(vmRegOp(inst.a)), regOp(inst.b)); {
LUAU_ASSERT(!"Unsupported instruction form");
}
break; break;
}
case IrCmd::STORE_DOUBLE: case IrCmd::STORE_DOUBLE:
{ {
OperandX64 valueLhs = inst.a.kind == IrOpKind::Inst ? qword[regOp(inst.a) + offsetof(TValue, value)] : luauRegValue(vmRegOp(inst.a)); OperandX64 valueLhs = inst.a.kind == IrOpKind::Inst ? qword[regOp(inst.a) + offsetof(TValue, value)] : luauRegValue(vmRegOp(inst.a));
@ -1786,10 +1801,19 @@ void IrLoweringX64::lowerInst(IrInst& inst, uint32_t index, const IrBlock& next)
break; break;
case IrCmd::BUFFER_WRITEI8: case IrCmd::BUFFER_WRITEI8:
{
if (FFlag::LuauCodeGenFixByteLower)
{
OperandX64 value = inst.c.kind == IrOpKind::Inst ? byteReg(regOp(inst.c)) : OperandX64(int8_t(intOp(inst.c)));
build.mov(byte[bufferAddrOp(inst.a, inst.b)], value);
}
else
{ {
OperandX64 value = inst.c.kind == IrOpKind::Inst ? byteReg(regOp(inst.c)) : OperandX64(intOp(inst.c)); OperandX64 value = inst.c.kind == IrOpKind::Inst ? byteReg(regOp(inst.c)) : OperandX64(intOp(inst.c));
build.mov(byte[bufferAddrOp(inst.a, inst.b)], value); build.mov(byte[bufferAddrOp(inst.a, inst.b)], value);
}
break; break;
} }
@ -1806,10 +1830,19 @@ void IrLoweringX64::lowerInst(IrInst& inst, uint32_t index, const IrBlock& next)
break; break;
case IrCmd::BUFFER_WRITEI16: case IrCmd::BUFFER_WRITEI16:
{
if (FFlag::LuauCodeGenFixByteLower)
{
OperandX64 value = inst.c.kind == IrOpKind::Inst ? wordReg(regOp(inst.c)) : OperandX64(int16_t(intOp(inst.c)));
build.mov(word[bufferAddrOp(inst.a, inst.b)], value);
}
else
{ {
OperandX64 value = inst.c.kind == IrOpKind::Inst ? wordReg(regOp(inst.c)) : OperandX64(intOp(inst.c)); OperandX64 value = inst.c.kind == IrOpKind::Inst ? wordReg(regOp(inst.c)) : OperandX64(intOp(inst.c));
build.mov(word[bufferAddrOp(inst.a, inst.b)], value); build.mov(word[bufferAddrOp(inst.a, inst.b)], value);
}
break; break;
} }

View File

@ -14,6 +14,7 @@
LUAU_FASTFLAGVARIABLE(LuauLowerAltLoopForn, false) LUAU_FASTFLAGVARIABLE(LuauLowerAltLoopForn, false)
LUAU_FASTFLAG(LuauImproveInsertIr) LUAU_FASTFLAG(LuauImproveInsertIr)
LUAU_FASTFLAGVARIABLE(LuauFullLoopLuserdata, false)
namespace Luau namespace Luau
{ {
@ -808,7 +809,7 @@ void translateInstForGPrepNext(IrBuilder& build, const Instruction* pc, int pcpo
build.inst(IrCmd::STORE_TAG, build.vmReg(ra), build.constTag(LUA_TNIL)); build.inst(IrCmd::STORE_TAG, build.vmReg(ra), build.constTag(LUA_TNIL));
// setpvalue(ra + 2, reinterpret_cast<void*>(uintptr_t(0))); // setpvalue(ra + 2, reinterpret_cast<void*>(uintptr_t(0)));
build.inst(IrCmd::STORE_INT, build.vmReg(ra + 2), build.constInt(0)); build.inst(FFlag::LuauFullLoopLuserdata ? IrCmd::STORE_POINTER : IrCmd::STORE_INT, build.vmReg(ra + 2), build.constInt(0));
build.inst(IrCmd::STORE_TAG, build.vmReg(ra + 2), build.constTag(LUA_TLIGHTUSERDATA)); build.inst(IrCmd::STORE_TAG, build.vmReg(ra + 2), build.constTag(LUA_TLIGHTUSERDATA));
build.inst(IrCmd::JUMP, target); build.inst(IrCmd::JUMP, target);
@ -840,7 +841,7 @@ void translateInstForGPrepInext(IrBuilder& build, const Instruction* pc, int pcp
build.inst(IrCmd::STORE_TAG, build.vmReg(ra), build.constTag(LUA_TNIL)); build.inst(IrCmd::STORE_TAG, build.vmReg(ra), build.constTag(LUA_TNIL));
// setpvalue(ra + 2, reinterpret_cast<void*>(uintptr_t(0))); // setpvalue(ra + 2, reinterpret_cast<void*>(uintptr_t(0)));
build.inst(IrCmd::STORE_INT, build.vmReg(ra + 2), build.constInt(0)); build.inst(FFlag::LuauFullLoopLuserdata ? IrCmd::STORE_POINTER : IrCmd::STORE_INT, build.vmReg(ra + 2), build.constInt(0));
build.inst(IrCmd::STORE_TAG, build.vmReg(ra + 2), build.constTag(LUA_TLIGHTUSERDATA)); build.inst(IrCmd::STORE_TAG, build.vmReg(ra + 2), build.constTag(LUA_TLIGHTUSERDATA));
build.inst(IrCmd::JUMP, target); build.inst(IrCmd::JUMP, target);

View File

@ -17,6 +17,7 @@ LUAU_FASTINTVARIABLE(LuauCodeGenReuseSlotLimit, 64)
LUAU_FASTFLAGVARIABLE(DebugLuauAbortingChecks, false) LUAU_FASTFLAGVARIABLE(DebugLuauAbortingChecks, false)
LUAU_FASTFLAGVARIABLE(LuauReuseArrSlots2, false) LUAU_FASTFLAGVARIABLE(LuauReuseArrSlots2, false)
LUAU_FASTFLAG(LuauLowerAltLoopForn) LUAU_FASTFLAG(LuauLowerAltLoopForn)
LUAU_FASTFLAGVARIABLE(LuauCodeGenFixByteLower, false)
namespace Luau namespace Luau
{ {
@ -618,6 +619,9 @@ static void constPropInInst(ConstPropState& state, IrBuilder& build, IrFunction&
if (inst.a.kind == IrOpKind::VmReg) if (inst.a.kind == IrOpKind::VmReg)
{ {
state.invalidateValue(inst.a); state.invalidateValue(inst.a);
if (inst.b.kind == IrOpKind::Inst)
{
state.forwardVmRegStoreToLoad(inst, IrCmd::LOAD_POINTER); state.forwardVmRegStoreToLoad(inst, IrCmd::LOAD_POINTER);
if (IrInst* instOp = function.asInstOp(inst.b); instOp && instOp->cmd == IrCmd::NEW_TABLE) if (IrInst* instOp = function.asInstOp(inst.b); instOp && instOp->cmd == IrCmd::NEW_TABLE)
@ -630,6 +634,7 @@ static void constPropInInst(ConstPropState& state, IrBuilder& build, IrFunction&
} }
} }
} }
}
break; break;
case IrCmd::STORE_DOUBLE: case IrCmd::STORE_DOUBLE:
if (inst.a.kind == IrOpKind::VmReg) if (inst.a.kind == IrOpKind::VmReg)

View File

@ -540,7 +540,7 @@ public:
return impl.end(); return impl.end();
} }
bool operator==(const DenseHashSet<Key, Hash, Eq>& other) bool operator==(const DenseHashSet<Key, Hash, Eq>& other) const
{ {
if (size() != other.size()) if (size() != other.size())
return false; return false;
@ -554,7 +554,7 @@ public:
return true; return true;
} }
bool operator!=(const DenseHashSet<Key, Hash, Eq>& other) bool operator!=(const DenseHashSet<Key, Hash, Eq>& other) const
{ {
return !(*this == other); return !(*this == other);
} }

View File

@ -54,6 +54,10 @@ COMPILE_CLI_SOURCES=CLI/FileUtils.cpp CLI/Flags.cpp CLI/Compile.cpp
COMPILE_CLI_OBJECTS=$(COMPILE_CLI_SOURCES:%=$(BUILD)/%.o) COMPILE_CLI_OBJECTS=$(COMPILE_CLI_SOURCES:%=$(BUILD)/%.o)
COMPILE_CLI_TARGET=$(BUILD)/luau-compile COMPILE_CLI_TARGET=$(BUILD)/luau-compile
BYTECODE_CLI_SOURCES=CLI/FileUtils.cpp CLI/Flags.cpp CLI/Bytecode.cpp
BYTECODE_CLI_OBJECTS=$(BYTECODE_CLI_SOURCES:%=$(BUILD)/%.o)
BYTECODE_CLI_TARGET=$(BUILD)/luau-bytecode
FUZZ_SOURCES=$(wildcard fuzz/*.cpp) fuzz/luau.pb.cpp FUZZ_SOURCES=$(wildcard fuzz/*.cpp) fuzz/luau.pb.cpp
FUZZ_OBJECTS=$(FUZZ_SOURCES:%=$(BUILD)/%.o) FUZZ_OBJECTS=$(FUZZ_SOURCES:%=$(BUILD)/%.o)
@ -65,8 +69,8 @@ ifneq ($(opt),)
TESTS_ARGS+=-O$(opt) TESTS_ARGS+=-O$(opt)
endif endif
OBJECTS=$(AST_OBJECTS) $(COMPILER_OBJECTS) $(CONFIG_OBJECTS) $(ANALYSIS_OBJECTS) $(CODEGEN_OBJECTS) $(VM_OBJECTS) $(ISOCLINE_OBJECTS) $(TESTS_OBJECTS) $(REPL_CLI_OBJECTS) $(ANALYZE_CLI_OBJECTS) $(COMPILE_CLI_OBJECTS) $(FUZZ_OBJECTS) OBJECTS=$(AST_OBJECTS) $(COMPILER_OBJECTS) $(CONFIG_OBJECTS) $(ANALYSIS_OBJECTS) $(CODEGEN_OBJECTS) $(VM_OBJECTS) $(ISOCLINE_OBJECTS) $(TESTS_OBJECTS) $(REPL_CLI_OBJECTS) $(ANALYZE_CLI_OBJECTS) $(COMPILE_CLI_OBJECTS) $(BYTECODE_CLI_OBJECTS) $(FUZZ_OBJECTS)
EXECUTABLE_ALIASES = luau luau-analyze luau-compile luau-tests EXECUTABLE_ALIASES = luau luau-analyze luau-compile luau-bytecode luau-tests
# common flags # common flags
CXXFLAGS=-g -Wall CXXFLAGS=-g -Wall
@ -142,6 +146,7 @@ $(TESTS_OBJECTS): CXXFLAGS+=-std=c++17 -ICommon/include -IAst/include -ICompiler
$(REPL_CLI_OBJECTS): CXXFLAGS+=-std=c++17 -ICommon/include -IAst/include -ICompiler/include -IVM/include -ICodeGen/include -Iextern -Iextern/isocline/include $(REPL_CLI_OBJECTS): CXXFLAGS+=-std=c++17 -ICommon/include -IAst/include -ICompiler/include -IVM/include -ICodeGen/include -Iextern -Iextern/isocline/include
$(ANALYZE_CLI_OBJECTS): CXXFLAGS+=-std=c++17 -ICommon/include -IAst/include -IAnalysis/include -IConfig/include -Iextern $(ANALYZE_CLI_OBJECTS): CXXFLAGS+=-std=c++17 -ICommon/include -IAst/include -IAnalysis/include -IConfig/include -Iextern
$(COMPILE_CLI_OBJECTS): CXXFLAGS+=-std=c++17 -ICommon/include -IAst/include -ICompiler/include -IVM/include -ICodeGen/include $(COMPILE_CLI_OBJECTS): CXXFLAGS+=-std=c++17 -ICommon/include -IAst/include -ICompiler/include -IVM/include -ICodeGen/include
$(BYTECODE_CLI_OBJECTS): CXXFLAGS+=-std=c++17 -ICommon/include -IAst/include -ICompiler/include -IVM/include -ICodeGen/include
$(FUZZ_OBJECTS): CXXFLAGS+=-std=c++17 -ICommon/include -IAst/include -ICompiler/include -IAnalysis/include -IVM/include -ICodeGen/include -IConfig/include $(FUZZ_OBJECTS): CXXFLAGS+=-std=c++17 -ICommon/include -IAst/include -ICompiler/include -IAnalysis/include -IVM/include -ICodeGen/include -IConfig/include
$(TESTS_TARGET): LDFLAGS+=-lpthread $(TESTS_TARGET): LDFLAGS+=-lpthread
@ -206,6 +211,9 @@ luau-analyze: $(ANALYZE_CLI_TARGET)
luau-compile: $(COMPILE_CLI_TARGET) luau-compile: $(COMPILE_CLI_TARGET)
ln -fs $^ $@ ln -fs $^ $@
luau-bytecode: $(BYTECODE_CLI_TARGET)
ln -fs $^ $@
luau-tests: $(TESTS_TARGET) luau-tests: $(TESTS_TARGET)
ln -fs $^ $@ ln -fs $^ $@
@ -214,8 +222,9 @@ $(TESTS_TARGET): $(TESTS_OBJECTS) $(ANALYSIS_TARGET) $(COMPILER_TARGET) $(CONFIG
$(REPL_CLI_TARGET): $(REPL_CLI_OBJECTS) $(COMPILER_TARGET) $(AST_TARGET) $(CODEGEN_TARGET) $(VM_TARGET) $(ISOCLINE_TARGET) $(REPL_CLI_TARGET): $(REPL_CLI_OBJECTS) $(COMPILER_TARGET) $(AST_TARGET) $(CODEGEN_TARGET) $(VM_TARGET) $(ISOCLINE_TARGET)
$(ANALYZE_CLI_TARGET): $(ANALYZE_CLI_OBJECTS) $(ANALYSIS_TARGET) $(AST_TARGET) $(CONFIG_TARGET) $(ANALYZE_CLI_TARGET): $(ANALYZE_CLI_OBJECTS) $(ANALYSIS_TARGET) $(AST_TARGET) $(CONFIG_TARGET)
$(COMPILE_CLI_TARGET): $(COMPILE_CLI_OBJECTS) $(COMPILER_TARGET) $(AST_TARGET) $(CODEGEN_TARGET) $(VM_TARGET) $(COMPILE_CLI_TARGET): $(COMPILE_CLI_OBJECTS) $(COMPILER_TARGET) $(AST_TARGET) $(CODEGEN_TARGET) $(VM_TARGET)
$(BYTECODE_CLI_TARGET): $(BYTECODE_CLI_OBJECTS) $(COMPILER_TARGET) $(AST_TARGET) $(CODEGEN_TARGET) $(VM_TARGET)
$(TESTS_TARGET) $(REPL_CLI_TARGET) $(ANALYZE_CLI_TARGET) $(COMPILE_CLI_TARGET): $(TESTS_TARGET) $(REPL_CLI_TARGET) $(ANALYZE_CLI_TARGET) $(COMPILE_CLI_TARGET) $(BYTECODE_CLI_TARGET):
$(CXX) $^ $(LDFLAGS) -o $@ $(CXX) $^ $(LDFLAGS) -o $@
# executable targets for fuzzing # executable targets for fuzzing

View File

@ -92,6 +92,7 @@ target_sources(Luau.CodeGen PRIVATE
CodeGen/include/Luau/UnwindBuilder.h CodeGen/include/Luau/UnwindBuilder.h
CodeGen/include/Luau/UnwindBuilderDwarf2.h CodeGen/include/Luau/UnwindBuilderDwarf2.h
CodeGen/include/Luau/UnwindBuilderWin.h CodeGen/include/Luau/UnwindBuilderWin.h
CodeGen/include/Luau/BytecodeSummary.h
CodeGen/include/luacodegen.h CodeGen/include/luacodegen.h
CodeGen/src/AssemblyBuilderA64.cpp CodeGen/src/AssemblyBuilderA64.cpp
@ -124,6 +125,7 @@ target_sources(Luau.CodeGen PRIVATE
CodeGen/src/OptimizeFinalX64.cpp CodeGen/src/OptimizeFinalX64.cpp
CodeGen/src/UnwindBuilderDwarf2.cpp CodeGen/src/UnwindBuilderDwarf2.cpp
CodeGen/src/UnwindBuilderWin.cpp CodeGen/src/UnwindBuilderWin.cpp
CodeGen/src/BytecodeSummary.cpp
CodeGen/src/BitUtils.h CodeGen/src/BitUtils.h
CodeGen/src/ByteUtils.h CodeGen/src/ByteUtils.h
@ -518,3 +520,13 @@ if(TARGET Luau.Compile.CLI)
CLI/Flags.cpp CLI/Flags.cpp
CLI/Compile.cpp) CLI/Compile.cpp)
endif() endif()
if(TARGET Luau.Bytecode.CLI)
# Luau.Bytecode.CLI Sources
target_sources(Luau.Bytecode.CLI PRIVATE
CLI/FileUtils.h
CLI/FileUtils.cpp
CLI/Flags.h
CLI/Flags.cpp
CLI/Bytecode.cpp)
endif()

View File

@ -10,7 +10,9 @@
#include "Luau/TypeInfer.h" #include "Luau/TypeInfer.h"
#include "Luau/BytecodeBuilder.h" #include "Luau/BytecodeBuilder.h"
#include "Luau/Frontend.h" #include "Luau/Frontend.h"
#include "Luau/Compiler.h"
#include "Luau/CodeGen.h" #include "Luau/CodeGen.h"
#include "Luau/BytecodeSummary.h"
#include "doctest.h" #include "doctest.h"
#include "ScopedFlags.h" #include "ScopedFlags.h"
@ -271,6 +273,25 @@ static void* limitedRealloc(void* ud, void* ptr, size_t osize, size_t nsize)
} }
} }
static std::vector<Luau::CodeGen::FunctionBytecodeSummary> analyzeFile(const char* source, const unsigned nestingLimit)
{
Luau::BytecodeBuilder bcb;
Luau::CompileOptions options;
options.optimizationLevel = optimizationLevel;
options.debugLevel = 1;
compileOrThrow(bcb, source, options);
const std::string& bytecode = bcb.getBytecode();
std::unique_ptr<lua_State, void (*)(lua_State*)> globalState(luaL_newstate(), lua_close);
lua_State* L = globalState.get();
LUAU_ASSERT(luau_load(L, "source", bytecode.data(), bytecode.size(), 0) == 0);
return Luau::CodeGen::summarizeBytecode(L, -1, nestingLimit);
}
TEST_SUITE_BEGIN("Conformance"); TEST_SUITE_BEGIN("Conformance");
TEST_CASE("CodegenSupported") TEST_CASE("CodegenSupported")
@ -292,6 +313,7 @@ TEST_CASE("Basic")
TEST_CASE("Buffers") TEST_CASE("Buffers")
{ {
ScopedFastFlag luauBufferBetterMsg{"LuauBufferBetterMsg", true}; ScopedFastFlag luauBufferBetterMsg{"LuauBufferBetterMsg", true};
ScopedFastFlag luauCodeGenFixByteLower{"LuauCodeGenFixByteLower", true};
runConformance("buffers.lua"); runConformance("buffers.lua");
} }
@ -1988,4 +2010,51 @@ TEST_CASE("HugeFunction")
CHECK(lua_tonumber(L, -1) == 42); CHECK(lua_tonumber(L, -1) == 42);
} }
TEST_CASE("BytecodeDistributionPerFunctionTest")
{
const char* source = R"(
local function first(n, p)
local t = {}
for i=1,p do t[i] = i*10 end
local function inner(_,n)
if n > 0 then
n = n-1
return n, unpack(t)
end
end
return inner, nil, n
end
local function second(x)
return x[1]
end
)";
std::vector<Luau::CodeGen::FunctionBytecodeSummary> summaries(analyzeFile(source, 0));
CHECK_EQ(summaries[0].getName(), "inner");
CHECK_EQ(summaries[0].getLine(), 6);
CHECK_EQ(summaries[0].getCounts(0),
std::vector<unsigned>({1, 1, 0, 0, 1, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0}));
CHECK_EQ(summaries[1].getName(), "first");
CHECK_EQ(summaries[1].getLine(), 2);
CHECK_EQ(summaries[1].getCounts(0),
std::vector<unsigned>({1, 0, 1, 0, 2, 0, 3, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}));
CHECK_EQ(summaries[2].getName(), "second");
CHECK_EQ(summaries[2].getLine(), 15);
CHECK_EQ(summaries[2].getCounts(0),
std::vector<unsigned>({0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}));
CHECK_EQ(summaries[3].getName(), "");
CHECK_EQ(summaries[3].getLine(), 1);
CHECK_EQ(summaries[3].getCounts(0),
std::vector<unsigned>({0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}));
}
TEST_SUITE_END(); TEST_SUITE_END();

View File

@ -317,4 +317,97 @@ TEST_CASE_FIXTURE(DataFlowGraphFixture, "mutate_property_of_table_owned_by_while
CHECK(x1 != x2); CHECK(x1 != x2);
} }
TEST_CASE_FIXTURE(DataFlowGraphFixture, "property_lookup_on_a_phi_node")
{
dfg(R"(
local t = {}
t.x = 5
if cond() then
t.x = 7
end
print(t.x)
)");
DefId x1 = getDef<AstExprIndexName, 1>(); // t.x = 5
DefId x2 = getDef<AstExprIndexName, 2>(); // t.x = 7
DefId x3 = getDef<AstExprIndexName, 3>(); // print(t.x)
CHECK(x1 != x2);
CHECK(x2 != x3);
const Phi* phi = get<Phi>(x3);
REQUIRE(phi);
CHECK(phi->operands.at(0) == x1);
CHECK(phi->operands.at(1) == x2);
}
TEST_CASE_FIXTURE(DataFlowGraphFixture, "property_lookup_on_a_phi_node_2")
{
dfg(R"(
local t = {}
if cond() then
t.x = 5
else
t.x = 7
end
print(t.x)
)");
DefId x1 = getDef<AstExprIndexName, 1>(); // t.x = 5
DefId x2 = getDef<AstExprIndexName, 2>(); // t.x = 7
DefId x3 = getDef<AstExprIndexName, 3>(); // print(t.x)
CHECK(x1 != x2);
CHECK(x2 != x3);
const Phi* phi = get<Phi>(x3);
REQUIRE(phi);
CHECK(phi->operands.at(0) == x2);
CHECK(phi->operands.at(1) == x1);
}
TEST_CASE_FIXTURE(DataFlowGraphFixture, "property_lookup_on_a_phi_node_3")
{
dfg(R"(
local t = {}
t.x = 3
if cond() then
t.x = 5
t.y = 7
else
t.z = 42
end
print(t.x)
print(t.y)
print(t.z)
)");
DefId x1 = getDef<AstExprIndexName, 1>(); // t.x = 3
DefId x2 = getDef<AstExprIndexName, 2>(); // t.x = 5
DefId y1 = getDef<AstExprIndexName, 3>(); // t.y = 7
DefId z1 = getDef<AstExprIndexName, 4>(); // t.z = 42
DefId x3 = getDef<AstExprIndexName, 5>(); // print(t.x)
DefId y2 = getDef<AstExprIndexName, 6>(); // print(t.y)
DefId z2 = getDef<AstExprIndexName, 7>(); // print(t.z)
CHECK(x1 != x2);
CHECK(x2 != x3);
CHECK(y1 == y2);
CHECK(z1 == z2);
const Phi* phi = get<Phi>(x3);
REQUIRE(phi);
CHECK(phi->operands.at(0) == x1);
CHECK(phi->operands.at(1) == x2);
}
TEST_SUITE_END(); TEST_SUITE_END();

View File

@ -412,7 +412,7 @@ TypeId Fixture::requireTypeAlias(const std::string& name)
{ {
std::optional<TypeId> ty = lookupType(name); std::optional<TypeId> ty = lookupType(name);
REQUIRE(ty); REQUIRE(ty);
return *ty; return follow(*ty);
} }
TypeId Fixture::requireExportedType(const ModuleName& moduleName, const std::string& name) TypeId Fixture::requireExportedType(const ModuleName& moduleName, const std::string& name)

View File

@ -6,12 +6,22 @@
#include "Luau/Common.h" #include "Luau/Common.h"
#include "Luau/Ast.h" #include "Luau/Ast.h"
#include "Luau/ModuleResolver.h" #include "Luau/ModuleResolver.h"
#include "Luau/VisitType.h"
#include "ScopedFlags.h" #include "ScopedFlags.h"
#include "doctest.h" #include "doctest.h"
#include <iostream> #include <iostream>
using namespace Luau; using namespace Luau;
#define NONSTRICT_REQUIRE_CHECKED_ERR(index, name, result) \
do \
{ \
REQUIRE(index < result.errors.size()); \
auto err##index = get<CheckedFunctionCallError>(result.errors[index]); \
REQUIRE(err##index != nullptr); \
CHECK_EQ((err##index)->checkedFunctionName, name); \
} while (false)
struct NonStrictTypeCheckerFixture : Fixture struct NonStrictTypeCheckerFixture : Fixture
{ {
@ -28,22 +38,167 @@ struct NonStrictTypeCheckerFixture : Fixture
std::string definitions = R"BUILTIN_SRC( std::string definitions = R"BUILTIN_SRC(
declare function @checked abs(n: number): number declare function @checked abs(n: number): number
declare function @checked lower(s: string): string
declare function cond() : boolean
)BUILTIN_SRC"; )BUILTIN_SRC";
}; };
TEST_SUITE_BEGIN("NonStrictTypeCheckerTest"); TEST_SUITE_BEGIN("NonStrictTypeCheckerTest");
TEST_CASE_FIXTURE(NonStrictTypeCheckerFixture, "simple_non_strict") TEST_CASE_FIXTURE(NonStrictTypeCheckerFixture, "simple_non_strict_failure")
{ {
auto res = checkNonStrict(R"BUILTIN_SRC( CheckResult result = checkNonStrict(R"BUILTIN_SRC(
abs("hi") abs("hi")
)BUILTIN_SRC"); )BUILTIN_SRC");
LUAU_REQUIRE_ERRORS(res); LUAU_REQUIRE_ERROR_COUNT(1, result);
REQUIRE(res.errors.size() == 1); NONSTRICT_REQUIRE_CHECKED_ERR(0, "abs", result);
auto err = get<CheckedFunctionCallError>(res.errors[0]);
REQUIRE(err != nullptr);
REQUIRE(err->checkedFunctionName == "abs");
} }
TEST_CASE_FIXTURE(NonStrictTypeCheckerFixture, "nested_function_calls_constant")
{
CheckResult result = checkNonStrict(R"(
local x
abs(lower(x))
)");
LUAU_REQUIRE_ERROR_COUNT(1, result);
NONSTRICT_REQUIRE_CHECKED_ERR(0, "abs", result);
}
TEST_CASE_FIXTURE(NonStrictTypeCheckerFixture, "if_then_else_warns_with_never_local")
{
CheckResult result = checkNonStrict(R"(
local x : never
if cond() then
abs(x)
else
lower(x)
end
)");
LUAU_REQUIRE_ERROR_COUNT(2, result);
NONSTRICT_REQUIRE_CHECKED_ERR(0, "abs", result);
NONSTRICT_REQUIRE_CHECKED_ERR(1, "lower", result);
}
TEST_CASE_FIXTURE(NonStrictTypeCheckerFixture, "if_then_else_warns_nil_branches")
{
auto result = checkNonStrict(R"(
local x
if cond() then
abs(x)
else
lower(x)
end
)");
LUAU_REQUIRE_ERROR_COUNT(2, result);
NONSTRICT_REQUIRE_CHECKED_ERR(0, "abs", result);
NONSTRICT_REQUIRE_CHECKED_ERR(1, "lower", result);
}
TEST_CASE_FIXTURE(NonStrictTypeCheckerFixture, "if_then_else_doesnt_warn_else_branch")
{
auto result = checkNonStrict(R"(
local x : string = "hi"
if cond() then
abs(x)
else
lower(x)
end
)");
LUAU_REQUIRE_ERROR_COUNT(1, result);
NONSTRICT_REQUIRE_CHECKED_ERR(0, "abs", result);
}
TEST_CASE_FIXTURE(NonStrictTypeCheckerFixture, "if_then_no_else")
{
CheckResult result = checkNonStrict(R"(
local x : string
if cond() then
abs(x)
end
)");
LUAU_REQUIRE_NO_ERRORS(result);
}
TEST_CASE_FIXTURE(NonStrictTypeCheckerFixture, "if_then_no_else_err_in_cond")
{
CheckResult result = checkNonStrict(R"(
local x : string
if abs(x) then
lower(x)
end
)");
LUAU_REQUIRE_ERROR_COUNT(1, result);
NONSTRICT_REQUIRE_CHECKED_ERR(0, "abs", result);
}
TEST_CASE_FIXTURE(NonStrictTypeCheckerFixture, "if_then_else_expr_should_warn")
{
CheckResult result = checkNonStrict(R"(
local x : never
local y = if cond() then abs(x) else lower(x)
)");
LUAU_REQUIRE_ERROR_COUNT(2, result);
NONSTRICT_REQUIRE_CHECKED_ERR(0, "abs", result);
NONSTRICT_REQUIRE_CHECKED_ERR(1, "lower", result);
}
TEST_CASE_FIXTURE(NonStrictTypeCheckerFixture, "if_then_else_expr_doesnt_warn_else_branch")
{
CheckResult result = checkNonStrict(R"(
local x : string = "hi"
local y = if cond() then abs(x) else lower(x)
)");
LUAU_REQUIRE_ERROR_COUNT(1, result);
NONSTRICT_REQUIRE_CHECKED_ERR(0, "abs", result);
}
TEST_CASE_FIXTURE(NonStrictTypeCheckerFixture, "sequencing_errors")
{
CheckResult result = checkNonStrict(R"(
function f(x)
abs(x)
lower(x)
end
)");
LUAU_REQUIRE_ERROR_COUNT(2, result);
NONSTRICT_REQUIRE_CHECKED_ERR(0, "abs", result);
NONSTRICT_REQUIRE_CHECKED_ERR(1, "lower", result);
}
TEST_CASE_FIXTURE(NonStrictTypeCheckerFixture, "sequencing_if_checked_call")
{
CheckResult result = checkNonStrict(R"(
local x
if cond() then
x = 5
else
x = nil
end
lower(x)
)");
LUAU_REQUIRE_ERROR_COUNT(1, result);
NONSTRICT_REQUIRE_CHECKED_ERR(0, "lower", result);
}
TEST_CASE_FIXTURE(NonStrictTypeCheckerFixture, "sequencing_unrelated_checked_calls")
{
CheckResult result = checkNonStrict(R"(
function h(x, y)
abs(x)
lower(y)
end
)");
LUAU_REQUIRE_NO_ERRORS(result);
}
TEST_SUITE_END(); TEST_SUITE_END();

View File

@ -60,4 +60,43 @@ TEST_CASE("erase_works_and_decreases_size")
CHECK(!s1.contains(2)); CHECK(!s1.contains(2));
} }
TEST_CASE("iterate_over_set")
{
Luau::Set<int> s1{0};
s1.insert(1);
s1.insert(2);
s1.insert(3);
REQUIRE(s1.size() == 3);
int sum = 0;
for (int e : s1)
sum += e;
CHECK(sum == 6);
}
TEST_CASE("iterate_over_set_skips_erased_elements")
{
Luau::Set<int> s1{0};
s1.insert(1);
s1.insert(2);
s1.insert(3);
s1.insert(4);
s1.insert(5);
s1.insert(6);
REQUIRE(s1.size() == 6);
s1.erase(2);
s1.erase(4);
s1.erase(6);
int sum = 0;
for (int e : s1)
sum += e;
CHECK(sum == 9);
}
TEST_SUITE_END(); TEST_SUITE_END();

View File

@ -10,6 +10,7 @@
#include "doctest.h" #include "doctest.h"
#include "Fixture.h" #include "Fixture.h"
#include "RegisterCallbacks.h" #include "RegisterCallbacks.h"
#include <initializer_list> #include <initializer_list>
using namespace Luau; using namespace Luau;
@ -17,9 +18,38 @@ using namespace Luau;
namespace Luau namespace Luau
{ {
std::ostream& operator<<(std::ostream& lhs, const SubtypingVariance& variance)
{
switch (variance)
{
case SubtypingVariance::Covariant:
return lhs << "covariant";
case SubtypingVariance::Invariant:
return lhs << "invariant";
case SubtypingVariance::Invalid:
return lhs << "*invalid*";
}
return lhs;
}
std::ostream& operator<<(std::ostream& lhs, const SubtypingReasoning& reasoning) std::ostream& operator<<(std::ostream& lhs, const SubtypingReasoning& reasoning)
{ {
return lhs << toString(reasoning.subPath) << " </: " << toString(reasoning.superPath); return lhs << toString(reasoning.subPath) << " </: " << toString(reasoning.superPath) << " (" << reasoning.variance << ")";
}
bool operator==(const DenseHashSet<SubtypingReasoning, SubtypingReasoningHash>& set, const std::vector<SubtypingReasoning>& items)
{
if (items.size() != set.size())
return false;
for (const SubtypingReasoning& r : items)
{
if (!set.contains(r))
return false;
}
return true;
} }
}; // namespace Luau }; // namespace Luau
@ -1105,20 +1135,6 @@ TEST_SUITE_END();
TEST_SUITE_BEGIN("Subtyping.Subpaths"); TEST_SUITE_BEGIN("Subtyping.Subpaths");
bool operator==(const DenseHashSet<SubtypingReasoning, SubtypingReasoningHash>& set, const std::vector<SubtypingReasoning>& items)
{
if (items.size() != set.size())
return false;
for (const SubtypingReasoning& r : items)
{
if (!set.contains(r))
return false;
}
return true;
}
TEST_CASE_FIXTURE(SubtypeFixture, "table_property") TEST_CASE_FIXTURE(SubtypeFixture, "table_property")
{ {
TypeId subTy = tbl({{"X", builtinTypes->numberType}}); TypeId subTy = tbl({{"X", builtinTypes->numberType}});
@ -1126,10 +1142,9 @@ TEST_CASE_FIXTURE(SubtypeFixture, "table_property")
SubtypingResult result = isSubtype(subTy, superTy); SubtypingResult result = isSubtype(subTy, superTy);
CHECK(!result.isSubtype); CHECK(!result.isSubtype);
CHECK(result.reasoning == std::vector{SubtypingReasoning{ CHECK(result.reasoning == std::vector{SubtypingReasoning{/* subPath */ Path(TypePath::Property("X")),
/* subPath */ Path(TypePath::Property("X")),
/* superPath */ Path(TypePath::Property("X")), /* superPath */ Path(TypePath::Property("X")),
}}); /* variance */ SubtypingVariance::Invariant}});
} }
TEST_CASE_FIXTURE(SubtypeFixture, "table_indexers") TEST_CASE_FIXTURE(SubtypeFixture, "table_indexers")
@ -1142,10 +1157,12 @@ TEST_CASE_FIXTURE(SubtypeFixture, "table_indexers")
CHECK(result.reasoning == std::vector{SubtypingReasoning{ CHECK(result.reasoning == std::vector{SubtypingReasoning{
/* subPath */ Path(TypePath::TypeField::IndexLookup), /* subPath */ Path(TypePath::TypeField::IndexLookup),
/* superPath */ Path(TypePath::TypeField::IndexLookup), /* superPath */ Path(TypePath::TypeField::IndexLookup),
/* variance */ SubtypingVariance::Invariant,
}, },
SubtypingReasoning{ SubtypingReasoning{
/* subPath */ Path(TypePath::TypeField::IndexResult), /* subPath */ Path(TypePath::TypeField::IndexResult),
/* superPath */ Path(TypePath::TypeField::IndexResult), /* superPath */ Path(TypePath::TypeField::IndexResult),
/* variance */ SubtypingVariance::Invariant,
}}); }});
} }
@ -1211,6 +1228,7 @@ TEST_CASE_FIXTURE(SubtypeFixture, "nested_table_properties")
CHECK(result.reasoning == std::vector{SubtypingReasoning{ CHECK(result.reasoning == std::vector{SubtypingReasoning{
/* subPath */ TypePath::PathBuilder().prop("X").prop("Y").prop("Z").build(), /* subPath */ TypePath::PathBuilder().prop("X").prop("Y").prop("Z").build(),
/* superPath */ TypePath::PathBuilder().prop("X").prop("Y").prop("Z").build(), /* superPath */ TypePath::PathBuilder().prop("X").prop("Y").prop("Z").build(),
/* variance */ SubtypingVariance::Invariant,
}}); }});
} }
@ -1252,8 +1270,10 @@ TEST_CASE_FIXTURE(SubtypeFixture, "multiple_reasonings")
SubtypingResult result = isSubtype(subTy, superTy); SubtypingResult result = isSubtype(subTy, superTy);
CHECK(!result.isSubtype); CHECK(!result.isSubtype);
CHECK(result.reasoning == std::vector{ CHECK(result.reasoning == std::vector{
SubtypingReasoning{/* subPath */ Path(TypePath::Property("X")), /* superPath */ Path(TypePath::Property("X"))}, SubtypingReasoning{/* subPath */ Path(TypePath::Property("X")), /* superPath */ Path(TypePath::Property("X")),
SubtypingReasoning{/* subPath */ Path(TypePath::Property("Y")), /* superPath */ Path(TypePath::Property("Y"))}, /* variance */ SubtypingVariance::Invariant},
SubtypingReasoning{/* subPath */ Path(TypePath::Property("Y")), /* superPath */ Path(TypePath::Property("Y")),
/* variance */ SubtypingVariance::Invariant},
}); });
} }

View File

@ -938,7 +938,7 @@ TEST_CASE_FIXTURE(Fixture, "tostring_error_mismatch")
//clang-format off //clang-format off
std::string expected = std::string expected =
(FFlag::DebugLuauDeferredConstraintResolution) (FFlag::DebugLuauDeferredConstraintResolution)
? R"(Type pack '{| a: number, b: string, c: {| d: string |} |}' could not be converted into '{ a: number, b: string, c: { d: number } }'; at [0]["c"]["d"], string is not a subtype of number)" ? R"(Type pack '{| a: number, b: string, c: {| d: string |} |}' could not be converted into '{ a: number, b: string, c: { d: number } }'; at [0]["c"]["d"], string is not exactly number)"
: :
R"(Type R"(Type
'{ a: number, b: string, c: { d: string } }' '{ a: number, b: string, c: { d: string } }'

View File

@ -198,8 +198,7 @@ TEST_CASE_FIXTURE(Fixture, "generic_aliases")
)"); )");
LUAU_REQUIRE_ERROR_COUNT(1, result); LUAU_REQUIRE_ERROR_COUNT(1, result);
const std::string expected = const std::string expected = R"(Type 'bad' could not be converted into 'T<number>'; at ["v"], string is not exactly number)";
R"(Type 'bad' could not be converted into 'T<number>'; at ["v"], string is not a subtype of number)";
CHECK(result.errors[0].location == Location{{4, 31}, {4, 44}}); CHECK(result.errors[0].location == Location{{4, 31}, {4, 44}});
CHECK_EQ(expected, toString(result.errors[0])); CHECK_EQ(expected, toString(result.errors[0]));
} }
@ -218,8 +217,7 @@ TEST_CASE_FIXTURE(Fixture, "dependent_generic_aliases")
)"); )");
LUAU_REQUIRE_ERROR_COUNT(1, result); LUAU_REQUIRE_ERROR_COUNT(1, result);
const std::string expected = const std::string expected = R"(Type 'bad' could not be converted into 'U<number>'; at ["t"]["v"], string is not exactly number)";
R"(Type 'bad' could not be converted into 'U<number>'; at ["t"]["v"], string is not a subtype of number)";
CHECK(result.errors[0].location == Location{{4, 31}, {4, 52}}); CHECK(result.errors[0].location == Location{{4, 31}, {4, 52}});
CHECK_EQ(expected, toString(result.errors[0])); CHECK_EQ(expected, toString(result.errors[0]));

View File

@ -50,8 +50,16 @@ TEST_CASE_FIXTURE(Fixture, "for_in_loop_iterator_returns_any2")
LUAU_REQUIRE_NO_ERRORS(result); 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_EQ("any", toString(requireType("a")));
} }
}
TEST_CASE_FIXTURE(Fixture, "for_in_loop_iterator_is_any") TEST_CASE_FIXTURE(Fixture, "for_in_loop_iterator_is_any")
{ {

View File

@ -111,7 +111,7 @@ TEST_CASE_FIXTURE(ClassFixture, "we_can_report_when_someone_is_trying_to_use_a_t
)"); )");
LUAU_REQUIRE_ERROR_COUNT(1, result); LUAU_REQUIRE_ERROR_COUNT(1, result);
TypeMismatch* tm = get<TypeMismatch>(result.errors[0]); TypeMismatch* tm = get<TypeMismatch>(result.errors.at(0));
REQUIRE(tm != nullptr); REQUIRE(tm != nullptr);
CHECK_EQ("Oopsies", toString(tm->givenType)); CHECK_EQ("Oopsies", toString(tm->givenType));
@ -186,7 +186,7 @@ TEST_CASE_FIXTURE(ClassFixture, "warn_when_prop_almost_matches")
LUAU_REQUIRE_ERROR_COUNT(1, result); LUAU_REQUIRE_ERROR_COUNT(1, result);
auto err = get<UnknownPropButFoundLikeProp>(result.errors[0]); auto err = get<UnknownPropButFoundLikeProp>(result.errors.at(0));
REQUIRE(err != nullptr); REQUIRE(err != nullptr);
REQUIRE_EQ(1, err->candidates.size()); REQUIRE_EQ(1, err->candidates.size());
@ -290,7 +290,7 @@ TEST_CASE_FIXTURE(ClassFixture, "table_properties_are_invariant")
)"); )");
LUAU_REQUIRE_ERROR_COUNT(2, result); LUAU_REQUIRE_ERROR_COUNT(2, result);
CHECK_EQ(6, result.errors[0].location.begin.line); CHECK_EQ(6, result.errors.at(0).location.begin.line);
CHECK_EQ(13, result.errors[1].location.begin.line); CHECK_EQ(13, result.errors[1].location.begin.line);
} }
@ -313,7 +313,7 @@ TEST_CASE_FIXTURE(ClassFixture, "table_indexers_are_invariant")
)"); )");
LUAU_REQUIRE_ERROR_COUNT(2, result); LUAU_REQUIRE_ERROR_COUNT(2, result);
CHECK_EQ(6, result.errors[0].location.begin.line); CHECK_EQ(6, result.errors.at(0).location.begin.line);
CHECK_EQ(13, result.errors[1].location.begin.line); CHECK_EQ(13, result.errors[1].location.begin.line);
} }
@ -331,7 +331,7 @@ TEST_CASE_FIXTURE(ClassFixture, "table_class_unification_reports_sane_errors_for
)"); )");
LUAU_REQUIRE_ERROR_COUNT(2, result); LUAU_REQUIRE_ERROR_COUNT(2, result);
REQUIRE_EQ("Key 'w' not found in class 'Vector2'", toString(result.errors[0])); REQUIRE_EQ("Key 'w' not found in class 'Vector2'", toString(result.errors.at(0)));
REQUIRE_EQ("Key 'x' not found in class 'Vector2'. Did you mean 'X'?", toString(result.errors[1])); REQUIRE_EQ("Key 'x' not found in class 'Vector2'. Did you mean 'X'?", toString(result.errors[1]));
} }
@ -345,7 +345,7 @@ TEST_CASE_FIXTURE(ClassFixture, "class_unification_type_mismatch_is_correct_orde
LUAU_REQUIRE_ERROR_COUNT(2, result); LUAU_REQUIRE_ERROR_COUNT(2, result);
REQUIRE_EQ("Type 'BaseClass' could not be converted into 'number'", toString(result.errors[0])); REQUIRE_EQ("Type 'BaseClass' could not be converted into 'number'", toString(result.errors.at(0)));
REQUIRE_EQ("Type 'number' could not be converted into 'BaseClass'", toString(result.errors[1])); REQUIRE_EQ("Type 'number' could not be converted into 'BaseClass'", toString(result.errors[1]));
} }
@ -359,7 +359,7 @@ b.X = 2 -- real Vector2.X is also read-only
)"); )");
LUAU_REQUIRE_ERROR_COUNT(4, result); LUAU_REQUIRE_ERROR_COUNT(4, result);
CHECK_EQ("Value of type 'Vector2?' could be nil", toString(result.errors[0])); CHECK_EQ("Value of type 'Vector2?' could be nil", toString(result.errors.at(0)));
CHECK_EQ("Value of type 'Vector2?' could be nil", toString(result.errors[1])); CHECK_EQ("Value of type 'Vector2?' could be nil", toString(result.errors[1]));
CHECK_EQ("Key 'Z' not found in class 'Vector2'", toString(result.errors[2])); CHECK_EQ("Key 'Z' not found in class 'Vector2'", toString(result.errors[2]));
CHECK_EQ("Value of type 'Vector2?' could be nil", toString(result.errors[3])); CHECK_EQ("Value of type 'Vector2?' could be nil", toString(result.errors[3]));
@ -385,7 +385,7 @@ b(a)
caused by: caused by:
Property 'Y' is not compatible. Property 'Y' is not compatible.
Type 'number' could not be converted into 'string')"; Type 'number' could not be converted into 'string')";
CHECK_EQ(expected, toString(result.errors[0])); CHECK_EQ(expected, toString(result.errors.at(0)));
} }
TEST_CASE_FIXTURE(ClassFixture, "class_type_mismatch_with_name_conflict") TEST_CASE_FIXTURE(ClassFixture, "class_type_mismatch_with_name_conflict")
@ -397,7 +397,7 @@ local a: ChildClass = i
)"); )");
LUAU_REQUIRE_ERROR_COUNT(1, result); LUAU_REQUIRE_ERROR_COUNT(1, result);
CHECK_EQ("Type 'ChildClass' from 'Test' could not be converted into 'ChildClass' from 'MainModule'", toString(result.errors[0])); CHECK_EQ("Type 'ChildClass' from 'Test' could not be converted into 'ChildClass' from 'MainModule'", toString(result.errors.at(0)));
} }
TEST_CASE_FIXTURE(ClassFixture, "intersections_of_unions_of_classes") TEST_CASE_FIXTURE(ClassFixture, "intersections_of_unions_of_classes")
@ -433,7 +433,7 @@ TEST_CASE_FIXTURE(ClassFixture, "index_instance_property")
)"); )");
LUAU_REQUIRE_ERROR_COUNT(1, result); LUAU_REQUIRE_ERROR_COUNT(1, result);
CHECK_EQ("Attempting a dynamic property access on type 'BaseClass' is unsafe and may cause exceptions at runtime", toString(result.errors[0])); CHECK_EQ("Attempting a dynamic property access on type 'BaseClass' is unsafe and may cause exceptions at runtime", toString(result.errors.at(0)));
} }
TEST_CASE_FIXTURE(ClassFixture, "index_instance_property_nonstrict") TEST_CASE_FIXTURE(ClassFixture, "index_instance_property_nonstrict")
@ -455,16 +455,22 @@ TEST_CASE_FIXTURE(ClassFixture, "type_mismatch_invariance_required_for_error")
type A = { x: ChildClass } type A = { x: ChildClass }
type B = { x: BaseClass } type B = { x: BaseClass }
local a: A local a: A = { x = ChildClass.New() }
local b: B = a local b: B = a
)"); )");
LUAU_REQUIRE_ERRORS(result); LUAU_REQUIRE_ERRORS(result);
if (FFlag::DebugLuauDeferredConstraintResolution)
CHECK(toString(result.errors.at(0)) == "Type 'a' could not be converted into 'B'; at [\"x\"], ChildClass is not exactly BaseClass");
else
{
const std::string expected = R"(Type 'A' could not be converted into 'B' const std::string expected = R"(Type 'A' could not be converted into 'B'
caused by: caused by:
Property 'x' is not compatible. Property 'x' is not compatible.
Type 'ChildClass' could not be converted into 'BaseClass' in an invariant context)"; Type 'ChildClass' could not be converted into 'BaseClass' in an invariant context)";
CHECK_EQ(expected, toString(result.errors[0])); CHECK_EQ(expected, toString(result.errors.at(0)));
}
} }
TEST_CASE_FIXTURE(ClassFixture, "callable_classes") TEST_CASE_FIXTURE(ClassFixture, "callable_classes")
@ -551,7 +557,7 @@ TEST_CASE_FIXTURE(ClassFixture, "indexable_classes")
CHECK_EQ( CHECK_EQ(
toString(result.errors[0]), "Type 'boolean' could not be converted into 'number | string'; none of the union options are compatible"); toString(result.errors.at(0)), "Type 'boolean' could not be converted into 'number | string'; none of the union options are compatible");
} }
{ {
CheckResult result = check(R"( CheckResult result = check(R"(
@ -560,7 +566,7 @@ TEST_CASE_FIXTURE(ClassFixture, "indexable_classes")
)"); )");
CHECK_EQ( CHECK_EQ(
toString(result.errors[0]), "Type 'boolean' could not be converted into 'number | string'; none of the union options are compatible"); toString(result.errors.at(0)), "Type 'boolean' could not be converted into 'number | string'; none of the union options are compatible");
} }
// Test type checking for the return type of the indexer (i.e. a number) // Test type checking for the return type of the indexer (i.e. a number)
@ -569,14 +575,14 @@ TEST_CASE_FIXTURE(ClassFixture, "indexable_classes")
local x : IndexableClass local x : IndexableClass
x.key = "string value" x.key = "string value"
)"); )");
CHECK_EQ(toString(result.errors[0]), "Type 'string' could not be converted into 'number'"); CHECK_EQ(toString(result.errors.at(0)), "Type 'string' could not be converted into 'number'");
} }
{ {
CheckResult result = check(R"( CheckResult result = check(R"(
local x : IndexableClass local x : IndexableClass
local str : string = x.key local str : string = x.key
)"); )");
CHECK_EQ(toString(result.errors[0]), "Type 'number' could not be converted into 'string'"); CHECK_EQ(toString(result.errors.at(0)), "Type 'number' could not be converted into 'string'");
} }
// Check that we string key are rejected if the indexer's key type is not compatible with string // Check that we string key are rejected if the indexer's key type is not compatible with string
@ -593,9 +599,9 @@ TEST_CASE_FIXTURE(ClassFixture, "indexable_classes")
x["key"] = 1 x["key"] = 1
)"); )");
if (FFlag::DebugLuauDeferredConstraintResolution) if (FFlag::DebugLuauDeferredConstraintResolution)
CHECK_EQ(toString(result.errors[0]), "Key 'key' not found in class 'IndexableNumericKeyClass'"); CHECK_EQ(toString(result.errors.at(0)), "Key 'key' not found in class 'IndexableNumericKeyClass'");
else else
CHECK_EQ(toString(result.errors[0]), "Type 'string' could not be converted into 'number'"); CHECK_EQ(toString(result.errors.at(0)), "Type 'string' could not be converted into 'number'");
} }
{ {
CheckResult result = check(R"( CheckResult result = check(R"(
@ -603,14 +609,14 @@ TEST_CASE_FIXTURE(ClassFixture, "indexable_classes")
local str : string local str : string
x[str] = 1 -- Index with a non-const string x[str] = 1 -- Index with a non-const string
)"); )");
CHECK_EQ(toString(result.errors[0]), "Type 'string' could not be converted into 'number'"); CHECK_EQ(toString(result.errors.at(0)), "Type 'string' could not be converted into 'number'");
} }
{ {
CheckResult result = check(R"( CheckResult result = check(R"(
local x : IndexableNumericKeyClass local x : IndexableNumericKeyClass
local y = x.key local y = x.key
)"); )");
CHECK_EQ(toString(result.errors[0]), "Key 'key' not found in class 'IndexableNumericKeyClass'"); CHECK_EQ(toString(result.errors.at(0)), "Key 'key' not found in class 'IndexableNumericKeyClass'");
} }
{ {
CheckResult result = check(R"( CheckResult result = check(R"(
@ -618,9 +624,9 @@ TEST_CASE_FIXTURE(ClassFixture, "indexable_classes")
local y = x["key"] local y = x["key"]
)"); )");
if (FFlag::DebugLuauDeferredConstraintResolution) if (FFlag::DebugLuauDeferredConstraintResolution)
CHECK_EQ(toString(result.errors[0]), "Key 'key' not found in class 'IndexableNumericKeyClass'"); CHECK_EQ(toString(result.errors.at(0)), "Key 'key' not found in class 'IndexableNumericKeyClass'");
else else
CHECK_EQ(toString(result.errors[0]), "Type 'string' could not be converted into 'number'"); CHECK_EQ(toString(result.errors.at(0)), "Type 'string' could not be converted into 'number'");
} }
{ {
CheckResult result = check(R"( CheckResult result = check(R"(
@ -628,7 +634,7 @@ TEST_CASE_FIXTURE(ClassFixture, "indexable_classes")
local str : string local str : string
local y = x[str] -- Index with a non-const string local y = x[str] -- Index with a non-const string
)"); )");
CHECK_EQ(toString(result.errors[0]), "Type 'string' could not be converted into 'number'"); CHECK_EQ(toString(result.errors.at(0)), "Type 'string' could not be converted into 'number'");
} }
} }

View File

@ -725,6 +725,12 @@ y.a.c = y
)"); )");
LUAU_REQUIRE_ERRORS(result); LUAU_REQUIRE_ERRORS(result);
if (FFlag::DebugLuauDeferredConstraintResolution)
CHECK(toString(result.errors.at(0)) ==
R"(Type 'x' could not be converted into 'T<number>'; type x["a"]["c"] (nil) is not exactly T<number>["a"]["c"][0] (T<number>))");
else
{
const std::string expected = R"(Type 'y' could not be converted into 'T<string>' const std::string expected = R"(Type 'y' could not be converted into 'T<string>'
caused by: caused by:
Property 'a' is not compatible. Property 'a' is not compatible.
@ -734,6 +740,7 @@ caused by:
Type 'number' could not be converted into 'string' in an invariant context)"; Type 'number' could not be converted into 'string' in an invariant context)";
CHECK_EQ(expected, toString(result.errors[0])); CHECK_EQ(expected, toString(result.errors[0]));
} }
}
TEST_CASE_FIXTURE(Fixture, "generic_type_pack_unification1") TEST_CASE_FIXTURE(Fixture, "generic_type_pack_unification1")
{ {

View File

@ -529,7 +529,7 @@ could not be converted into
TEST_CASE_FIXTURE(Fixture, "intersection_of_tables_with_top_properties") TEST_CASE_FIXTURE(Fixture, "intersection_of_tables_with_top_properties")
{ {
CheckResult result = check(R"( CheckResult result = check(R"(
local x : { p : number?, q : any } & { p : unknown, q : string? } local x : { p : number?, q : any } & { p : unknown, q : string? } = { p = 123, q = "foo" }
local y : { p : number?, q : string? } = x -- OK local y : { p : number?, q : string? } = x -- OK
local z : { p : string?, q : number? } = x -- Not OK local z : { p : string?, q : number? } = x -- Not OK
)"); )");

View File

@ -410,13 +410,19 @@ local b: B.T = a
)"; )";
CheckResult result = frontend.check("game/C"); CheckResult result = frontend.check("game/C");
LUAU_REQUIRE_ERROR_COUNT(1, result);
if (FFlag::DebugLuauDeferredConstraintResolution)
CHECK(toString(result.errors.at(0)) == "Type 'a' could not be converted into 'T'; at [\"x\"], number is not exactly string");
else
{
const std::string expected = R"(Type 'T' from 'game/A' could not be converted into 'T' from 'game/B' const std::string expected = R"(Type 'T' from 'game/A' could not be converted into 'T' from 'game/B'
caused by: caused by:
Property 'x' is not compatible. Property 'x' is not compatible.
Type 'number' could not be converted into 'string' in an invariant context)"; Type 'number' could not be converted into 'string' in an invariant context)";
LUAU_REQUIRE_ERROR_COUNT(1, result);
CHECK_EQ(expected, toString(result.errors[0])); CHECK_EQ(expected, toString(result.errors[0]));
} }
}
TEST_CASE_FIXTURE(BuiltinsFixture, "module_type_conflict_instantiated") TEST_CASE_FIXTURE(BuiltinsFixture, "module_type_conflict_instantiated")
{ {
@ -445,13 +451,19 @@ local b: B.T = a
)"; )";
CheckResult result = frontend.check("game/D"); CheckResult result = frontend.check("game/D");
LUAU_REQUIRE_ERROR_COUNT(1, result);
if (FFlag::DebugLuauDeferredConstraintResolution)
CHECK(toString(result.errors.at(0)) == "Type 'a' could not be converted into 'T'; at [\"x\"], number is not exactly string");
else
{
const std::string expected = R"(Type 'T' from 'game/B' could not be converted into 'T' from 'game/C' const std::string expected = R"(Type 'T' from 'game/B' could not be converted into 'T' from 'game/C'
caused by: caused by:
Property 'x' is not compatible. Property 'x' is not compatible.
Type 'number' could not be converted into 'string' in an invariant context)"; Type 'number' could not be converted into 'string' in an invariant context)";
LUAU_REQUIRE_ERROR_COUNT(1, result);
CHECK_EQ(expected, toString(result.errors[0])); CHECK_EQ(expected, toString(result.errors[0]));
} }
}
TEST_CASE_FIXTURE(BuiltinsFixture, "constrained_anyification_clone_immutable_types") TEST_CASE_FIXTURE(BuiltinsFixture, "constrained_anyification_clone_immutable_types")
{ {

View File

@ -1939,9 +1939,18 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "refine_unknown_to_table")
LUAU_REQUIRE_NO_ERRORS(result); LUAU_REQUIRE_NO_ERRORS(result);
if (FFlag::DebugLuauDeferredConstraintResolution)
{
// Bug: We do not simplify at the right time
CHECK_EQ("unknown?", toString(requireType("idx")));
CHECK_EQ("unknown?", toString(requireType("val")));
}
else
{
CHECK_EQ("unknown", toString(requireType("idx"))); CHECK_EQ("unknown", toString(requireType("idx")));
CHECK_EQ("unknown", toString(requireType("val"))); CHECK_EQ("unknown", toString(requireType("val")));
} }
}
TEST_CASE_FIXTURE(BuiltinsFixture, "conditional_refinement_should_stay_error_suppressing") TEST_CASE_FIXTURE(BuiltinsFixture, "conditional_refinement_should_stay_error_suppressing")
{ {

View File

@ -367,8 +367,8 @@ TEST_CASE_FIXTURE(Fixture, "parametric_tagged_union_alias")
LUAU_REQUIRE_ERROR_COUNT(1, result); LUAU_REQUIRE_ERROR_COUNT(1, result);
const std::string expectedError = const std::string expectedError =
"Type 'a' could not be converted into 'Err<number> | Ok<string>'; type a (a) is not a subtype of Err<number> | Ok<string>[1] (Err<number>)" "Type 'a' could not be converted into 'Err<number> | Ok<string>'; type a (a) is not a subtype of Err<number> | Ok<string>[1] (Err<number>)\n"
"\n\ttype a[\"success\"] (false) is not a subtype of Err<number> | Ok<string>[0][\"success\"] (true)"; "\ttype a[\"success\"] (false) is not exactly Err<number> | Ok<string>[0][\"success\"] (true)";
CHECK(toString(result.errors[0]) == expectedError); CHECK(toString(result.errors[0]) == expectedError);
} }

View File

@ -2147,17 +2147,23 @@ TEST_CASE_FIXTURE(Fixture, "error_detailed_prop")
type A = { x: number, y: number } type A = { x: number, y: number }
type B = { x: number, y: string } type B = { x: number, y: string }
local a: A local a: A = { x = 123, y = 456 }
local b: B = a local b: B = a
)"); )");
LUAU_REQUIRE_ERRORS(result); LUAU_REQUIRE_ERRORS(result);
if (FFlag::DebugLuauDeferredConstraintResolution)
CHECK(toString(result.errors.at(0)) == R"(Type 'a' could not be converted into 'B'; at ["y"], number is not exactly string)");
else
{
const std::string expected = R"(Type 'A' could not be converted into 'B' const std::string expected = R"(Type 'A' could not be converted into 'B'
caused by: caused by:
Property 'y' is not compatible. Property 'y' is not compatible.
Type 'number' could not be converted into 'string' in an invariant context)"; Type 'number' could not be converted into 'string' in an invariant context)";
CHECK_EQ(expected, toString(result.errors[0])); CHECK_EQ(expected, toString(result.errors[0]));
} }
}
TEST_CASE_FIXTURE(Fixture, "error_detailed_prop_nested") TEST_CASE_FIXTURE(Fixture, "error_detailed_prop_nested")
{ {
@ -2168,11 +2174,16 @@ type BS = { x: number, y: string }
type A = { a: boolean, b: AS } type A = { a: boolean, b: AS }
type B = { a: boolean, b: BS } type B = { a: boolean, b: BS }
local a: A local a: A = { a = false, b = { x = 123, y = 456 } }
local b: B = a local b: B = a
)"); )");
LUAU_REQUIRE_ERRORS(result); LUAU_REQUIRE_ERRORS(result);
if (FFlag::DebugLuauDeferredConstraintResolution)
CHECK(toString(result.errors.at(0)) == R"(Type 'a' could not be converted into 'B'; at ["b"]["y"], number is not exactly string)");
else
{
const std::string expected = R"(Type 'A' could not be converted into 'B' const std::string expected = R"(Type 'A' could not be converted into 'B'
caused by: caused by:
Property 'b' is not compatible. Property 'b' is not compatible.
@ -2182,6 +2193,7 @@ caused by:
Type 'number' could not be converted into 'string' in an invariant context)"; Type 'number' could not be converted into 'string' in an invariant context)";
CHECK_EQ(expected, toString(result.errors[0])); CHECK_EQ(expected, toString(result.errors[0]));
} }
}
TEST_CASE_FIXTURE(BuiltinsFixture, "error_detailed_metatable_prop") TEST_CASE_FIXTURE(BuiltinsFixture, "error_detailed_metatable_prop")
{ {
@ -3945,9 +3957,9 @@ TEST_CASE_FIXTURE(Fixture, "identify_all_problematic_table_fields")
LUAU_REQUIRE_ERROR_COUNT(1, result); LUAU_REQUIRE_ERROR_COUNT(1, result);
std::string expected = "Type 'a' could not be converted into 'T'; at [\"a\"], string is not a subtype of number" std::string expected = "Type 'a' could not be converted into 'T'; at [\"a\"], string is not exactly number"
"\n\tat [\"b\"], boolean is not a subtype of string" "\n\tat [\"b\"], boolean is not exactly string"
"\n\tat [\"c\"], number is not a subtype of boolean"; "\n\tat [\"c\"], number is not exactly boolean";
CHECK(toString(result.errors[0]) == expected); CHECK(toString(result.errors[0]) == expected);
} }

View File

@ -28,8 +28,7 @@ TEST_CASE_FIXTURE(Fixture, "tc_hello_world")
CheckResult result = check("local a = 7"); CheckResult result = check("local a = 7");
LUAU_REQUIRE_NO_ERRORS(result); LUAU_REQUIRE_NO_ERRORS(result);
TypeId aType = requireType("a"); CHECK("number" == toString(requireType("a")));
CHECK_EQ(getPrimitiveType(aType), PrimitiveType::Number);
} }
TEST_CASE_FIXTURE(Fixture, "tc_propagation") TEST_CASE_FIXTURE(Fixture, "tc_propagation")
@ -44,15 +43,32 @@ TEST_CASE_FIXTURE(Fixture, "tc_propagation")
TEST_CASE_FIXTURE(Fixture, "tc_error") TEST_CASE_FIXTURE(Fixture, "tc_error")
{ {
CheckResult result = check("local a = 7 local b = 'hi' a = b"); CheckResult result = check("local a = 7 local b = 'hi' a = b");
if (FFlag::DebugLuauDeferredConstraintResolution)
{
LUAU_REQUIRE_NO_ERRORS(result);
CHECK("number | string" == toString(requireType("a")));
}
else
{
LUAU_REQUIRE_ERROR_COUNT(1, result); LUAU_REQUIRE_ERROR_COUNT(1, result);
CHECK_EQ( CHECK_EQ(
result.errors[0], (TypeError{Location{Position{0, 35}, Position{0, 36}}, TypeMismatch{builtinTypes->numberType, builtinTypes->stringType}})); result.errors[0], (TypeError{Location{Position{0, 35}, Position{0, 36}}, TypeMismatch{builtinTypes->numberType, builtinTypes->stringType}}));
} }
}
TEST_CASE_FIXTURE(Fixture, "tc_error_2") TEST_CASE_FIXTURE(Fixture, "tc_error_2")
{ {
CheckResult result = check("local a = 7 a = 'hi'"); CheckResult result = check("local a = 7 a = 'hi'");
if (FFlag::DebugLuauDeferredConstraintResolution)
{
LUAU_REQUIRE_NO_ERRORS(result);
CHECK("number | string" == toString(requireType("a")));
}
else
{
LUAU_REQUIRE_ERROR_COUNT(1, result); LUAU_REQUIRE_ERROR_COUNT(1, result);
CHECK_EQ(result.errors[0], (TypeError{Location{Position{0, 18}, Position{0, 22}}, TypeMismatch{ CHECK_EQ(result.errors[0], (TypeError{Location{Position{0, 18}, Position{0, 22}}, TypeMismatch{
@ -60,15 +76,23 @@ TEST_CASE_FIXTURE(Fixture, "tc_error_2")
builtinTypes->stringType, builtinTypes->stringType,
}})); }}));
} }
}
TEST_CASE_FIXTURE(Fixture, "infer_locals_with_nil_value") TEST_CASE_FIXTURE(Fixture, "infer_locals_with_nil_value")
{ {
CheckResult result = check("local f = nil; f = 'hello world'"); CheckResult result = check("local f = nil; f = 'hello world'");
LUAU_REQUIRE_NO_ERRORS(result); LUAU_REQUIRE_NO_ERRORS(result);
if (FFlag::DebugLuauDeferredConstraintResolution)
{
CHECK("string?" == toString(requireType("f")));
}
else
{
TypeId ty = requireType("f"); TypeId ty = requireType("f");
CHECK_EQ(getPrimitiveType(ty), PrimitiveType::String); CHECK_EQ(getPrimitiveType(ty), PrimitiveType::String);
} }
}
TEST_CASE_FIXTURE(Fixture, "infer_locals_with_nil_value_2") TEST_CASE_FIXTURE(Fixture, "infer_locals_with_nil_value_2")
{ {
@ -93,8 +117,8 @@ TEST_CASE_FIXTURE(Fixture, "infer_locals_via_assignment_from_its_call_site")
if (FFlag::DebugLuauDeferredConstraintResolution) if (FFlag::DebugLuauDeferredConstraintResolution)
{ {
CHECK("number | string" == toString(requireType("a"))); CHECK("unknown" == toString(requireType("a")));
CHECK("(number | string) -> ()" == toString(requireType("f"))); CHECK("(unknown) -> ()" == toString(requireType("f")));
LUAU_REQUIRE_NO_ERRORS(result); LUAU_REQUIRE_NO_ERRORS(result);
} }
@ -105,27 +129,6 @@ TEST_CASE_FIXTURE(Fixture, "infer_locals_via_assignment_from_its_call_site")
CHECK_EQ("number", toString(requireType("a"))); CHECK_EQ("number", toString(requireType("a")));
} }
} }
TEST_CASE_FIXTURE(Fixture, "interesting_local_type_inference_case")
{
if (!FFlag::DebugLuauDeferredConstraintResolution)
return;
ScopedFastFlag sff[] = {
{"DebugLuauDeferredConstraintResolution", true},
};
CheckResult result = check(R"(
local a
function f(x) a = x end
f({x = 5})
f({x = 5})
)");
CHECK("{ x: number }" == toString(requireType("a")));
CHECK("({ x: number }) -> ()" == toString(requireType("f")));
LUAU_REQUIRE_NO_ERRORS(result);
}
TEST_CASE_FIXTURE(Fixture, "infer_in_nocheck_mode") TEST_CASE_FIXTURE(Fixture, "infer_in_nocheck_mode")
{ {
@ -178,9 +181,17 @@ TEST_CASE_FIXTURE(Fixture, "if_statement")
LUAU_REQUIRE_NO_ERRORS(result); LUAU_REQUIRE_NO_ERRORS(result);
if (FFlag::DebugLuauDeferredConstraintResolution)
{
CHECK("string?" == toString(requireType("a")));
CHECK("number?" == toString(requireType("b")));
}
else
{
CHECK_EQ(*builtinTypes->stringType, *requireType("a")); CHECK_EQ(*builtinTypes->stringType, *requireType("a"));
CHECK_EQ(*builtinTypes->numberType, *requireType("b")); CHECK_EQ(*builtinTypes->numberType, *requireType("b"));
} }
}
TEST_CASE_FIXTURE(Fixture, "statements_are_topologically_sorted") TEST_CASE_FIXTURE(Fixture, "statements_are_topologically_sorted")
{ {

View File

@ -274,4 +274,45 @@ TEST_CASE_FIXTURE(TypeStateFixture, "then_branch_assigns_but_is_met_with_return_
CHECK("string?" == toString(requireType("y"))); CHECK("string?" == toString(requireType("y")));
} }
TEST_CASE_FIXTURE(TypeStateFixture, "invalidate_type_refinements_upon_assignments")
{
CheckResult result = check(R"(
type Ok<T> = { tag: "ok", val: T }
type Err<E> = { tag: "err", err: E }
type Result<T, E> = Ok<T> | Err<E>
local function f<T, E>(res: Result<T, E>)
assert(res.tag == "ok")
local tag: "ok", val: T = res.tag, res.val
res = { tag = "err" :: "err", err = (5 :: any) :: E }
local tag: "err", err: E = res.tag, res.err
end
)");
LUAU_REQUIRE_NO_ERRORS(result);
}
TEST_CASE_FIXTURE(TypeStateFixture, "local_t_is_assigned_a_fresh_table_with_x_assigned_a_union_and_then_assert_restricts_actual_outflow_of_types")
{
CheckResult result = check(R"(
local t = nil
if math.random() > 0.5 then
t = {}
t.x = if math.random() > 0.5 then 5 else "hello"
assert(typeof(t.x) == "string")
else
t = {}
t.x = if math.random() > 0.5 then 7 else true
assert(typeof(t.x) == "boolean")
end
local x = t.x
)");
LUAU_REQUIRE_NO_ERRORS(result);
// CHECK("boolean | string" == toString(requireType("x")));
CHECK("boolean | number | number | string" == toString(requireType("x")));
}
TEST_SUITE_END(); TEST_SUITE_END();

View File

@ -586,6 +586,15 @@ local function misc(t16)
buffer.writei32(b, #t16, 10) buffer.writei32(b, #t16, 10)
assert(buffer.readi32(b, 16) == 10) assert(buffer.readi32(b, 16) == 10)
buffer.writeu8(b, 100, 0xff)
buffer.writeu8(b, 110, 0x80)
assert(buffer.readu32(b, 100) == 255)
assert(buffer.readu32(b, 110) == 128)
buffer.writeu16(b, 200, 0xffff)
buffer.writeu16(b, 210, 0x8000)
assert(buffer.readu32(b, 200) == 65535)
assert(buffer.readu32(b, 210) == 32768)
end end
misc(table.create(16, 0)) misc(table.create(16, 0))

View File

@ -275,4 +275,22 @@ end
assert(arrayIndexingSpecialNumbers1(1, 256, 65536) == 3456789) assert(arrayIndexingSpecialNumbers1(1, 256, 65536) == 3456789)
function loopIteratorProtocol(a, t)
local sum = 0
do
local a, b, c, d, e, f, g = {}, {}, {}, {}, {}, {}, {}
end
for k, v in ipairs(t) do
if k == 10 then sum += math.abs('-8') end
sum += k
end
return sum
end
assert(loopIteratorProtocol(0, table.create(100, 5)) == 5058)
return('OK') return('OK')

View File

@ -13,7 +13,6 @@ AutocompleteTest.autocomplete_string_singleton_equality
AutocompleteTest.autocomplete_string_singleton_escape AutocompleteTest.autocomplete_string_singleton_escape
AutocompleteTest.autocomplete_string_singletons AutocompleteTest.autocomplete_string_singletons
AutocompleteTest.do_wrong_compatible_nonself_calls AutocompleteTest.do_wrong_compatible_nonself_calls
AutocompleteTest.frontend_use_correct_global_scope
AutocompleteTest.no_incompatible_self_calls_on_class AutocompleteTest.no_incompatible_self_calls_on_class
AutocompleteTest.string_singleton_in_if_statement AutocompleteTest.string_singleton_in_if_statement
AutocompleteTest.suggest_external_module_type AutocompleteTest.suggest_external_module_type
@ -165,7 +164,6 @@ GenericsTests.generic_argument_count_too_many
GenericsTests.generic_factories GenericsTests.generic_factories
GenericsTests.generic_functions_dont_cache_type_parameters GenericsTests.generic_functions_dont_cache_type_parameters
GenericsTests.generic_functions_in_types GenericsTests.generic_functions_in_types
GenericsTests.generic_functions_should_be_memory_safe
GenericsTests.generic_type_pack_parentheses GenericsTests.generic_type_pack_parentheses
GenericsTests.generic_type_pack_unification1 GenericsTests.generic_type_pack_unification1
GenericsTests.generic_type_pack_unification2 GenericsTests.generic_type_pack_unification2
@ -234,7 +232,6 @@ Linter.DeprecatedApiFenv
Linter.FormatStringTyped Linter.FormatStringTyped
Linter.TableOperationsIndexer Linter.TableOperationsIndexer
ModuleTests.clone_self_property ModuleTests.clone_self_property
Negations.cofinite_strings_can_be_compared_for_equality
Negations.negated_string_is_a_subtype_of_string Negations.negated_string_is_a_subtype_of_string
NonstrictModeTests.inconsistent_module_return_types_are_ok NonstrictModeTests.inconsistent_module_return_types_are_ok
NonstrictModeTests.infer_nullary_function NonstrictModeTests.infer_nullary_function
@ -286,8 +283,6 @@ RefinementTest.function_call_with_colon_after_refining_not_to_be_nil
RefinementTest.impossible_type_narrow_is_not_an_error RefinementTest.impossible_type_narrow_is_not_an_error
RefinementTest.index_on_a_refined_property RefinementTest.index_on_a_refined_property
RefinementTest.isa_type_refinement_must_be_known_ahead_of_time RefinementTest.isa_type_refinement_must_be_known_ahead_of_time
RefinementTest.luau_polyfill_isindexkey_refine_conjunction
RefinementTest.luau_polyfill_isindexkey_refine_conjunction_variant
RefinementTest.merge_should_be_fully_agnostic_of_hashmap_ordering RefinementTest.merge_should_be_fully_agnostic_of_hashmap_ordering
RefinementTest.narrow_property_of_a_bounded_variable RefinementTest.narrow_property_of_a_bounded_variable
RefinementTest.nonoptional_type_can_narrow_to_nil_if_sense_is_true RefinementTest.nonoptional_type_can_narrow_to_nil_if_sense_is_true
@ -340,15 +335,12 @@ TableTests.dont_suggest_exact_match_keys
TableTests.error_detailed_indexer_key TableTests.error_detailed_indexer_key
TableTests.error_detailed_indexer_value TableTests.error_detailed_indexer_value
TableTests.error_detailed_metatable_prop TableTests.error_detailed_metatable_prop
TableTests.error_detailed_prop
TableTests.error_detailed_prop_nested
TableTests.expected_indexer_from_table_union TableTests.expected_indexer_from_table_union
TableTests.expected_indexer_value_type_extra TableTests.expected_indexer_value_type_extra
TableTests.expected_indexer_value_type_extra_2 TableTests.expected_indexer_value_type_extra_2
TableTests.explicitly_typed_table TableTests.explicitly_typed_table
TableTests.explicitly_typed_table_error TableTests.explicitly_typed_table_error
TableTests.explicitly_typed_table_with_indexer TableTests.explicitly_typed_table_with_indexer
TableTests.fuzz_table_unify_instantiated_table_with_prop_realloc
TableTests.generalize_table_argument TableTests.generalize_table_argument
TableTests.generic_table_instantiation_potential_regression TableTests.generic_table_instantiation_potential_regression
TableTests.indexer_mismatch TableTests.indexer_mismatch
@ -420,7 +412,6 @@ TableTests.table_unifies_into_map
TableTests.top_table_type TableTests.top_table_type
TableTests.type_mismatch_on_massive_table_is_cut_short TableTests.type_mismatch_on_massive_table_is_cut_short
TableTests.unification_of_unions_in_a_self_referential_type TableTests.unification_of_unions_in_a_self_referential_type
TableTests.unifying_tables_shouldnt_uaf1
TableTests.used_colon_instead_of_dot TableTests.used_colon_instead_of_dot
TableTests.used_dot_instead_of_colon TableTests.used_dot_instead_of_colon
TableTests.used_dot_instead_of_colon_but_correctly TableTests.used_dot_instead_of_colon_but_correctly
@ -485,14 +476,10 @@ TypeInfer.follow_on_new_types_in_substitution
TypeInfer.globals TypeInfer.globals
TypeInfer.globals2 TypeInfer.globals2
TypeInfer.globals_are_banned_in_strict_mode TypeInfer.globals_are_banned_in_strict_mode
TypeInfer.if_statement
TypeInfer.infer_assignment_value_types TypeInfer.infer_assignment_value_types
TypeInfer.infer_assignment_value_types_mutable_lval
TypeInfer.infer_locals_via_assignment_from_its_call_site TypeInfer.infer_locals_via_assignment_from_its_call_site
TypeInfer.infer_locals_with_nil_value
TypeInfer.infer_through_group_expr TypeInfer.infer_through_group_expr
TypeInfer.infer_type_assertion_value_type TypeInfer.infer_type_assertion_value_type
TypeInfer.interesting_local_type_inference_case
TypeInfer.no_infinite_loop_when_trying_to_unify_uh_this TypeInfer.no_infinite_loop_when_trying_to_unify_uh_this
TypeInfer.no_stack_overflow_from_isoptional TypeInfer.no_stack_overflow_from_isoptional
TypeInfer.promote_tail_type_packs TypeInfer.promote_tail_type_packs
@ -500,8 +487,6 @@ TypeInfer.recursive_function_that_invokes_itself_with_a_refinement_of_its_parame
TypeInfer.recursive_function_that_invokes_itself_with_a_refinement_of_its_parameter_2 TypeInfer.recursive_function_that_invokes_itself_with_a_refinement_of_its_parameter_2
TypeInfer.stringify_nested_unions_with_optionals TypeInfer.stringify_nested_unions_with_optionals
TypeInfer.tc_after_error_recovery_no_replacement_name_in_error TypeInfer.tc_after_error_recovery_no_replacement_name_in_error
TypeInfer.tc_error
TypeInfer.tc_error_2
TypeInfer.tc_if_else_expressions_expected_type_3 TypeInfer.tc_if_else_expressions_expected_type_3
TypeInfer.type_infer_recursion_limit_no_ice TypeInfer.type_infer_recursion_limit_no_ice
TypeInfer.type_infer_recursion_limit_normalizer TypeInfer.type_infer_recursion_limit_normalizer
@ -531,7 +516,6 @@ TypeInferClasses.intersections_of_unions_of_classes
TypeInferClasses.optional_class_field_access_error TypeInferClasses.optional_class_field_access_error
TypeInferClasses.table_class_unification_reports_sane_errors_for_missing_properties TypeInferClasses.table_class_unification_reports_sane_errors_for_missing_properties
TypeInferClasses.table_indexers_are_invariant TypeInferClasses.table_indexers_are_invariant
TypeInferClasses.type_mismatch_invariance_required_for_error
TypeInferClasses.unions_of_intersections_of_classes TypeInferClasses.unions_of_intersections_of_classes
TypeInferClasses.we_can_report_when_someone_is_trying_to_use_a_table_rather_than_a_class TypeInferClasses.we_can_report_when_someone_is_trying_to_use_a_table_rather_than_a_class
TypeInferFunctions.another_other_higher_order_function TypeInferFunctions.another_other_higher_order_function
@ -605,6 +589,7 @@ TypeInferLoops.for_in_loop_on_non_function
TypeInferLoops.for_in_loop_with_custom_iterator TypeInferLoops.for_in_loop_with_custom_iterator
TypeInferLoops.for_in_loop_with_incompatible_args_to_iterator TypeInferLoops.for_in_loop_with_incompatible_args_to_iterator
TypeInferLoops.for_in_loop_with_next TypeInferLoops.for_in_loop_with_next
TypeInferLoops.for_in_with_a_custom_iterator_should_type_check
TypeInferLoops.for_in_with_an_iterator_of_type_any TypeInferLoops.for_in_with_an_iterator_of_type_any
TypeInferLoops.for_in_with_generic_next TypeInferLoops.for_in_with_generic_next
TypeInferLoops.for_in_with_just_one_iterator_is_ok TypeInferLoops.for_in_with_just_one_iterator_is_ok
@ -626,10 +611,7 @@ TypeInferLoops.varlist_declared_by_for_in_loop_should_be_free
TypeInferLoops.while_loop TypeInferLoops.while_loop
TypeInferModules.bound_free_table_export_is_ok TypeInferModules.bound_free_table_export_is_ok
TypeInferModules.do_not_modify_imported_types TypeInferModules.do_not_modify_imported_types
TypeInferModules.do_not_modify_imported_types_4
TypeInferModules.do_not_modify_imported_types_5 TypeInferModules.do_not_modify_imported_types_5
TypeInferModules.module_type_conflict
TypeInferModules.module_type_conflict_instantiated
TypeInferModules.require TypeInferModules.require
TypeInferModules.require_failed_module TypeInferModules.require_failed_module
TypeInferOOP.CheckMethodsOfSealed TypeInferOOP.CheckMethodsOfSealed
@ -680,7 +662,6 @@ TypeInferUnknownNever.index_on_union_of_tables_for_properties_that_is_sorta_neve
TypeInferUnknownNever.length_of_never TypeInferUnknownNever.length_of_never
TypeInferUnknownNever.math_operators_and_never TypeInferUnknownNever.math_operators_and_never
TypeInferUnknownNever.type_packs_containing_never_is_itself_uninhabitable TypeInferUnknownNever.type_packs_containing_never_is_itself_uninhabitable
TypePackTests.fuzz_typepack_iter_follow_2
TypePackTests.pack_tail_unification_check TypePackTests.pack_tail_unification_check
TypePackTests.type_alias_backwards_compatible TypePackTests.type_alias_backwards_compatible
TypePackTests.type_alias_default_type_errors TypePackTests.type_alias_default_type_errors
@ -699,6 +680,8 @@ TypeSingletons.table_properties_singleton_strings
TypeSingletons.table_properties_type_error_escapes TypeSingletons.table_properties_type_error_escapes
TypeSingletons.widen_the_supertype_if_it_is_free_and_subtype_has_singleton TypeSingletons.widen_the_supertype_if_it_is_free_and_subtype_has_singleton
TypeSingletons.widening_happens_almost_everywhere TypeSingletons.widening_happens_almost_everywhere
TypeStatesTest.invalidate_type_refinements_upon_assignments
TypeStatesTest.local_t_is_assigned_a_fresh_table_with_x_assigned_a_union_and_then_assert_restricts_actual_outflow_of_types
UnionTypes.disallow_less_specific_assign UnionTypes.disallow_less_specific_assign
UnionTypes.disallow_less_specific_assign2 UnionTypes.disallow_less_specific_assign2
UnionTypes.error_detailed_optional UnionTypes.error_detailed_optional

View File

@ -113,6 +113,12 @@ def main():
action="store_true", action="store_true",
help="Run the tests with read-write properties enabled.", help="Run the tests with read-write properties enabled.",
) )
parser.add_argument(
"--ts",
dest="suite",
action="store",
help="Only run a specific suite."
)
parser.add_argument("--randomize", action="store_true", help="Pick a random seed") parser.add_argument("--randomize", action="store_true", help="Pick a random seed")
@ -139,6 +145,9 @@ def main():
elif args.randomize: elif args.randomize:
commandLine.append("--randomize") commandLine.append("--randomize")
if args.suite:
commandLine.append(f'--ts={args.suite}')
print_stderr(">", " ".join(commandLine)) print_stderr(">", " ".join(commandLine))
p = sp.Popen( p = sp.Popen(
@ -146,6 +155,8 @@ def main():
stdout=sp.PIPE, stdout=sp.PIPE,
) )
assert p.stdout
handler = Handler(failList) handler = Handler(failList)
if args.dump: if args.dump: