Sync to upstream/release/604 (#1106)

New Solver

* New algorithm for inferring the types of locals that have no
annotations. This
algorithm is very conservative by default, but is augmented with some
control
  flow awareness to handle most common scenarios.
* Fix bugs in type inference of tables
* Improve performance of by switching out standard C++ containers for
`DenseHashMap`
* Infrastructure to support clearer error messages in strict mode

Native Code Generation

* Fix a lowering issue with buffer.writeu8 and 0x80-0xff values: A
constant
  argument wasn't truncated to the target type range and that causes an
  assertion failure in `build.mov`.
* Store full lightuserdata value in loop iteration protocol lowering
* Add analysis to compute function bytecode distribution
* This includes a class to analyze the bytecode operator distribution
per
function and a CLI tool that produces a JSON report. See the new cmake
      target `Luau.Bytecode.CLI`

---------

Co-authored-by: Aaron Weiss <aaronweiss@roblox.com>
Co-authored-by: Alexander McCord <amccord@roblox.com>
Co-authored-by: Andy Friesen <afriesen@roblox.com>
Co-authored-by: Aviral Goel <agoel@roblox.com>
Co-authored-by: Lily Brown <lbrown@roblox.com>
Co-authored-by: Vyacheslav Egorov <vegorov@roblox.com>
This commit is contained in:
Andy Friesen 2023-11-17 10:46:18 -08:00 committed by GitHub
parent 298cd70154
commit 74c532053f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
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: