mirror of
https://github.com/luau-lang/luau.git
synced 2024-11-15 06:15:44 +08:00
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:
parent
298cd70154
commit
74c532053f
@ -190,6 +190,11 @@ struct UnpackConstraint
|
||||
{
|
||||
TypePackId resultPack;
|
||||
TypePackId sourcePack;
|
||||
|
||||
// UnpackConstraint is sometimes used to resolve the types of assignments.
|
||||
// When this is the case, any LocalTypes in resultPack can have their
|
||||
// domains extended by the corresponding type from sourcePack.
|
||||
bool resultIsLValue = false;
|
||||
};
|
||||
|
||||
// resultType ~ refine type mode discriminant
|
||||
|
@ -78,11 +78,13 @@ struct ConstraintGenerator
|
||||
TypeIds types;
|
||||
};
|
||||
|
||||
// During constraint generation, we only populate the Scope::bindings
|
||||
// property for annotated symbols. Unannotated symbols must be handled in a
|
||||
// postprocessing step because we have not yet allocated the types that will
|
||||
// be assigned to those unannotated symbols, so we queue them up here.
|
||||
std::map<Symbol, InferredBinding> inferredBindings;
|
||||
// Some locals have multiple type states. We wish for Scope::bindings to
|
||||
// map each local name onto the union of every type that the local can have
|
||||
// over its lifetime, so we use this map to accumulate the set of types it
|
||||
// might have.
|
||||
//
|
||||
// See the functions recordInferredBinding and fillInInferredBindings.
|
||||
DenseHashMap<Symbol, InferredBinding> inferredBindings{{}};
|
||||
|
||||
// Constraints that go straight to the solver.
|
||||
std::vector<ConstraintPtr> constraints;
|
||||
@ -245,8 +247,6 @@ private:
|
||||
std::optional<TypeId> checkLValue(const ScopePtr& scope, AstExprIndexExpr* indexExpr, TypeId assignedTy);
|
||||
TypeId updateProperty(const ScopePtr& scope, AstExpr* expr, TypeId assignedTy);
|
||||
|
||||
void updateLValueType(AstExpr* lvalue, TypeId ty);
|
||||
|
||||
struct FunctionSignature
|
||||
{
|
||||
// The type of the function.
|
||||
@ -336,6 +336,10 @@ private:
|
||||
*/
|
||||
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);
|
||||
|
||||
/** Given a function type annotation, return a vector describing the expected types of the calls to the function
|
||||
|
@ -77,8 +77,11 @@ struct DfgScope
|
||||
DfgScope* parent;
|
||||
bool isLoopScope;
|
||||
|
||||
DenseHashMap<Symbol, const Def*> bindings{Symbol{}};
|
||||
DenseHashMap<const Def*, std::unordered_map<std::string, const Def*>> props{nullptr};
|
||||
using Bindings = DenseHashMap<Symbol, const Def*>;
|
||||
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(DefId def, const std::string& key) const;
|
||||
@ -115,7 +118,13 @@ private:
|
||||
std::vector<std::unique_ptr<DfgScope>> scopes;
|
||||
|
||||
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 visitBlockWithoutChildScope(DfgScope* scope, AstStatBlock* b);
|
||||
|
@ -80,6 +80,7 @@ struct DefArena
|
||||
|
||||
DefId freshCell(bool subscripted = false);
|
||||
DefId phi(DefId a, DefId b);
|
||||
DefId phi(const std::vector<DefId>& defs);
|
||||
};
|
||||
|
||||
} // namespace Luau
|
||||
|
@ -56,6 +56,7 @@ struct Scope
|
||||
void addBuiltinTypeBinding(const Name& name, const TypeFun& tyFun);
|
||||
|
||||
std::optional<TypeId> lookup(Symbol sym) const;
|
||||
std::optional<TypeId> lookupUnrefinedType(DefId def) const;
|
||||
std::optional<TypeId> lookup(DefId def) const;
|
||||
std::optional<std::pair<TypeId, Scope*>> lookupEx(DefId def);
|
||||
std::optional<std::pair<Binding*, Scope*>> lookupEx(Symbol sym);
|
||||
|
@ -15,10 +15,14 @@ template<typename T, typename Hash = SetHashDefault<T>>
|
||||
class Set
|
||||
{
|
||||
private:
|
||||
DenseHashMap<T, bool, Hash> mapping;
|
||||
using Impl = DenseHashMap<T, bool, Hash>;
|
||||
Impl mapping;
|
||||
size_t entryCount = 0;
|
||||
|
||||
public:
|
||||
class const_iterator;
|
||||
using iterator = const_iterator;
|
||||
|
||||
Set(const T& empty_key)
|
||||
: mapping{empty_key}
|
||||
{
|
||||
@ -83,6 +87,16 @@ public:
|
||||
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
|
||||
{
|
||||
// if the sets are unequal sizes, then they cannot possibly be equal.
|
||||
@ -100,6 +114,58 @@ public:
|
||||
// otherwise, we've proven the two equal!
|
||||
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
|
||||
|
@ -27,10 +27,19 @@ struct TypeArena;
|
||||
struct Scope;
|
||||
struct TableIndexer;
|
||||
|
||||
enum class SubtypingVariance
|
||||
{
|
||||
// Used for an empty key. Should never appear in actual code.
|
||||
Invalid,
|
||||
Covariant,
|
||||
Invariant,
|
||||
};
|
||||
|
||||
struct SubtypingReasoning
|
||||
{
|
||||
Path subPath;
|
||||
Path superPath;
|
||||
SubtypingVariance variance = SubtypingVariance::Covariant;
|
||||
|
||||
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
|
||||
/// 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& orElse(const SubtypingResult& other);
|
||||
@ -59,6 +69,7 @@ struct SubtypingResult
|
||||
SubtypingResult& withBothPath(TypePath::Path path);
|
||||
SubtypingResult& withSubPath(TypePath::Path path);
|
||||
SubtypingResult& withSuperPath(TypePath::Path path);
|
||||
SubtypingResult& withVariance(SubtypingVariance variance);
|
||||
|
||||
// Only negates the `isSubtype`.
|
||||
static SubtypingResult negate(const SubtypingResult& result);
|
||||
|
@ -86,6 +86,24 @@ struct FreeType
|
||||
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
|
||||
{
|
||||
// By default, generics are global, with a synthetic name
|
||||
@ -623,7 +641,7 @@ struct NegationType
|
||||
using ErrorType = Unifiable::Error;
|
||||
|
||||
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>;
|
||||
|
||||
struct Type final
|
||||
|
@ -97,6 +97,10 @@ struct GenericTypeVisitor
|
||||
{
|
||||
return visit(ty);
|
||||
}
|
||||
virtual bool visit(TypeId ty, const LocalType& ftv)
|
||||
{
|
||||
return visit(ty);
|
||||
}
|
||||
virtual bool visit(TypeId ty, const GenericType& gtv)
|
||||
{
|
||||
return visit(ty);
|
||||
@ -241,6 +245,11 @@ struct GenericTypeVisitor
|
||||
else
|
||||
visit(ty, *ftv);
|
||||
}
|
||||
else if (auto lt = get<LocalType>(ty))
|
||||
{
|
||||
if (visit(ty, *lt))
|
||||
traverse(lt->domain);
|
||||
}
|
||||
else if (auto gtv = get<GenericType>(ty))
|
||||
visit(ty, *gtv);
|
||||
else if (auto etv = get<ErrorType>(ty))
|
||||
|
@ -261,6 +261,11 @@ private:
|
||||
t->upperBound = shallowClone(t->upperBound);
|
||||
}
|
||||
|
||||
void cloneChildren(LocalType* t)
|
||||
{
|
||||
t->domain = shallowClone(t->domain);
|
||||
}
|
||||
|
||||
void cloneChildren(GenericType* t)
|
||||
{
|
||||
// TOOD: clone upper bounds.
|
||||
@ -504,6 +509,7 @@ struct TypeCloner
|
||||
void defaultClone(const T& t);
|
||||
|
||||
void operator()(const FreeType& t);
|
||||
void operator()(const LocalType& t);
|
||||
void operator()(const GenericType& t);
|
||||
void operator()(const BoundType& t);
|
||||
void operator()(const ErrorType& t);
|
||||
@ -631,6 +637,11 @@ void TypeCloner::operator()(const FreeType& t)
|
||||
defaultClone(t);
|
||||
}
|
||||
|
||||
void TypeCloner::operator()(const LocalType& t)
|
||||
{
|
||||
defaultClone(t);
|
||||
}
|
||||
|
||||
void TypeCloner::operator()(const GenericType& t)
|
||||
{
|
||||
defaultClone(t);
|
||||
|
@ -205,33 +205,6 @@ ScopePtr ConstraintGenerator::childScope(AstNode* node, const ScopePtr& parent)
|
||||
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)
|
||||
{
|
||||
if (get<Cell>(def))
|
||||
@ -243,7 +216,7 @@ std::optional<TypeId> ConstraintGenerator::lookup(Scope* scope, DefId def)
|
||||
|
||||
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
|
||||
// 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)
|
||||
{
|
||||
std::vector<std::optional<TypeId>> varTypes;
|
||||
varTypes.reserve(statLocal->vars.size);
|
||||
std::vector<TypeId> annotatedTypes;
|
||||
annotatedTypes.reserve(statLocal->vars.size);
|
||||
bool hasAnnotation = false;
|
||||
|
||||
std::vector<std::optional<TypeId>> expectedTypes;
|
||||
expectedTypes.reserve(statLocal->vars.size);
|
||||
|
||||
std::vector<TypeId> assignees;
|
||||
assignees.reserve(statLocal->vars.size);
|
||||
@ -635,7 +612,8 @@ ControlFlow ConstraintGenerator::visit(const ScopePtr& scope, AstStatLocal* stat
|
||||
{
|
||||
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);
|
||||
|
||||
if (!firstValueType)
|
||||
@ -643,16 +621,21 @@ ControlFlow ConstraintGenerator::visit(const ScopePtr& scope, AstStatLocal* stat
|
||||
|
||||
if (local->annotation)
|
||||
{
|
||||
hasAnnotation = true;
|
||||
TypeId annotationTy = resolveType(scope, local->annotation, /* inTypeArguments */ false);
|
||||
varTypes.push_back(annotationTy);
|
||||
|
||||
addConstraint(scope, local->location, SubtypeConstraint{assignee, annotationTy});
|
||||
annotatedTypes.push_back(annotationTy);
|
||||
expectedTypes.push_back(annotationTy);
|
||||
|
||||
scope->bindings[local] = Binding{annotationTy, location};
|
||||
}
|
||||
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}};
|
||||
}
|
||||
@ -661,8 +644,12 @@ ControlFlow ConstraintGenerator::visit(const ScopePtr& scope, AstStatLocal* stat
|
||||
scope->lvalueTypes[def] = assignee;
|
||||
}
|
||||
|
||||
TypePackId resultPack = checkPack(scope, statLocal->values, varTypes).tp;
|
||||
addConstraint(scope, statLocal->location, UnpackConstraint{arena->addTypePack(std::move(assignees)), resultPack});
|
||||
TypePackId resultPack = checkPack(scope, statLocal->values, expectedTypes).tp;
|
||||
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)
|
||||
{
|
||||
@ -1006,26 +993,22 @@ static void bindFreeType(TypeId a, TypeId b)
|
||||
|
||||
ControlFlow ConstraintGenerator::visit(const ScopePtr& scope, AstStatAssign* assign)
|
||||
{
|
||||
std::vector<std::optional<TypeId>> expectedTypes;
|
||||
expectedTypes.reserve(assign->vars.size);
|
||||
|
||||
std::vector<TypeId> assignees;
|
||||
assignees.reserve(assign->vars.size);
|
||||
|
||||
for (AstExpr* lvalue : assign->vars)
|
||||
{
|
||||
TypeId assignee = arena->addType(BlockedType{});
|
||||
assignees.push_back(assignee);
|
||||
|
||||
checkLValue(scope, lvalue, assignee);
|
||||
assignees.push_back(assignee);
|
||||
|
||||
DefId def = dfg->getDef(lvalue);
|
||||
scope->lvalueTypes[def] = assignee;
|
||||
updateLValueType(lvalue, assignee);
|
||||
}
|
||||
|
||||
TypePackId resultPack = checkPack(scope, assign->values, expectedTypes).tp;
|
||||
addConstraint(scope, assign->location, UnpackConstraint{arena->addTypePack(std::move(assignees)), resultPack});
|
||||
TypePackId resultPack = checkPack(scope, assign->values).tp;
|
||||
addConstraint(scope, assign->location, UnpackConstraint{arena->addTypePack(std::move(assignees)), resultPack, /*resultIsLValue*/ true});
|
||||
|
||||
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->rvalueRefinements[def] = resultTy; // TODO: typestates: track this as an assignment
|
||||
|
||||
if (auto it = inferredBindings.find(targetLocal->local); it != inferredBindings.end())
|
||||
it->second.types.insert(resultTy);
|
||||
recordInferredBinding(targetLocal->local, resultTy);
|
||||
}
|
||||
|
||||
return InferencePack{arena->addTypePack({resultTy}), {refinementArena.variadic(returnRefinements)}};
|
||||
@ -1723,8 +1705,8 @@ Inference ConstraintGenerator::check(const ScopePtr& scope, AstExprLocal* local)
|
||||
if (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)};
|
||||
}
|
||||
@ -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)
|
||||
{
|
||||
/*
|
||||
* 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);
|
||||
LUAU_ASSERT(annotatedTy);
|
||||
if (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)
|
||||
@ -2379,15 +2373,6 @@ TypeId ConstraintGenerator::updateProperty(const ScopePtr& scope, AstExpr* expr,
|
||||
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)
|
||||
{
|
||||
const bool expectedTypeIsFree = expectedType && get<FreeType>(follow(*expectedType));
|
||||
@ -2611,13 +2596,7 @@ ConstraintGenerator::FunctionSignature ConstraintGenerator::checkFunctionSignatu
|
||||
argTypes.push_back(argTy);
|
||||
argNames.emplace_back(FunctionArgument{local->name.value, local->location});
|
||||
|
||||
if (local->annotation)
|
||||
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);
|
||||
signatureScope->lvalueTypes[def] = argTy;
|
||||
@ -3125,6 +3104,12 @@ void ConstraintGenerator::prepopulateGlobalScope(const ScopePtr& globalScope, As
|
||||
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)
|
||||
{
|
||||
for (const auto& [symbol, p] : inferredBindings)
|
||||
|
@ -993,6 +993,27 @@ bool ConstraintSolver::tryDispatch(const FunctionCallConstraint& c, NotNull<cons
|
||||
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 it = begin(t);
|
||||
auto endIt = end(t);
|
||||
@ -1018,10 +1039,12 @@ bool ConstraintSolver::tryDispatch(const FunctionCallConstraint& c, NotNull<cons
|
||||
// We don't support magic __call metamethods.
|
||||
if (std::optional<TypeId> callMm = findMetatableEntry(builtinTypes, errors, fn, "__call", constraint->location))
|
||||
{
|
||||
auto [head, tail] = flatten(c.argsPack);
|
||||
head.insert(head.begin(), fn);
|
||||
argsHead.insert(argsHead.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);
|
||||
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)
|
||||
{
|
||||
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))
|
||||
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);
|
||||
if (!blocked.empty())
|
||||
{
|
||||
@ -1162,8 +1176,8 @@ bool ConstraintSolver::tryDispatch(const HasPropConstraint& c, NotNull<const Con
|
||||
return false;
|
||||
}
|
||||
|
||||
bindBlockedType(c.resultType, result.value_or(builtinTypes->anyType), c.subjectType, constraint->location);
|
||||
unblock(c.resultType, constraint->location);
|
||||
bindBlockedType(resultType, result.value_or(builtinTypes->anyType), c.subjectType, constraint->location);
|
||||
unblock(resultType, constraint->location);
|
||||
return true;
|
||||
}
|
||||
|
||||
@ -1438,32 +1452,57 @@ bool ConstraintSolver::tryDispatch(const UnpackConstraint& c, NotNull<const Cons
|
||||
|
||||
TypePack srcPack = extendTypePack(*arena, builtinTypes, sourcePack, size(resultPack));
|
||||
|
||||
auto destIter = begin(resultPack);
|
||||
auto destEnd = end(resultPack);
|
||||
auto resultIter = begin(resultPack);
|
||||
auto resultEnd = end(resultPack);
|
||||
|
||||
size_t i = 0;
|
||||
while (destIter != destEnd)
|
||||
while (resultIter != resultEnd)
|
||||
{
|
||||
if (i >= srcPack.head.size())
|
||||
break;
|
||||
|
||||
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);
|
||||
asMutable(*destIter)->ty.emplace<BoundType>(f);
|
||||
asMutable(resultTy)->ty.emplace<BoundType>(f);
|
||||
}
|
||||
else
|
||||
asMutable(*destIter)->ty.emplace<BoundType>(srcTy);
|
||||
unblock(*destIter, constraint->location);
|
||||
asMutable(resultTy)->ty.emplace<BoundType>(srcTy);
|
||||
}
|
||||
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;
|
||||
}
|
||||
|
||||
@ -1471,15 +1510,25 @@ bool ConstraintSolver::tryDispatch(const UnpackConstraint& c, NotNull<const Cons
|
||||
// sourcePack is long enough to fill every value. Replace every remaining
|
||||
// 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);
|
||||
unblock(*destIter, constraint->location);
|
||||
lt->domain = simplifyUnion(builtinTypes, arena, lt->domain, builtinTypes->nilType).result;
|
||||
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;
|
||||
@ -1999,14 +2048,23 @@ std::pair<std::vector<TypeId>, std::optional<TypeId>> ConstraintSolver::lookupTa
|
||||
}
|
||||
else if (auto ft = get<FreeType>(subjectType))
|
||||
{
|
||||
Scope* scope = ft->scope;
|
||||
const TypeId upperBound = follow(ft->upperBound);
|
||||
|
||||
TableType* tt = &asMutable(subjectType)->ty.emplace<TableType>();
|
||||
tt->state = TableState::Free;
|
||||
tt->scope = scope;
|
||||
if (get<TableType>(upperBound))
|
||||
return lookupTableProp(upperBound, propName, suppressSimplification, seen);
|
||||
|
||||
// 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);
|
||||
tt->props[propName] = Property{propType};
|
||||
|
||||
unify(scope, Location{}, subjectType, newUpperBound);
|
||||
|
||||
return {{}, propType};
|
||||
}
|
||||
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)
|
||||
{
|
||||
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)
|
||||
|
@ -161,23 +161,107 @@ DfgScope* DataFlowGraphBuilder::childScope(DfgScope* scope, bool isLoopScope)
|
||||
|
||||
void DataFlowGraphBuilder::join(DfgScope* p, DfgScope* a, DfgScope* b)
|
||||
{
|
||||
// TODO TODO FIXME IMPLEMENT JOIN LOGIC FOR PROPERTIES
|
||||
|
||||
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});
|
||||
joinBindings(p->bindings, a->bindings, b->bindings);
|
||||
joinProps(p->props, a->props, b->props);
|
||||
}
|
||||
|
||||
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;
|
||||
else if (auto def2 = p->bindings.find(sym))
|
||||
p->bindings[sym] = defArena->phi(NotNull{def1}, NotNull{*def2});
|
||||
else if (auto it = p.find(k); it != p.end())
|
||||
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)
|
||||
@ -585,6 +669,7 @@ DataFlowResult DataFlowGraphBuilder::visitExpr(DfgScope* scope, AstExprGroup* gr
|
||||
|
||||
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))
|
||||
{
|
||||
const RefinementKey* key = keyArena->leaf(*def);
|
||||
@ -596,11 +681,7 @@ DataFlowResult DataFlowGraphBuilder::visitExpr(DfgScope* scope, AstExprLocal* l)
|
||||
|
||||
DataFlowResult DataFlowGraphBuilder::visitExpr(DfgScope* scope, AstExprGlobal* g)
|
||||
{
|
||||
if (auto def = scope->lookup(g->name))
|
||||
return {*def, keyArena->leaf(*def)};
|
||||
|
||||
DefId def = defArena->freshCell();
|
||||
moduleScope->bindings[g->name] = def;
|
||||
DefId def = lookup(scope, g->name);
|
||||
return {def, keyArena->leaf(def)};
|
||||
}
|
||||
|
||||
@ -619,15 +700,10 @@ DataFlowResult DataFlowGraphBuilder::visitExpr(DfgScope* scope, AstExprIndexName
|
||||
auto [parentDef, parentKey] = visitExpr(scope, i->expr);
|
||||
|
||||
std::string index = i->index.value;
|
||||
if (auto propDef = scope->lookup(parentDef, index))
|
||||
return {*propDef, keyArena->node(parentKey, *propDef, index)};
|
||||
else
|
||||
{
|
||||
DefId def = defArena->freshCell();
|
||||
scope->props[parentDef][index] = def;
|
||||
|
||||
DefId def = lookup(scope, parentDef, index);
|
||||
return {def, keyArena->node(parentKey, def, index)};
|
||||
}
|
||||
}
|
||||
|
||||
DataFlowResult DataFlowGraphBuilder::visitExpr(DfgScope* scope, AstExprIndexExpr* i)
|
||||
{
|
||||
@ -637,15 +713,10 @@ DataFlowResult DataFlowGraphBuilder::visitExpr(DfgScope* scope, AstExprIndexExpr
|
||||
if (auto string = i->index->as<AstExprConstantString>())
|
||||
{
|
||||
std::string index{string->value.data, string->value.size};
|
||||
if (auto propDef = scope->lookup(parentDef, index))
|
||||
return {*propDef, keyArena->node(parentKey, *propDef, index)};
|
||||
else
|
||||
{
|
||||
DefId def = defArena->freshCell();
|
||||
scope->props[parentDef][index] = def;
|
||||
|
||||
DefId def = lookup(scope, parentDef, index);
|
||||
return {def, keyArena->node(parentKey, def, index)};
|
||||
}
|
||||
}
|
||||
|
||||
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.
|
||||
if (isCompoundAssignment)
|
||||
{
|
||||
if (auto def = scope->lookup(g->name))
|
||||
graph.compoundAssignDefs[g] = *def;
|
||||
DefId def = lookup(scope, g->name);
|
||||
graph.compoundAssignDefs[g] = def;
|
||||
}
|
||||
|
||||
// In order to avoid alias tracking, we need to clip the reference to the parent def.
|
||||
|
@ -1,8 +1,9 @@
|
||||
// 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/Common.h"
|
||||
#include "Luau/DenseHash.h"
|
||||
|
||||
#include "Luau/Common.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <deque>
|
||||
|
||||
namespace Luau
|
||||
@ -13,26 +14,8 @@ bool containsSubscriptedDefinition(DefId def)
|
||||
if (auto cell = get<Cell>(def))
|
||||
return cell->subscripted;
|
||||
else if (auto phi = get<Phi>(def))
|
||||
{
|
||||
std::deque<DefId> queue(begin(phi->operands), end(phi->operands));
|
||||
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 std::any_of(phi->operands.begin(), phi->operands.end(), containsSubscriptedDefinition);
|
||||
else
|
||||
return false;
|
||||
}
|
||||
|
||||
@ -41,12 +24,35 @@ DefId DefArena::freshCell(bool 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)
|
||||
{
|
||||
if (a == b)
|
||||
return a;
|
||||
return phi({a, b});
|
||||
}
|
||||
|
||||
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
|
||||
return NotNull{allocator.allocate(Def{Phi{{a, b}}})};
|
||||
return NotNull{allocator.allocate(Def{Phi{std::move(operands)}})};
|
||||
}
|
||||
|
||||
} // namespace Luau
|
||||
|
@ -84,7 +84,7 @@ struct NonStrictContext
|
||||
|
||||
for (auto [def, rightTy] : right.context)
|
||||
{
|
||||
if (!right.find(def).has_value())
|
||||
if (!left.find(def).has_value())
|
||||
disj.context[def] = rightTy;
|
||||
}
|
||||
|
||||
@ -270,18 +270,24 @@ struct NonStrictTypeChecker
|
||||
NonStrictContext visit(AstStatBlock* block)
|
||||
{
|
||||
auto StackPusher = pushStack(block);
|
||||
NonStrictContext ctx;
|
||||
for (AstStat* statement : block->body)
|
||||
visit(statement);
|
||||
return {};
|
||||
ctx = NonStrictContext::disjunction(builtinTypes, NotNull{&arena}, ctx, visit(statement));
|
||||
return ctx;
|
||||
}
|
||||
|
||||
NonStrictContext visit(AstStatIf* ifStatement)
|
||||
{
|
||||
NonStrictContext condB = visit(ifStatement->condition);
|
||||
NonStrictContext thenB = visit(ifStatement->thenbody);
|
||||
NonStrictContext elseB = visit(ifStatement->elsebody);
|
||||
return NonStrictContext::disjunction(
|
||||
builtinTypes, NotNull{&arena}, condB, NonStrictContext::conjunction(builtinTypes, NotNull{&arena}, thenB, elseB));
|
||||
NonStrictContext branchContext;
|
||||
// If there is no else branch, don't bother generating warnings for the then branch - we can't prove there is an error
|
||||
if (ifStatement->elsebody)
|
||||
{
|
||||
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)
|
||||
@ -316,6 +322,8 @@ struct NonStrictTypeChecker
|
||||
|
||||
NonStrictContext visit(AstStatLocal* local)
|
||||
{
|
||||
for (AstExpr* rhs : local->values)
|
||||
visit(rhs);
|
||||
return {};
|
||||
}
|
||||
|
||||
@ -341,12 +349,12 @@ struct NonStrictTypeChecker
|
||||
|
||||
NonStrictContext visit(AstStatFunction* statFn)
|
||||
{
|
||||
return {};
|
||||
return visit(statFn->func);
|
||||
}
|
||||
|
||||
NonStrictContext visit(AstStatLocalFunction* localFn)
|
||||
{
|
||||
return {};
|
||||
return visit(localFn->func);
|
||||
}
|
||||
|
||||
NonStrictContext visit(AstStatTypeAlias* typeAlias)
|
||||
@ -530,7 +538,7 @@ struct NonStrictTypeChecker
|
||||
NonStrictContext visit(AstExprFunction* exprFn)
|
||||
{
|
||||
auto pusher = pushStack(exprFn);
|
||||
return {};
|
||||
return visit(exprFn->body);
|
||||
}
|
||||
|
||||
NonStrictContext visit(AstExprTable* table)
|
||||
@ -589,10 +597,6 @@ struct NonStrictTypeChecker
|
||||
SubtypingResult r = subtyping.isSubtype(actualType, *contextTy);
|
||||
if (r.normalizationTooComplex)
|
||||
reportError(NormalizationTooComplex{}, fragment->location);
|
||||
|
||||
if (!r.isSubtype && !r.isErrorSuppressing)
|
||||
reportError(TypeMismatch{actualType, *contextTy}, fragment->location);
|
||||
|
||||
if (r.isSubtype)
|
||||
return {actualType};
|
||||
}
|
||||
|
@ -1623,6 +1623,12 @@ bool Normalizer::unionNormalWithTy(NormalizedType& here, TypeId there, Set<TypeI
|
||||
inter.tops = builtinTypes->unknownType;
|
||||
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))
|
||||
unionFunctionsWithFunction(here.functions, there);
|
||||
else if (get<TableType>(there) || get<MetatableType>(there))
|
||||
|
@ -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
|
||||
{
|
||||
for (const Scope* current = this; current; current = current->parent.get())
|
||||
|
@ -19,8 +19,12 @@ static TypeId shallowClone(TypeId ty, TypeArena& dest, const TxnLog* log, bool a
|
||||
auto go = [ty, &dest, alwaysClone](auto&& 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>)
|
||||
return ty;
|
||||
else if constexpr (std::is_same_v<T, LocalType>)
|
||||
return ty;
|
||||
else if constexpr (std::is_same_v<T, BoundType>)
|
||||
{
|
||||
// This should never happen, but visit() cannot see it.
|
||||
|
@ -47,12 +47,12 @@ struct VarianceFlipper
|
||||
|
||||
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
|
||||
{
|
||||
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)
|
||||
@ -162,6 +162,19 @@ SubtypingResult& SubtypingResult::withSuperPath(TypePath::Path path)
|
||||
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)
|
||||
{
|
||||
return SubtypingResult{
|
||||
@ -671,7 +684,7 @@ SubtypingResult Subtyping::isContravariantWith(SubtypingEnvironment& env, SubTy&
|
||||
template<typename SubTy, typename 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>
|
||||
@ -689,7 +702,7 @@ SubtypingResult Subtyping::isContravariantWith(SubtypingEnvironment& env, const
|
||||
template<typename SubTy, typename SuperTy>
|
||||
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)
|
||||
{
|
||||
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
|
||||
// the subtyping rule for this is just if the table component is a subtype
|
||||
|
@ -261,6 +261,14 @@ void StateDot::visitChildren(TypeId ty, int index)
|
||||
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>)
|
||||
{
|
||||
formatAppend(result, "AnyType %d", index);
|
||||
|
@ -100,6 +100,16 @@ struct FindCyclicTypes final : TypeVisitor
|
||||
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
|
||||
{
|
||||
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)
|
||||
{
|
||||
stringify(btv.boundTo);
|
||||
|
@ -329,10 +329,14 @@ public:
|
||||
{
|
||||
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());
|
||||
}
|
||||
AstType* operator()(const LocalType& lt)
|
||||
{
|
||||
return Luau::visit(*this, lt.domain->ty);
|
||||
}
|
||||
AstType* operator()(const UnionType& uv)
|
||||
{
|
||||
AstArray<AstType*> unionTypes;
|
||||
|
@ -2464,13 +2464,16 @@ struct TypeChecker2
|
||||
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);
|
||||
|
||||
std::string relation = "a subtype of";
|
||||
if (reasoning.variance == SubtypingVariance::Invariant)
|
||||
relation = "exactly";
|
||||
|
||||
std::string reason;
|
||||
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
|
||||
reason = "type " + toString(subTy) + toString(reasoning.subPath, /* prefixDot */ true) + " (" + toString(*subLeaf) +
|
||||
") is not a subtype of " + toString(superTy) + toString(reasoning.superPath, /* prefixDot */ true) + " (" +
|
||||
toString(*superLeaf) + ")";
|
||||
reason = "type " + toString(subTy) + toString(reasoning.subPath, /* prefixDot */ true) + " (" + toString(*subLeaf) + ") is not " +
|
||||
relation + " " + toString(superTy) + toString(reasoning.superPath, /* prefixDot */ true) + " (" + toString(*superLeaf) + ")";
|
||||
|
||||
reasons.push_back(reason);
|
||||
}
|
||||
|
295
CLI/Bytecode.cpp
Normal file
295
CLI/Bytecode.cpp
Normal 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;
|
||||
}
|
@ -37,6 +37,7 @@ if(LUAU_BUILD_CLI)
|
||||
add_executable(Luau.Ast.CLI)
|
||||
add_executable(Luau.Reduce.CLI)
|
||||
add_executable(Luau.Compile.CLI)
|
||||
add_executable(Luau.Bytecode.CLI)
|
||||
|
||||
# This also adds target `name` on Linux/macOS and `name.exe` on Windows
|
||||
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.Reduce.CLI PROPERTIES OUTPUT_NAME luau-reduce)
|
||||
set_target_properties(Luau.Compile.CLI PROPERTIES OUTPUT_NAME luau-compile)
|
||||
set_target_properties(Luau.Bytecode.CLI PROPERTIES OUTPUT_NAME luau-bytecode)
|
||||
endif()
|
||||
|
||||
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.Ast.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)
|
||||
|
||||
@ -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.Compile.CLI PRIVATE Luau.Compiler Luau.VM Luau.CodeGen)
|
||||
|
||||
target_link_libraries(Luau.Bytecode.CLI PRIVATE Luau.Compiler Luau.VM Luau.CodeGen)
|
||||
endif()
|
||||
|
||||
if(LUAU_BUILD_TESTS)
|
||||
|
81
CodeGen/include/Luau/BytecodeSummary.h
Normal file
81
CodeGen/include/Luau/BytecodeSummary.h
Normal 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
|
@ -6,6 +6,8 @@
|
||||
#include <stdarg.h>
|
||||
#include <stdio.h>
|
||||
|
||||
LUAU_FASTFLAG(LuauCodeGenFixByteLower)
|
||||
|
||||
namespace Luau
|
||||
{
|
||||
namespace CodeGen
|
||||
@ -1437,11 +1439,19 @@ void AssemblyBuilderX64::placeImm8(int32_t imm)
|
||||
{
|
||||
int8_t imm8 = int8_t(imm);
|
||||
|
||||
if (FFlag::LuauCodeGenFixByteLower)
|
||||
{
|
||||
LUAU_ASSERT(imm8 == imm);
|
||||
place(imm8);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (imm8 == imm)
|
||||
place(imm8);
|
||||
else
|
||||
LUAU_ASSERT(!"Invalid immediate value");
|
||||
}
|
||||
}
|
||||
|
||||
void AssemblyBuilderX64::placeImm16(int16_t imm)
|
||||
{
|
||||
|
71
CodeGen/src/BytecodeSummary.cpp
Normal file
71
CodeGen/src/BytecodeSummary.cpp
Normal 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
|
@ -405,7 +405,15 @@ void IrLoweringA64::lowerInst(IrInst& inst, uint32_t index, const IrBlock& next)
|
||||
case IrCmd::STORE_POINTER:
|
||||
{
|
||||
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);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case IrCmd::STORE_DOUBLE:
|
||||
|
@ -15,6 +15,8 @@
|
||||
#include "lstate.h"
|
||||
#include "lgc.h"
|
||||
|
||||
LUAU_FASTFLAG(LuauCodeGenFixByteLower)
|
||||
|
||||
namespace Luau
|
||||
{
|
||||
namespace CodeGen
|
||||
@ -213,11 +215,24 @@ void IrLoweringX64::lowerInst(IrInst& inst, uint32_t index, const IrBlock& next)
|
||||
LUAU_ASSERT(!"Unsupported instruction form");
|
||||
break;
|
||||
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
|
||||
build.mov(luauRegValue(vmRegOp(inst.a)), regOp(inst.b));
|
||||
{
|
||||
LUAU_ASSERT(!"Unsupported instruction form");
|
||||
}
|
||||
break;
|
||||
}
|
||||
case IrCmd::STORE_DOUBLE:
|
||||
{
|
||||
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;
|
||||
|
||||
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));
|
||||
|
||||
build.mov(byte[bufferAddrOp(inst.a, inst.b)], value);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
@ -1806,10 +1830,19 @@ void IrLoweringX64::lowerInst(IrInst& inst, uint32_t index, const IrBlock& next)
|
||||
break;
|
||||
|
||||
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));
|
||||
|
||||
build.mov(word[bufferAddrOp(inst.a, inst.b)], value);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
|
@ -14,6 +14,7 @@
|
||||
|
||||
LUAU_FASTFLAGVARIABLE(LuauLowerAltLoopForn, false)
|
||||
LUAU_FASTFLAG(LuauImproveInsertIr)
|
||||
LUAU_FASTFLAGVARIABLE(LuauFullLoopLuserdata, false)
|
||||
|
||||
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));
|
||||
|
||||
// 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::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));
|
||||
|
||||
// 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::JUMP, target);
|
||||
|
@ -17,6 +17,7 @@ LUAU_FASTINTVARIABLE(LuauCodeGenReuseSlotLimit, 64)
|
||||
LUAU_FASTFLAGVARIABLE(DebugLuauAbortingChecks, false)
|
||||
LUAU_FASTFLAGVARIABLE(LuauReuseArrSlots2, false)
|
||||
LUAU_FASTFLAG(LuauLowerAltLoopForn)
|
||||
LUAU_FASTFLAGVARIABLE(LuauCodeGenFixByteLower, false)
|
||||
|
||||
namespace Luau
|
||||
{
|
||||
@ -618,6 +619,9 @@ static void constPropInInst(ConstPropState& state, IrBuilder& build, IrFunction&
|
||||
if (inst.a.kind == IrOpKind::VmReg)
|
||||
{
|
||||
state.invalidateValue(inst.a);
|
||||
|
||||
if (inst.b.kind == IrOpKind::Inst)
|
||||
{
|
||||
state.forwardVmRegStoreToLoad(inst, IrCmd::LOAD_POINTER);
|
||||
|
||||
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;
|
||||
case IrCmd::STORE_DOUBLE:
|
||||
if (inst.a.kind == IrOpKind::VmReg)
|
||||
|
@ -540,7 +540,7 @@ public:
|
||||
return impl.end();
|
||||
}
|
||||
|
||||
bool operator==(const DenseHashSet<Key, Hash, Eq>& other)
|
||||
bool operator==(const DenseHashSet<Key, Hash, Eq>& other) const
|
||||
{
|
||||
if (size() != other.size())
|
||||
return false;
|
||||
@ -554,7 +554,7 @@ public:
|
||||
return true;
|
||||
}
|
||||
|
||||
bool operator!=(const DenseHashSet<Key, Hash, Eq>& other)
|
||||
bool operator!=(const DenseHashSet<Key, Hash, Eq>& other) const
|
||||
{
|
||||
return !(*this == other);
|
||||
}
|
||||
|
15
Makefile
15
Makefile
@ -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_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_OBJECTS=$(FUZZ_SOURCES:%=$(BUILD)/%.o)
|
||||
|
||||
@ -65,8 +69,8 @@ ifneq ($(opt),)
|
||||
TESTS_ARGS+=-O$(opt)
|
||||
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)
|
||||
EXECUTABLE_ALIASES = luau luau-analyze luau-compile luau-tests
|
||||
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-bytecode luau-tests
|
||||
|
||||
# common flags
|
||||
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
|
||||
$(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
|
||||
$(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
|
||||
|
||||
$(TESTS_TARGET): LDFLAGS+=-lpthread
|
||||
@ -206,6 +211,9 @@ luau-analyze: $(ANALYZE_CLI_TARGET)
|
||||
luau-compile: $(COMPILE_CLI_TARGET)
|
||||
ln -fs $^ $@
|
||||
|
||||
luau-bytecode: $(BYTECODE_CLI_TARGET)
|
||||
ln -fs $^ $@
|
||||
|
||||
luau-tests: $(TESTS_TARGET)
|
||||
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)
|
||||
$(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)
|
||||
$(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 $@
|
||||
|
||||
# executable targets for fuzzing
|
||||
|
@ -92,6 +92,7 @@ target_sources(Luau.CodeGen PRIVATE
|
||||
CodeGen/include/Luau/UnwindBuilder.h
|
||||
CodeGen/include/Luau/UnwindBuilderDwarf2.h
|
||||
CodeGen/include/Luau/UnwindBuilderWin.h
|
||||
CodeGen/include/Luau/BytecodeSummary.h
|
||||
CodeGen/include/luacodegen.h
|
||||
|
||||
CodeGen/src/AssemblyBuilderA64.cpp
|
||||
@ -124,6 +125,7 @@ target_sources(Luau.CodeGen PRIVATE
|
||||
CodeGen/src/OptimizeFinalX64.cpp
|
||||
CodeGen/src/UnwindBuilderDwarf2.cpp
|
||||
CodeGen/src/UnwindBuilderWin.cpp
|
||||
CodeGen/src/BytecodeSummary.cpp
|
||||
|
||||
CodeGen/src/BitUtils.h
|
||||
CodeGen/src/ByteUtils.h
|
||||
@ -518,3 +520,13 @@ if(TARGET Luau.Compile.CLI)
|
||||
CLI/Flags.cpp
|
||||
CLI/Compile.cpp)
|
||||
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()
|
||||
|
@ -10,7 +10,9 @@
|
||||
#include "Luau/TypeInfer.h"
|
||||
#include "Luau/BytecodeBuilder.h"
|
||||
#include "Luau/Frontend.h"
|
||||
#include "Luau/Compiler.h"
|
||||
#include "Luau/CodeGen.h"
|
||||
#include "Luau/BytecodeSummary.h"
|
||||
|
||||
#include "doctest.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_CASE("CodegenSupported")
|
||||
@ -292,6 +313,7 @@ TEST_CASE("Basic")
|
||||
TEST_CASE("Buffers")
|
||||
{
|
||||
ScopedFastFlag luauBufferBetterMsg{"LuauBufferBetterMsg", true};
|
||||
ScopedFastFlag luauCodeGenFixByteLower{"LuauCodeGenFixByteLower", true};
|
||||
|
||||
runConformance("buffers.lua");
|
||||
}
|
||||
@ -1988,4 +2010,51 @@ TEST_CASE("HugeFunction")
|
||||
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();
|
||||
|
@ -317,4 +317,97 @@ TEST_CASE_FIXTURE(DataFlowGraphFixture, "mutate_property_of_table_owned_by_while
|
||||
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();
|
||||
|
@ -412,7 +412,7 @@ TypeId Fixture::requireTypeAlias(const std::string& name)
|
||||
{
|
||||
std::optional<TypeId> ty = lookupType(name);
|
||||
REQUIRE(ty);
|
||||
return *ty;
|
||||
return follow(*ty);
|
||||
}
|
||||
|
||||
TypeId Fixture::requireExportedType(const ModuleName& moduleName, const std::string& name)
|
||||
|
@ -6,12 +6,22 @@
|
||||
#include "Luau/Common.h"
|
||||
#include "Luau/Ast.h"
|
||||
#include "Luau/ModuleResolver.h"
|
||||
#include "Luau/VisitType.h"
|
||||
#include "ScopedFlags.h"
|
||||
#include "doctest.h"
|
||||
#include <iostream>
|
||||
|
||||
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
|
||||
{
|
||||
|
||||
@ -28,22 +38,167 @@ struct NonStrictTypeCheckerFixture : Fixture
|
||||
|
||||
std::string definitions = R"BUILTIN_SRC(
|
||||
declare function @checked abs(n: number): number
|
||||
declare function @checked lower(s: string): string
|
||||
declare function cond() : boolean
|
||||
)BUILTIN_SRC";
|
||||
};
|
||||
|
||||
|
||||
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")
|
||||
)BUILTIN_SRC");
|
||||
LUAU_REQUIRE_ERRORS(res);
|
||||
REQUIRE(res.errors.size() == 1);
|
||||
auto err = get<CheckedFunctionCallError>(res.errors[0]);
|
||||
REQUIRE(err != nullptr);
|
||||
REQUIRE(err->checkedFunctionName == "abs");
|
||||
LUAU_REQUIRE_ERROR_COUNT(1, result);
|
||||
NONSTRICT_REQUIRE_CHECKED_ERR(0, "abs", result);
|
||||
}
|
||||
|
||||
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();
|
||||
|
@ -60,4 +60,43 @@ TEST_CASE("erase_works_and_decreases_size")
|
||||
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();
|
||||
|
@ -10,6 +10,7 @@
|
||||
#include "doctest.h"
|
||||
#include "Fixture.h"
|
||||
#include "RegisterCallbacks.h"
|
||||
|
||||
#include <initializer_list>
|
||||
|
||||
using namespace Luau;
|
||||
@ -17,9 +18,38 @@ using 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)
|
||||
{
|
||||
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
|
||||
@ -1105,20 +1135,6 @@ TEST_SUITE_END();
|
||||
|
||||
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")
|
||||
{
|
||||
TypeId subTy = tbl({{"X", builtinTypes->numberType}});
|
||||
@ -1126,10 +1142,9 @@ TEST_CASE_FIXTURE(SubtypeFixture, "table_property")
|
||||
|
||||
SubtypingResult result = isSubtype(subTy, superTy);
|
||||
CHECK(!result.isSubtype);
|
||||
CHECK(result.reasoning == std::vector{SubtypingReasoning{
|
||||
/* subPath */ Path(TypePath::Property("X")),
|
||||
CHECK(result.reasoning == std::vector{SubtypingReasoning{/* subPath */ Path(TypePath::Property("X")),
|
||||
/* superPath */ Path(TypePath::Property("X")),
|
||||
}});
|
||||
/* variance */ SubtypingVariance::Invariant}});
|
||||
}
|
||||
|
||||
TEST_CASE_FIXTURE(SubtypeFixture, "table_indexers")
|
||||
@ -1142,10 +1157,12 @@ TEST_CASE_FIXTURE(SubtypeFixture, "table_indexers")
|
||||
CHECK(result.reasoning == std::vector{SubtypingReasoning{
|
||||
/* subPath */ Path(TypePath::TypeField::IndexLookup),
|
||||
/* superPath */ Path(TypePath::TypeField::IndexLookup),
|
||||
/* variance */ SubtypingVariance::Invariant,
|
||||
},
|
||||
SubtypingReasoning{
|
||||
/* subPath */ 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{
|
||||
/* subPath */ 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);
|
||||
CHECK(!result.isSubtype);
|
||||
CHECK(result.reasoning == std::vector{
|
||||
SubtypingReasoning{/* subPath */ Path(TypePath::Property("X")), /* superPath */ Path(TypePath::Property("X"))},
|
||||
SubtypingReasoning{/* subPath */ Path(TypePath::Property("Y")), /* superPath */ Path(TypePath::Property("Y"))},
|
||||
SubtypingReasoning{/* subPath */ Path(TypePath::Property("X")), /* superPath */ Path(TypePath::Property("X")),
|
||||
/* variance */ SubtypingVariance::Invariant},
|
||||
SubtypingReasoning{/* subPath */ Path(TypePath::Property("Y")), /* superPath */ Path(TypePath::Property("Y")),
|
||||
/* variance */ SubtypingVariance::Invariant},
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -938,7 +938,7 @@ TEST_CASE_FIXTURE(Fixture, "tostring_error_mismatch")
|
||||
//clang-format off
|
||||
std::string expected =
|
||||
(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
|
||||
'{ a: number, b: string, c: { d: string } }'
|
||||
|
@ -198,8 +198,7 @@ TEST_CASE_FIXTURE(Fixture, "generic_aliases")
|
||||
)");
|
||||
|
||||
LUAU_REQUIRE_ERROR_COUNT(1, result);
|
||||
const std::string expected =
|
||||
R"(Type 'bad' could not be converted into 'T<number>'; at ["v"], string is not a subtype of number)";
|
||||
const std::string expected = R"(Type 'bad' could not be converted into 'T<number>'; at ["v"], string is not exactly number)";
|
||||
CHECK(result.errors[0].location == Location{{4, 31}, {4, 44}});
|
||||
CHECK_EQ(expected, toString(result.errors[0]));
|
||||
}
|
||||
@ -218,8 +217,7 @@ TEST_CASE_FIXTURE(Fixture, "dependent_generic_aliases")
|
||||
)");
|
||||
|
||||
LUAU_REQUIRE_ERROR_COUNT(1, result);
|
||||
const std::string expected =
|
||||
R"(Type 'bad' could not be converted into 'U<number>'; at ["t"]["v"], string is not a subtype of number)";
|
||||
const std::string expected = R"(Type 'bad' could not be converted into 'U<number>'; at ["t"]["v"], string is not exactly number)";
|
||||
|
||||
CHECK(result.errors[0].location == Location{{4, 31}, {4, 52}});
|
||||
CHECK_EQ(expected, toString(result.errors[0]));
|
||||
|
@ -50,8 +50,16 @@ TEST_CASE_FIXTURE(Fixture, "for_in_loop_iterator_returns_any2")
|
||||
|
||||
LUAU_REQUIRE_NO_ERRORS(result);
|
||||
|
||||
if (FFlag::DebugLuauDeferredConstraintResolution)
|
||||
{
|
||||
// Bug: We do not simplify at the right time
|
||||
CHECK_EQ("any?", toString(requireType("a")));
|
||||
}
|
||||
else
|
||||
{
|
||||
CHECK_EQ("any", toString(requireType("a")));
|
||||
}
|
||||
}
|
||||
|
||||
TEST_CASE_FIXTURE(Fixture, "for_in_loop_iterator_is_any")
|
||||
{
|
||||
|
@ -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);
|
||||
TypeMismatch* tm = get<TypeMismatch>(result.errors[0]);
|
||||
TypeMismatch* tm = get<TypeMismatch>(result.errors.at(0));
|
||||
REQUIRE(tm != nullptr);
|
||||
|
||||
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);
|
||||
|
||||
auto err = get<UnknownPropButFoundLikeProp>(result.errors[0]);
|
||||
auto err = get<UnknownPropButFoundLikeProp>(result.errors.at(0));
|
||||
REQUIRE(err != nullptr);
|
||||
|
||||
REQUIRE_EQ(1, err->candidates.size());
|
||||
@ -290,7 +290,7 @@ TEST_CASE_FIXTURE(ClassFixture, "table_properties_are_invariant")
|
||||
)");
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
@ -313,7 +313,7 @@ TEST_CASE_FIXTURE(ClassFixture, "table_indexers_are_invariant")
|
||||
)");
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
@ -331,7 +331,7 @@ TEST_CASE_FIXTURE(ClassFixture, "table_class_unification_reports_sane_errors_for
|
||||
)");
|
||||
|
||||
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]));
|
||||
}
|
||||
|
||||
@ -345,7 +345,7 @@ TEST_CASE_FIXTURE(ClassFixture, "class_unification_type_mismatch_is_correct_orde
|
||||
|
||||
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]));
|
||||
}
|
||||
|
||||
@ -359,7 +359,7 @@ b.X = 2 -- real Vector2.X is also read-only
|
||||
)");
|
||||
|
||||
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("Key 'Z' not found in class 'Vector2'", toString(result.errors[2]));
|
||||
CHECK_EQ("Value of type 'Vector2?' could be nil", toString(result.errors[3]));
|
||||
@ -385,7 +385,7 @@ b(a)
|
||||
caused by:
|
||||
Property 'Y' is not compatible.
|
||||
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")
|
||||
@ -397,7 +397,7 @@ local a: ChildClass = i
|
||||
)");
|
||||
|
||||
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")
|
||||
@ -433,7 +433,7 @@ TEST_CASE_FIXTURE(ClassFixture, "index_instance_property")
|
||||
)");
|
||||
|
||||
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")
|
||||
@ -455,16 +455,22 @@ TEST_CASE_FIXTURE(ClassFixture, "type_mismatch_invariance_required_for_error")
|
||||
type A = { x: ChildClass }
|
||||
type B = { x: BaseClass }
|
||||
|
||||
local a: A
|
||||
local a: A = { x = ChildClass.New() }
|
||||
local b: B = a
|
||||
)");
|
||||
|
||||
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'
|
||||
caused by:
|
||||
Property 'x' is not compatible.
|
||||
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")
|
||||
@ -551,7 +557,7 @@ TEST_CASE_FIXTURE(ClassFixture, "indexable_classes")
|
||||
|
||||
|
||||
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"(
|
||||
@ -560,7 +566,7 @@ TEST_CASE_FIXTURE(ClassFixture, "indexable_classes")
|
||||
)");
|
||||
|
||||
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)
|
||||
@ -569,14 +575,14 @@ TEST_CASE_FIXTURE(ClassFixture, "indexable_classes")
|
||||
local x : IndexableClass
|
||||
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"(
|
||||
local x : IndexableClass
|
||||
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
|
||||
@ -593,9 +599,9 @@ TEST_CASE_FIXTURE(ClassFixture, "indexable_classes")
|
||||
x["key"] = 1
|
||||
)");
|
||||
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
|
||||
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"(
|
||||
@ -603,14 +609,14 @@ TEST_CASE_FIXTURE(ClassFixture, "indexable_classes")
|
||||
local str : string
|
||||
x[str] = 1 -- Index with a non-const string
|
||||
)");
|
||||
CHECK_EQ(toString(result.errors[0]), "Type 'string' could not be converted into 'number'");
|
||||
CHECK_EQ(toString(result.errors.at(0)), "Type 'string' could not be converted into 'number'");
|
||||
}
|
||||
{
|
||||
CheckResult result = check(R"(
|
||||
local x : IndexableNumericKeyClass
|
||||
local y = x.key
|
||||
)");
|
||||
CHECK_EQ(toString(result.errors[0]), "Key 'key' not found in class 'IndexableNumericKeyClass'");
|
||||
CHECK_EQ(toString(result.errors.at(0)), "Key 'key' not found in class 'IndexableNumericKeyClass'");
|
||||
}
|
||||
{
|
||||
CheckResult result = check(R"(
|
||||
@ -618,9 +624,9 @@ TEST_CASE_FIXTURE(ClassFixture, "indexable_classes")
|
||||
local y = x["key"]
|
||||
)");
|
||||
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
|
||||
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"(
|
||||
@ -628,7 +634,7 @@ TEST_CASE_FIXTURE(ClassFixture, "indexable_classes")
|
||||
local str : string
|
||||
local y = x[str] -- Index with a non-const string
|
||||
)");
|
||||
CHECK_EQ(toString(result.errors[0]), "Type 'string' could not be converted into 'number'");
|
||||
CHECK_EQ(toString(result.errors.at(0)), "Type 'string' could not be converted into 'number'");
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -725,6 +725,12 @@ y.a.c = y
|
||||
)");
|
||||
|
||||
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>'
|
||||
caused by:
|
||||
Property 'a' is not compatible.
|
||||
@ -734,6 +740,7 @@ caused by:
|
||||
Type 'number' could not be converted into 'string' in an invariant context)";
|
||||
CHECK_EQ(expected, toString(result.errors[0]));
|
||||
}
|
||||
}
|
||||
|
||||
TEST_CASE_FIXTURE(Fixture, "generic_type_pack_unification1")
|
||||
{
|
||||
|
@ -529,7 +529,7 @@ could not be converted into
|
||||
TEST_CASE_FIXTURE(Fixture, "intersection_of_tables_with_top_properties")
|
||||
{
|
||||
CheckResult result = check(R"(
|
||||
local x : { p : number?, q : any } & { p : unknown, q : string? }
|
||||
local x : { p : number?, q : any } & { p : unknown, q : string? } = { p = 123, q = "foo" }
|
||||
local y : { p : number?, q : string? } = x -- OK
|
||||
local z : { p : string?, q : number? } = x -- Not OK
|
||||
)");
|
||||
|
@ -410,13 +410,19 @@ local b: B.T = a
|
||||
)";
|
||||
|
||||
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'
|
||||
caused by:
|
||||
Property 'x' is not compatible.
|
||||
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]));
|
||||
}
|
||||
}
|
||||
|
||||
TEST_CASE_FIXTURE(BuiltinsFixture, "module_type_conflict_instantiated")
|
||||
{
|
||||
@ -445,13 +451,19 @@ local b: B.T = a
|
||||
)";
|
||||
|
||||
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'
|
||||
caused by:
|
||||
Property 'x' is not compatible.
|
||||
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]));
|
||||
}
|
||||
}
|
||||
|
||||
TEST_CASE_FIXTURE(BuiltinsFixture, "constrained_anyification_clone_immutable_types")
|
||||
{
|
||||
|
@ -1939,9 +1939,18 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "refine_unknown_to_table")
|
||||
|
||||
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("val")));
|
||||
}
|
||||
}
|
||||
|
||||
TEST_CASE_FIXTURE(BuiltinsFixture, "conditional_refinement_should_stay_error_suppressing")
|
||||
{
|
||||
|
@ -367,8 +367,8 @@ TEST_CASE_FIXTURE(Fixture, "parametric_tagged_union_alias")
|
||||
LUAU_REQUIRE_ERROR_COUNT(1, result);
|
||||
|
||||
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>)"
|
||||
"\n\ttype a[\"success\"] (false) is not a subtype of Err<number> | Ok<string>[0][\"success\"] (true)";
|
||||
"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"
|
||||
"\ttype a[\"success\"] (false) is not exactly Err<number> | Ok<string>[0][\"success\"] (true)";
|
||||
|
||||
CHECK(toString(result.errors[0]) == expectedError);
|
||||
}
|
||||
|
@ -2147,17 +2147,23 @@ TEST_CASE_FIXTURE(Fixture, "error_detailed_prop")
|
||||
type A = { x: number, y: number }
|
||||
type B = { x: number, y: string }
|
||||
|
||||
local a: A
|
||||
local a: A = { x = 123, y = 456 }
|
||||
local b: B = a
|
||||
)");
|
||||
|
||||
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'
|
||||
caused by:
|
||||
Property 'y' is not compatible.
|
||||
Type 'number' could not be converted into 'string' in an invariant context)";
|
||||
CHECK_EQ(expected, toString(result.errors[0]));
|
||||
}
|
||||
}
|
||||
|
||||
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 B = { a: boolean, b: BS }
|
||||
|
||||
local a: A
|
||||
local a: A = { a = false, b = { x = 123, y = 456 } }
|
||||
local b: B = a
|
||||
)");
|
||||
|
||||
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'
|
||||
caused by:
|
||||
Property 'b' is not compatible.
|
||||
@ -2182,6 +2193,7 @@ caused by:
|
||||
Type 'number' could not be converted into 'string' in an invariant context)";
|
||||
CHECK_EQ(expected, toString(result.errors[0]));
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
std::string expected = "Type 'a' could not be converted into 'T'; at [\"a\"], string is not a subtype of number"
|
||||
"\n\tat [\"b\"], boolean is not a subtype of string"
|
||||
"\n\tat [\"c\"], number is not a subtype of boolean";
|
||||
std::string expected = "Type 'a' could not be converted into 'T'; at [\"a\"], string is not exactly number"
|
||||
"\n\tat [\"b\"], boolean is not exactly string"
|
||||
"\n\tat [\"c\"], number is not exactly boolean";
|
||||
CHECK(toString(result.errors[0]) == expected);
|
||||
}
|
||||
|
||||
|
@ -28,8 +28,7 @@ TEST_CASE_FIXTURE(Fixture, "tc_hello_world")
|
||||
CheckResult result = check("local a = 7");
|
||||
LUAU_REQUIRE_NO_ERRORS(result);
|
||||
|
||||
TypeId aType = requireType("a");
|
||||
CHECK_EQ(getPrimitiveType(aType), PrimitiveType::Number);
|
||||
CHECK("number" == toString(requireType("a")));
|
||||
}
|
||||
|
||||
TEST_CASE_FIXTURE(Fixture, "tc_propagation")
|
||||
@ -44,15 +43,32 @@ TEST_CASE_FIXTURE(Fixture, "tc_propagation")
|
||||
TEST_CASE_FIXTURE(Fixture, "tc_error")
|
||||
{
|
||||
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);
|
||||
|
||||
CHECK_EQ(
|
||||
result.errors[0], (TypeError{Location{Position{0, 35}, Position{0, 36}}, TypeMismatch{builtinTypes->numberType, builtinTypes->stringType}}));
|
||||
}
|
||||
}
|
||||
|
||||
TEST_CASE_FIXTURE(Fixture, "tc_error_2")
|
||||
{
|
||||
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);
|
||||
|
||||
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,
|
||||
}}));
|
||||
}
|
||||
}
|
||||
|
||||
TEST_CASE_FIXTURE(Fixture, "infer_locals_with_nil_value")
|
||||
{
|
||||
CheckResult result = check("local f = nil; f = 'hello world'");
|
||||
LUAU_REQUIRE_NO_ERRORS(result);
|
||||
|
||||
if (FFlag::DebugLuauDeferredConstraintResolution)
|
||||
{
|
||||
CHECK("string?" == toString(requireType("f")));
|
||||
}
|
||||
else
|
||||
{
|
||||
TypeId ty = requireType("f");
|
||||
CHECK_EQ(getPrimitiveType(ty), PrimitiveType::String);
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
{
|
||||
CHECK("number | string" == toString(requireType("a")));
|
||||
CHECK("(number | string) -> ()" == toString(requireType("f")));
|
||||
CHECK("unknown" == toString(requireType("a")));
|
||||
CHECK("(unknown) -> ()" == toString(requireType("f")));
|
||||
|
||||
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")));
|
||||
}
|
||||
}
|
||||
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")
|
||||
{
|
||||
@ -178,9 +181,17 @@ TEST_CASE_FIXTURE(Fixture, "if_statement")
|
||||
|
||||
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->numberType, *requireType("b"));
|
||||
}
|
||||
}
|
||||
|
||||
TEST_CASE_FIXTURE(Fixture, "statements_are_topologically_sorted")
|
||||
{
|
||||
|
@ -274,4 +274,45 @@ TEST_CASE_FIXTURE(TypeStateFixture, "then_branch_assigns_but_is_met_with_return_
|
||||
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();
|
||||
|
@ -586,6 +586,15 @@ local function misc(t16)
|
||||
|
||||
buffer.writei32(b, #t16, 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
|
||||
|
||||
misc(table.create(16, 0))
|
||||
|
@ -275,4 +275,22 @@ end
|
||||
|
||||
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')
|
||||
|
@ -13,7 +13,6 @@ AutocompleteTest.autocomplete_string_singleton_equality
|
||||
AutocompleteTest.autocomplete_string_singleton_escape
|
||||
AutocompleteTest.autocomplete_string_singletons
|
||||
AutocompleteTest.do_wrong_compatible_nonself_calls
|
||||
AutocompleteTest.frontend_use_correct_global_scope
|
||||
AutocompleteTest.no_incompatible_self_calls_on_class
|
||||
AutocompleteTest.string_singleton_in_if_statement
|
||||
AutocompleteTest.suggest_external_module_type
|
||||
@ -165,7 +164,6 @@ GenericsTests.generic_argument_count_too_many
|
||||
GenericsTests.generic_factories
|
||||
GenericsTests.generic_functions_dont_cache_type_parameters
|
||||
GenericsTests.generic_functions_in_types
|
||||
GenericsTests.generic_functions_should_be_memory_safe
|
||||
GenericsTests.generic_type_pack_parentheses
|
||||
GenericsTests.generic_type_pack_unification1
|
||||
GenericsTests.generic_type_pack_unification2
|
||||
@ -234,7 +232,6 @@ Linter.DeprecatedApiFenv
|
||||
Linter.FormatStringTyped
|
||||
Linter.TableOperationsIndexer
|
||||
ModuleTests.clone_self_property
|
||||
Negations.cofinite_strings_can_be_compared_for_equality
|
||||
Negations.negated_string_is_a_subtype_of_string
|
||||
NonstrictModeTests.inconsistent_module_return_types_are_ok
|
||||
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.index_on_a_refined_property
|
||||
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.narrow_property_of_a_bounded_variable
|
||||
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_value
|
||||
TableTests.error_detailed_metatable_prop
|
||||
TableTests.error_detailed_prop
|
||||
TableTests.error_detailed_prop_nested
|
||||
TableTests.expected_indexer_from_table_union
|
||||
TableTests.expected_indexer_value_type_extra
|
||||
TableTests.expected_indexer_value_type_extra_2
|
||||
TableTests.explicitly_typed_table
|
||||
TableTests.explicitly_typed_table_error
|
||||
TableTests.explicitly_typed_table_with_indexer
|
||||
TableTests.fuzz_table_unify_instantiated_table_with_prop_realloc
|
||||
TableTests.generalize_table_argument
|
||||
TableTests.generic_table_instantiation_potential_regression
|
||||
TableTests.indexer_mismatch
|
||||
@ -420,7 +412,6 @@ TableTests.table_unifies_into_map
|
||||
TableTests.top_table_type
|
||||
TableTests.type_mismatch_on_massive_table_is_cut_short
|
||||
TableTests.unification_of_unions_in_a_self_referential_type
|
||||
TableTests.unifying_tables_shouldnt_uaf1
|
||||
TableTests.used_colon_instead_of_dot
|
||||
TableTests.used_dot_instead_of_colon
|
||||
TableTests.used_dot_instead_of_colon_but_correctly
|
||||
@ -485,14 +476,10 @@ TypeInfer.follow_on_new_types_in_substitution
|
||||
TypeInfer.globals
|
||||
TypeInfer.globals2
|
||||
TypeInfer.globals_are_banned_in_strict_mode
|
||||
TypeInfer.if_statement
|
||||
TypeInfer.infer_assignment_value_types
|
||||
TypeInfer.infer_assignment_value_types_mutable_lval
|
||||
TypeInfer.infer_locals_via_assignment_from_its_call_site
|
||||
TypeInfer.infer_locals_with_nil_value
|
||||
TypeInfer.infer_through_group_expr
|
||||
TypeInfer.infer_type_assertion_value_type
|
||||
TypeInfer.interesting_local_type_inference_case
|
||||
TypeInfer.no_infinite_loop_when_trying_to_unify_uh_this
|
||||
TypeInfer.no_stack_overflow_from_isoptional
|
||||
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.stringify_nested_unions_with_optionals
|
||||
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.type_infer_recursion_limit_no_ice
|
||||
TypeInfer.type_infer_recursion_limit_normalizer
|
||||
@ -531,7 +516,6 @@ TypeInferClasses.intersections_of_unions_of_classes
|
||||
TypeInferClasses.optional_class_field_access_error
|
||||
TypeInferClasses.table_class_unification_reports_sane_errors_for_missing_properties
|
||||
TypeInferClasses.table_indexers_are_invariant
|
||||
TypeInferClasses.type_mismatch_invariance_required_for_error
|
||||
TypeInferClasses.unions_of_intersections_of_classes
|
||||
TypeInferClasses.we_can_report_when_someone_is_trying_to_use_a_table_rather_than_a_class
|
||||
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_incompatible_args_to_iterator
|
||||
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_generic_next
|
||||
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
|
||||
TypeInferModules.bound_free_table_export_is_ok
|
||||
TypeInferModules.do_not_modify_imported_types
|
||||
TypeInferModules.do_not_modify_imported_types_4
|
||||
TypeInferModules.do_not_modify_imported_types_5
|
||||
TypeInferModules.module_type_conflict
|
||||
TypeInferModules.module_type_conflict_instantiated
|
||||
TypeInferModules.require
|
||||
TypeInferModules.require_failed_module
|
||||
TypeInferOOP.CheckMethodsOfSealed
|
||||
@ -680,7 +662,6 @@ TypeInferUnknownNever.index_on_union_of_tables_for_properties_that_is_sorta_neve
|
||||
TypeInferUnknownNever.length_of_never
|
||||
TypeInferUnknownNever.math_operators_and_never
|
||||
TypeInferUnknownNever.type_packs_containing_never_is_itself_uninhabitable
|
||||
TypePackTests.fuzz_typepack_iter_follow_2
|
||||
TypePackTests.pack_tail_unification_check
|
||||
TypePackTests.type_alias_backwards_compatible
|
||||
TypePackTests.type_alias_default_type_errors
|
||||
@ -699,6 +680,8 @@ TypeSingletons.table_properties_singleton_strings
|
||||
TypeSingletons.table_properties_type_error_escapes
|
||||
TypeSingletons.widen_the_supertype_if_it_is_free_and_subtype_has_singleton
|
||||
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_assign2
|
||||
UnionTypes.error_detailed_optional
|
||||
|
@ -113,6 +113,12 @@ def main():
|
||||
action="store_true",
|
||||
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")
|
||||
|
||||
@ -139,6 +145,9 @@ def main():
|
||||
elif args.randomize:
|
||||
commandLine.append("--randomize")
|
||||
|
||||
if args.suite:
|
||||
commandLine.append(f'--ts={args.suite}')
|
||||
|
||||
print_stderr(">", " ".join(commandLine))
|
||||
|
||||
p = sp.Popen(
|
||||
@ -146,6 +155,8 @@ def main():
|
||||
stdout=sp.PIPE,
|
||||
)
|
||||
|
||||
assert p.stdout
|
||||
|
||||
handler = Handler(failList)
|
||||
|
||||
if args.dump:
|
||||
|
Loading…
Reference in New Issue
Block a user