mirror of
https://github.com/luau-lang/luau.git
synced 2024-11-15 22:35:43 +08:00
Merge branch 'upstream' into merge
This commit is contained in:
commit
2d4a544709
@ -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