Merge branch 'upstream' of https://github.com/luau-lang/luau into upstream

This commit is contained in:
Vighnesh 2024-01-11 14:55:06 -08:00
commit d4883bfb32
153 changed files with 1649 additions and 857 deletions

View file

@ -197,24 +197,6 @@ struct UnpackConstraint
bool resultIsLValue = false; bool resultIsLValue = false;
}; };
// resultType ~ refine type mode discriminant
//
// Compute type & discriminant (or type | discriminant) as soon as possible (but
// no sooner), simplify, and bind resultType to that type.
struct RefineConstraint
{
enum
{
Intersection,
Union
} mode;
TypeId resultType;
TypeId type;
TypeId discriminant;
};
// resultType ~ T0 op T1 op ... op TN // resultType ~ T0 op T1 op ... op TN
// //
// op is either union or intersection. If any of the input types are blocked, // op is either union or intersection. If any of the input types are blocked,
@ -249,7 +231,7 @@ struct ReducePackConstraint
using ConstraintV = Variant<SubtypeConstraint, PackSubtypeConstraint, GeneralizationConstraint, InstantiationConstraint, IterableConstraint, using ConstraintV = Variant<SubtypeConstraint, PackSubtypeConstraint, GeneralizationConstraint, InstantiationConstraint, IterableConstraint,
NameConstraint, TypeAliasExpansionConstraint, FunctionCallConstraint, PrimitiveTypeConstraint, HasPropConstraint, SetPropConstraint, NameConstraint, TypeAliasExpansionConstraint, FunctionCallConstraint, PrimitiveTypeConstraint, HasPropConstraint, SetPropConstraint,
SetIndexerConstraint, SingletonOrTopTypeConstraint, UnpackConstraint, RefineConstraint, SetOpConstraint, ReduceConstraint, ReducePackConstraint>; SetIndexerConstraint, SingletonOrTopTypeConstraint, UnpackConstraint, SetOpConstraint, ReduceConstraint, ReducePackConstraint>;
struct Constraint struct Constraint
{ {

View file

@ -150,7 +150,7 @@ private:
*/ */
ScopePtr childScope(AstNode* node, const ScopePtr& parent); ScopePtr childScope(AstNode* node, const ScopePtr& parent);
std::optional<TypeId> lookup(Scope* scope, DefId def); std::optional<TypeId> lookup(Scope* scope, DefId def, bool prototype = true);
/** /**
* Adds a new constraint with no dependencies to a given scope. * Adds a new constraint with no dependencies to a given scope.

View file

@ -132,7 +132,6 @@ struct ConstraintSolver
bool tryDispatch(const SetIndexerConstraint& c, NotNull<const Constraint> constraint, bool force); bool tryDispatch(const SetIndexerConstraint& c, NotNull<const Constraint> constraint, bool force);
bool tryDispatch(const SingletonOrTopTypeConstraint& c, NotNull<const Constraint> constraint); bool tryDispatch(const SingletonOrTopTypeConstraint& c, NotNull<const Constraint> constraint);
bool tryDispatch(const UnpackConstraint& c, NotNull<const Constraint> constraint); bool tryDispatch(const UnpackConstraint& c, NotNull<const Constraint> constraint);
bool tryDispatch(const RefineConstraint& c, NotNull<const Constraint> constraint, bool force);
bool tryDispatch(const SetOpConstraint& c, NotNull<const Constraint> constraint, bool force); bool tryDispatch(const SetOpConstraint& c, NotNull<const Constraint> constraint, bool force);
bool tryDispatch(const ReduceConstraint& c, NotNull<const Constraint> constraint, bool force); bool tryDispatch(const ReduceConstraint& c, NotNull<const Constraint> constraint, bool force);
bool tryDispatch(const ReducePackConstraint& c, NotNull<const Constraint> constraint, bool force); bool tryDispatch(const ReducePackConstraint& c, NotNull<const Constraint> constraint, bool force);

View file

@ -74,8 +74,15 @@ private:
struct DfgScope struct DfgScope
{ {
enum ScopeType
{
Linear,
Loop,
Function,
};
DfgScope* parent; DfgScope* parent;
bool isLoopScope; ScopeType scopeType;
using Bindings = DenseHashMap<Symbol, const Def*>; using Bindings = DenseHashMap<Symbol, const Def*>;
using Props = DenseHashMap<const Def*, std::unordered_map<std::string, const Def*>>; using Props = DenseHashMap<const Def*, std::unordered_map<std::string, const Def*>>;
@ -117,11 +124,21 @@ private:
std::vector<std::unique_ptr<DfgScope>> scopes; std::vector<std::unique_ptr<DfgScope>> scopes;
DfgScope* childScope(DfgScope* scope, bool isLoopScope = false); struct FunctionCapture
{
std::vector<DefId> captureDefs;
std::vector<DefId> allVersions;
size_t versionOffset = 0;
};
DenseHashMap<Symbol, FunctionCapture> captures{Symbol{}};
void resolveCaptures();
DfgScope* childScope(DfgScope* scope, DfgScope::ScopeType scopeType = DfgScope::Linear);
void join(DfgScope* p, 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 joinBindings(DfgScope* p, const DfgScope& a, const DfgScope& b);
void joinProps(DfgScope::Props& p, const DfgScope::Props& a, const DfgScope::Props& b); void joinProps(DfgScope* p, const DfgScope& a, const DfgScope& b);
DefId lookup(DfgScope* scope, Symbol symbol); DefId lookup(DfgScope* scope, Symbol symbol);
DefId lookup(DfgScope* scope, DefId def, const std::string& key); DefId lookup(DfgScope* scope, DefId def, const std::string& key);
@ -167,11 +184,11 @@ private:
DataFlowResult visitExpr(DfgScope* scope, AstExprError* error); DataFlowResult visitExpr(DfgScope* scope, AstExprError* error);
void visitLValue(DfgScope* scope, AstExpr* e, DefId incomingDef, bool isCompoundAssignment = false); void visitLValue(DfgScope* scope, AstExpr* e, DefId incomingDef, bool isCompoundAssignment = false);
void visitLValue(DfgScope* scope, AstExprLocal* l, DefId incomingDef, bool isCompoundAssignment); DefId visitLValue(DfgScope* scope, AstExprLocal* l, DefId incomingDef, bool isCompoundAssignment);
void visitLValue(DfgScope* scope, AstExprGlobal* g, DefId incomingDef, bool isCompoundAssignment); DefId visitLValue(DfgScope* scope, AstExprGlobal* g, DefId incomingDef, bool isCompoundAssignment);
void visitLValue(DfgScope* scope, AstExprIndexName* i, DefId incomingDef); DefId visitLValue(DfgScope* scope, AstExprIndexName* i, DefId incomingDef);
void visitLValue(DfgScope* scope, AstExprIndexExpr* i, DefId incomingDef); DefId visitLValue(DfgScope* scope, AstExprIndexExpr* i, DefId incomingDef);
void visitLValue(DfgScope* scope, AstExprError* e, DefId incomingDef); DefId visitLValue(DfgScope* scope, AstExprError* e, DefId incomingDef);
void visitType(DfgScope* scope, AstType* t); void visitType(DfgScope* scope, AstType* t);
void visitType(DfgScope* scope, AstTypeReference* r); void visitType(DfgScope* scope, AstTypeReference* r);

View file

@ -73,6 +73,7 @@ const T* get(DefId def)
} }
bool containsSubscriptedDefinition(DefId def); bool containsSubscriptedDefinition(DefId def);
void collectOperands(DefId def, std::vector<DefId>* operands);
struct DefArena struct DefArena
{ {

View file

@ -118,6 +118,12 @@ public:
class const_iterator class const_iterator
{ {
public: public:
using value_type = T;
using reference = T&;
using pointer = T*;
using difference_type = ptrdiff_t;
using iterator_category = std::forward_iterator_tag;
const_iterator(typename Impl::const_iterator impl, typename Impl::const_iterator end) const_iterator(typename Impl::const_iterator impl, typename Impl::const_iterator end)
: impl(impl) : impl(impl)
, end(end) , end(end)

View file

@ -176,7 +176,7 @@ struct PrimitiveType
} }
}; };
// Singleton types https://github.com/Roblox/luau/blob/master/rfcs/syntax-singleton-types.md // Singleton types https://github.com/luau-lang/rfcs/blob/master/docs/syntax-singleton-types.md
// Types for true and false // Types for true and false
struct BooleanSingleton struct BooleanSingleton
{ {

View file

@ -162,6 +162,8 @@ struct BuiltinTypeFamilies
TypeFamily leFamily; TypeFamily leFamily;
TypeFamily eqFamily; TypeFamily eqFamily;
TypeFamily refineFamily;
void addToScope(NotNull<TypeArena> arena, NotNull<Scope> scope) const; void addToScope(NotNull<TypeArena> arena, NotNull<Scope> scope) const;
}; };

View file

@ -200,6 +200,23 @@ struct AstJsonEncoder : public AstVisitor
{ {
writeString(name.value ? name.value : ""); writeString(name.value ? name.value : "");
} }
void write(std::optional<AstArgumentName> name)
{
if (name)
write(*name);
else
writeRaw("null");
}
void write(AstArgumentName name)
{
writeRaw("{");
bool c = pushComma();
writeType("AstArgumentName");
write("name", name.first);
write("location", name.second);
popComma(c);
writeRaw("}");
}
void write(const Position& position) void write(const Position& position)
{ {
@ -848,6 +865,7 @@ struct AstJsonEncoder : public AstVisitor
PROP(generics); PROP(generics);
PROP(genericPacks); PROP(genericPacks);
PROP(argTypes); PROP(argTypes);
PROP(argNames);
PROP(returnTypes); PROP(returnTypes);
}); });
} }
@ -902,6 +920,22 @@ struct AstJsonEncoder : public AstVisitor
}); });
} }
bool visit(class AstTypeSingletonBool* node) override
{
writeNode(node, "AstTypeSingletonBool", [&]() {
write("value", node->value);
});
return false;
}
bool visit(class AstTypeSingletonString* node) override
{
writeNode(node, "AstTypeSingletonString", [&]() {
write("value", node->value);
});
return false;
}
bool visit(class AstExprGroup* node) override bool visit(class AstExprGroup* node) override
{ {
write(node); write(node);

View file

@ -205,7 +205,7 @@ ScopePtr ConstraintGenerator::childScope(AstNode* node, const ScopePtr& parent)
return scope; return scope;
} }
std::optional<TypeId> ConstraintGenerator::lookup(Scope* scope, DefId def) std::optional<TypeId> ConstraintGenerator::lookup(Scope* scope, DefId def, bool prototype)
{ {
if (get<Cell>(def)) if (get<Cell>(def))
return scope->lookup(def); return scope->lookup(def);
@ -213,22 +213,24 @@ std::optional<TypeId> ConstraintGenerator::lookup(Scope* scope, DefId def)
{ {
if (auto found = scope->lookup(def)) if (auto found = scope->lookup(def))
return *found; return *found;
else if (!prototype)
return std::nullopt;
TypeId res = builtinTypes->neverType; TypeId res = builtinTypes->neverType;
for (DefId operand : phi->operands) for (DefId operand : phi->operands)
{ {
// `scope->lookup(operand)` may return nothing because it could be a phi node of globals, but one of // `scope->lookup(operand)` may return nothing because we only bind a type to that operand
// the operand of that global has never been assigned a type, and so it should be an error. // once we've seen that particular `DefId`. In this case, we need to prototype those types
// e.g. // and use those at a later time.
// ``` std::optional<TypeId> ty = lookup(scope, operand, /*prototype*/false);
// if foo() then if (!ty)
// g = 5 {
// end ty = arena->addType(BlockedType{});
// -- `g` here is a phi node of the assignment to `g`, or the original revision of `g` before the branch. rootScope->lvalueTypes[operand] = *ty;
// ``` }
TypeId ty = scope->lookup(operand).value_or(builtinTypes->errorRecoveryType());
res = simplifyUnion(builtinTypes, arena, res, ty).result; res = simplifyUnion(builtinTypes, arena, res, *ty).result;
} }
scope->lvalueTypes[def] = res; scope->lvalueTypes[def] = res;
@ -255,7 +257,7 @@ void ConstraintGenerator::unionRefinements(const RefinementContext& lhs, const R
return types[0]; return types[0];
else if (2 == types.size()) else if (2 == types.size())
{ {
// TODO: It may be advantageous to create a RefineConstraint here when there are blockedTypes. // TODO: It may be advantageous to introduce a refine type family here when there are blockedTypes.
SimplifyResult sr = simplifyIntersection(builtinTypes, arena, types[0], types[1]); SimplifyResult sr = simplifyIntersection(builtinTypes, arena, types[0], types[1]);
if (sr.blockedTypes.empty()) if (sr.blockedTypes.empty())
return sr.result; return sr.result;
@ -439,10 +441,14 @@ void ConstraintGenerator::applyRefinements(const ScopePtr& scope, Location locat
{ {
if (mustDeferIntersection(ty) || mustDeferIntersection(dt)) if (mustDeferIntersection(ty) || mustDeferIntersection(dt))
{ {
TypeId r = arena->addType(BlockedType{}); TypeId resultType = arena->addType(TypeFamilyInstanceType{
addConstraint(scope, location, RefineConstraint{RefineConstraint::Intersection, r, ty, dt}); NotNull{&kBuiltinTypeFamilies.refineFamily},
{ty, dt},
{},
});
addConstraint(scope, location, ReduceConstraint{resultType});
ty = r; ty = resultType;
} }
else else
{ {
@ -861,7 +867,7 @@ ControlFlow ConstraintGenerator::visit(const ScopePtr& scope, AstStatFunction* f
DenseHashSet<Constraint*> excludeList{nullptr}; DenseHashSet<Constraint*> excludeList{nullptr};
DefId def = dfg->getDef(function->name); DefId def = dfg->getDef(function->name);
std::optional<TypeId> existingFunctionTy = scope->lookup(def); std::optional<TypeId> existingFunctionTy = lookup(scope.get(), def);
if (AstExprLocal* localName = function->name->as<AstExprLocal>()) if (AstExprLocal* localName = function->name->as<AstExprLocal>())
{ {
@ -1003,9 +1009,6 @@ ControlFlow ConstraintGenerator::visit(const ScopePtr& scope, AstStatAssign* ass
checkLValue(scope, lvalue, assignee); checkLValue(scope, lvalue, assignee);
assignees.push_back(assignee); assignees.push_back(assignee);
DefId def = dfg->getDef(lvalue);
scope->lvalueTypes[def] = assignee;
} }
TypePackId resultPack = checkPack(scope, assign->values).tp; TypePackId resultPack = checkPack(scope, assign->values).tp;
@ -1724,16 +1727,14 @@ Inference ConstraintGenerator::check(const ScopePtr& scope, AstExprGlobal* globa
/* prepopulateGlobalScope() has already added all global functions to the environment by this point, so any /* prepopulateGlobalScope() has already added all global functions to the environment by this point, so any
* global that is not already in-scope is definitely an unknown symbol. * global that is not already in-scope is definitely an unknown symbol.
*/ */
if (auto ty = lookup(scope.get(), def)) if (auto ty = lookup(scope.get(), def, /*prototype=*/false))
return Inference{*ty, refinementArena.proposition(key, builtinTypes->truthyType)};
else if (auto ty = scope->lookup(global->name))
{ {
rootScope->lvalueTypes[def] = *ty; rootScope->lvalueTypes[def] = *ty;
return Inference{*ty, refinementArena.proposition(key, builtinTypes->truthyType)}; return Inference{*ty, refinementArena.proposition(key, builtinTypes->truthyType)};
} }
else else
{ {
reportError(global->location, UnknownSymbol{global->name.value}); reportError(global->location, UnknownSymbol{global->name.value, UnknownSymbol::Binding});
return Inference{builtinTypes->errorRecoveryType()}; return Inference{builtinTypes->errorRecoveryType()};
} }
} }
@ -3110,6 +3111,16 @@ struct GlobalPrepopulator : AstVisitor
return true; return true;
} }
bool visit(AstType*) override
{
return true;
}
bool visit(class AstTypePack* node) override
{
return true;
}
}; };
void ConstraintGenerator::prepopulateGlobalScope(const ScopePtr& globalScope, AstStatBlock* program) void ConstraintGenerator::prepopulateGlobalScope(const ScopePtr& globalScope, AstStatBlock* program)

View file

@ -545,8 +545,6 @@ bool ConstraintSolver::tryDispatch(NotNull<const Constraint> constraint, bool fo
success = tryDispatch(*sottc, constraint); success = tryDispatch(*sottc, constraint);
else if (auto uc = get<UnpackConstraint>(*constraint)) else if (auto uc = get<UnpackConstraint>(*constraint))
success = tryDispatch(*uc, constraint); success = tryDispatch(*uc, constraint);
else if (auto rc = get<RefineConstraint>(*constraint))
success = tryDispatch(*rc, constraint, force);
else if (auto soc = get<SetOpConstraint>(*constraint)) else if (auto soc = get<SetOpConstraint>(*constraint))
success = tryDispatch(*soc, constraint, force); success = tryDispatch(*soc, constraint, force);
else if (auto rc = get<ReduceConstraint>(*constraint)) else if (auto rc = get<ReduceConstraint>(*constraint))
@ -887,9 +885,9 @@ bool ConstraintSolver::tryDispatch(const TypeAliasExpansionConstraint& c, NotNul
// In order to prevent infinite types from being expanded and causing us to // In order to prevent infinite types from being expanded and causing us to
// cycle infinitely, we need to scan the type function for cases where we // cycle infinitely, we need to scan the type function for cases where we
// expand the same alias with different type saturatedTypeArguments. See // expand the same alias with different type saturatedTypeArguments. See
// https://github.com/Roblox/luau/pull/68 for the RFC responsible for this. // https://github.com/luau-lang/luau/pull/68 for the RFC responsible for
// This is a little nicer than using a recursion limit because we can catch // this. This is a little nicer than using a recursion limit because we can
// the infinite expansion before actually trying to expand it. // catch the infinite expansion before actually trying to expand it.
InfiniteTypeFinder itf{this, signature, constraint->scope}; InfiniteTypeFinder itf{this, signature, constraint->scope};
itf.traverse(tf->type); itf.traverse(tf->type);
@ -1505,151 +1503,6 @@ bool ConstraintSolver::tryDispatch(const UnpackConstraint& c, NotNull<const Cons
return true; return true;
} }
namespace
{
/*
* Search for types that prevent us from being ready to dispatch a particular
* RefineConstraint.
*/
struct FindRefineConstraintBlockers : TypeOnceVisitor
{
DenseHashSet<TypeId> found{nullptr};
bool visit(TypeId ty, const BlockedType&) override
{
found.insert(ty);
return false;
}
bool visit(TypeId ty, const PendingExpansionType&) override
{
found.insert(ty);
return false;
}
bool visit(TypeId ty, const ClassType&) override
{
return false;
}
};
} // namespace
static bool isNegatedAny(TypeId ty)
{
ty = follow(ty);
const NegationType* nt = get<NegationType>(ty);
if (!nt)
return false;
TypeId negatedTy = follow(nt->ty);
return bool(get<AnyType>(negatedTy));
}
bool ConstraintSolver::tryDispatch(const RefineConstraint& c, NotNull<const Constraint> constraint, bool force)
{
if (isBlocked(c.discriminant))
return block(c.discriminant, constraint);
FindRefineConstraintBlockers fbt;
fbt.traverse(c.discriminant);
if (!fbt.found.empty())
{
bool foundOne = false;
for (TypeId blocked : fbt.found)
{
if (blocked == c.type)
continue;
block(blocked, constraint);
foundOne = true;
}
if (foundOne)
return false;
}
/* HACK: Refinements sometimes produce a type T & ~any under the assumption
* that ~any is the same as any. This is so so weird, but refinements needs
* some way to say "I may refine this, but I'm not sure."
*
* It does this by refining on a blocked type and deferring the decision
* until it is unblocked.
*
* Refinements also get negated, so we wind up with types like T & ~*blocked*
*
* We need to treat T & ~any as T in this case.
*/
if (c.mode == RefineConstraint::Intersection && isNegatedAny(c.discriminant))
{
asMutable(c.resultType)->ty.emplace<BoundType>(c.type);
unblock(c.resultType, constraint->location);
return true;
}
const TypeId type = follow(c.type);
if (hasUnresolvedConstraints(type))
return block(type, constraint);
LUAU_ASSERT(get<BlockedType>(c.resultType));
if (type == c.resultType)
{
/*
* Sometimes, we get a constraint of the form
*
* *blocked-N* ~ refine *blocked-N* & U
*
* The constraint essentially states that a particular type is a
* refinement of itself. This is weird and I think vacuous.
*
* I *believe* it is safe to replace the result with a fresh type that
* is constrained by U. We effect this by minting a fresh type for the
* result when U = any, else we bind the result to whatever discriminant
* was offered.
*/
if (get<AnyType>(follow(c.discriminant)))
{
TypeId f = freshType(arena, builtinTypes, constraint->scope);
asMutable(c.resultType)->ty.emplace<BoundType>(f);
}
else
asMutable(c.resultType)->ty.emplace<BoundType>(c.discriminant);
unblock(c.resultType, constraint->location);
return true;
}
auto [result, blockedTypes] = c.mode == RefineConstraint::Intersection ? simplifyIntersection(builtinTypes, NotNull{arena}, type, c.discriminant)
: simplifyUnion(builtinTypes, NotNull{arena}, type, c.discriminant);
if (!force && !blockedTypes.empty())
return block(blockedTypes, constraint);
switch (shouldSuppressErrors(normalizer, c.type))
{
case ErrorSuppression::Suppress:
{
auto resultOrError = simplifyUnion(builtinTypes, arena, result, builtinTypes->errorType).result;
asMutable(c.resultType)->ty.emplace<BoundType>(resultOrError);
break;
}
case ErrorSuppression::DoNotSuppress:
asMutable(c.resultType)->ty.emplace<BoundType>(result);
break;
case ErrorSuppression::NormalizationFailed:
reportError(NormalizationTooComplex{}, constraint->location);
break;
}
unblock(c.resultType, constraint->location);
return true;
}
bool ConstraintSolver::tryDispatch(const SetOpConstraint& c, NotNull<const Constraint> constraint, bool force) bool ConstraintSolver::tryDispatch(const SetOpConstraint& c, NotNull<const Constraint> constraint, bool force)
{ {
bool blocked = false; bool blocked = false;

View file

@ -116,7 +116,7 @@ bool DfgScope::canUpdateDefinition(Symbol symbol) const
{ {
if (current->bindings.find(symbol)) if (current->bindings.find(symbol))
return true; return true;
else if (current->isLoopScope) else if (current->scopeType == DfgScope::Loop)
return false; return false;
} }
@ -129,7 +129,7 @@ bool DfgScope::canUpdateDefinition(DefId def, const std::string& key) const
{ {
if (auto props = current->props.find(def)) if (auto props = current->props.find(def))
return true; return true;
else if (current->isLoopScope) else if (current->scopeType == DfgScope::Loop)
return false; return false;
} }
@ -144,6 +144,7 @@ DataFlowGraph DataFlowGraphBuilder::build(AstStatBlock* block, NotNull<InternalE
builder.handle = handle; builder.handle = handle;
builder.moduleScope = builder.childScope(nullptr); // nullptr is the root DFG scope. builder.moduleScope = builder.childScope(nullptr); // nullptr is the root DFG scope.
builder.visitBlockWithoutChildScope(builder.moduleScope, block); builder.visitBlockWithoutChildScope(builder.moduleScope, block);
builder.resolveCaptures();
if (FFlag::DebugLuauFreezeArena) if (FFlag::DebugLuauFreezeArena)
{ {
@ -154,43 +155,64 @@ DataFlowGraph DataFlowGraphBuilder::build(AstStatBlock* block, NotNull<InternalE
return std::move(builder.graph); return std::move(builder.graph);
} }
DfgScope* DataFlowGraphBuilder::childScope(DfgScope* scope, bool isLoopScope) void DataFlowGraphBuilder::resolveCaptures()
{ {
return scopes.emplace_back(new DfgScope{scope, isLoopScope}).get(); for (const auto& [_, capture] : captures)
{
std::vector<DefId> operands;
for (size_t i = capture.versionOffset; i < capture.allVersions.size(); ++i)
collectOperands(capture.allVersions[i], &operands);
for (DefId captureDef : capture.captureDefs)
{
Phi* phi = const_cast<Phi*>(get<Phi>(captureDef));
LUAU_ASSERT(phi);
LUAU_ASSERT(phi->operands.empty());
phi->operands = operands;
}
}
}
DfgScope* DataFlowGraphBuilder::childScope(DfgScope* scope, DfgScope::ScopeType scopeType)
{
return scopes.emplace_back(new DfgScope{scope, scopeType}).get();
} }
void DataFlowGraphBuilder::join(DfgScope* p, DfgScope* a, DfgScope* b) void DataFlowGraphBuilder::join(DfgScope* p, DfgScope* a, DfgScope* b)
{ {
joinBindings(p->bindings, a->bindings, b->bindings); joinBindings(p, *a, *b);
joinProps(p->props, a->props, b->props); joinProps(p, *a, *b);
} }
void DataFlowGraphBuilder::joinBindings(DfgScope::Bindings& p, const DfgScope::Bindings& a, const DfgScope::Bindings& b) void DataFlowGraphBuilder::joinBindings(DfgScope* p, const DfgScope& a, const DfgScope& b)
{ {
for (const auto& [sym, def1] : a) for (const auto& [sym, def1] : a.bindings)
{ {
if (auto def2 = b.find(sym)) if (auto def2 = b.bindings.find(sym))
p[sym] = defArena->phi(NotNull{def1}, NotNull{*def2}); p->bindings[sym] = defArena->phi(NotNull{def1}, NotNull{*def2});
else if (auto def2 = p.find(sym)) else if (auto def2 = p->lookup(sym))
p[sym] = defArena->phi(NotNull{def1}, NotNull{*def2}); p->bindings[sym] = defArena->phi(NotNull{def1}, NotNull{*def2});
} }
for (const auto& [sym, def1] : b) for (const auto& [sym, def1] : b.bindings)
{ {
if (auto def2 = p.find(sym)) if (auto def2 = p->lookup(sym))
p[sym] = defArena->phi(NotNull{def1}, NotNull{*def2}); p->bindings[sym] = defArena->phi(NotNull{def1}, NotNull{*def2});
} }
} }
void DataFlowGraphBuilder::joinProps(DfgScope::Props& p, const DfgScope::Props& a, const DfgScope::Props& b) void DataFlowGraphBuilder::joinProps(DfgScope* result, const DfgScope& a, const DfgScope& b)
{ {
auto phinodify = [this](auto& p, const auto& a, const auto& b) mutable { auto phinodify = [this](DfgScope* scope, const auto& a, const auto& b, DefId parent) mutable {
auto& p = scope->props[parent];
for (const auto& [k, defA] : a) for (const auto& [k, defA] : a)
{ {
if (auto it = b.find(k); it != b.end()) if (auto it = b.find(k); it != b.end())
p[k] = defArena->phi(NotNull{it->second}, NotNull{defA}); p[k] = defArena->phi(NotNull{it->second}, NotNull{defA});
else if (auto it = p.find(k); it != p.end()) else if (auto it = p.find(k); it != p.end())
p[k] = defArena->phi(NotNull{it->second}, NotNull{defA}); p[k] = defArena->phi(NotNull{it->second}, NotNull{defA});
else if (auto def2 = scope->lookup(parent, k))
p[k] = defArena->phi(*def2, NotNull{defA});
else else
p[k] = defA; p[k] = defA;
} }
@ -201,50 +223,72 @@ void DataFlowGraphBuilder::joinProps(DfgScope::Props& p, const DfgScope::Props&
continue; continue;
else if (auto it = p.find(k); it != p.end()) else if (auto it = p.find(k); it != p.end())
p[k] = defArena->phi(NotNull{it->second}, NotNull{defB}); p[k] = defArena->phi(NotNull{it->second}, NotNull{defB});
else if (auto def2 = scope->lookup(parent, k))
p[k] = defArena->phi(*def2, NotNull{defB});
else else
p[k] = defB; p[k] = defB;
} }
}; };
for (const auto& [def, a1] : a) for (const auto& [def, a1] : a.props)
{ {
p.try_insert(def, {}); result->props.try_insert(def, {});
if (auto a2 = b.find(def)) if (auto a2 = b.props.find(def))
phinodify(p[def], a1, *a2); phinodify(result, a1, *a2, NotNull{def});
else if (auto a2 = p.find(def)) else if (auto a2 = result->props.find(def))
phinodify(p[def], a1, *a2); phinodify(result, a1, *a2, NotNull{def});
} }
for (const auto& [def, a1] : b) for (const auto& [def, a1] : b.props)
{ {
p.try_insert(def, {}); result->props.try_insert(def, {});
if (a.find(def)) if (a.props.find(def))
continue; continue;
else if (auto a2 = p.find(def)) else if (auto a2 = result->props.find(def))
phinodify(p[def], a1, *a2); phinodify(result, a1, *a2, NotNull{def});
} }
} }
DefId DataFlowGraphBuilder::lookup(DfgScope* scope, Symbol symbol) DefId DataFlowGraphBuilder::lookup(DfgScope* scope, Symbol symbol)
{ {
if (auto found = scope->lookup(symbol)) for (DfgScope* current = scope; current; current = current->parent)
return *found;
else
{ {
DefId result = defArena->freshCell(); if (auto found = current->bindings.find(symbol))
if (symbol.local) return NotNull{*found};
scope->bindings[symbol] = result; else if (current->scopeType == DfgScope::Function)
else {
moduleScope->bindings[symbol] = result; FunctionCapture& capture = captures[symbol];
return result; DefId captureDef = defArena->phi({});
capture.captureDefs.push_back(captureDef);
scope->bindings[symbol] = captureDef;
return NotNull{captureDef};
}
} }
DefId result = defArena->freshCell();
scope->bindings[symbol] = result;
captures[symbol].allVersions.push_back(result);
return result;
} }
DefId DataFlowGraphBuilder::lookup(DfgScope* scope, DefId def, const std::string& key) DefId DataFlowGraphBuilder::lookup(DfgScope* scope, DefId def, const std::string& key)
{ {
if (auto found = scope->lookup(def, key)) for (DfgScope* current = scope; current; current = current->parent)
return *found; {
else if (auto phi = get<Phi>(def)) if (auto props = current->props.find(def))
{
if (auto it = props->find(key); it != props->end())
return NotNull{it->second};
}
else if (auto phi = get<Phi>(def); phi && phi->operands.empty()) // Unresolved phi nodes
{
DefId result = defArena->freshCell();
scope->props[def][key] = result;
return result;
}
}
if (auto phi = get<Phi>(def))
{ {
std::vector<DefId> defs; std::vector<DefId> defs;
for (DefId operand : phi->operands) for (DefId operand : phi->operands)
@ -361,7 +405,7 @@ ControlFlow DataFlowGraphBuilder::visit(DfgScope* scope, AstStatIf* i)
ControlFlow DataFlowGraphBuilder::visit(DfgScope* scope, AstStatWhile* w) ControlFlow DataFlowGraphBuilder::visit(DfgScope* scope, AstStatWhile* w)
{ {
// TODO(controlflow): entry point has a back edge from exit point // TODO(controlflow): entry point has a back edge from exit point
DfgScope* whileScope = childScope(scope, /*isLoopScope=*/true); DfgScope* whileScope = childScope(scope, DfgScope::Loop);
visitExpr(whileScope, w->condition); visitExpr(whileScope, w->condition);
visit(whileScope, w->body); visit(whileScope, w->body);
@ -373,7 +417,7 @@ ControlFlow DataFlowGraphBuilder::visit(DfgScope* scope, AstStatWhile* w)
ControlFlow DataFlowGraphBuilder::visit(DfgScope* scope, AstStatRepeat* r) ControlFlow DataFlowGraphBuilder::visit(DfgScope* scope, AstStatRepeat* r)
{ {
// TODO(controlflow): entry point has a back edge from exit point // TODO(controlflow): entry point has a back edge from exit point
DfgScope* repeatScope = childScope(scope, /*isLoopScope=*/true); DfgScope* repeatScope = childScope(scope, DfgScope::Loop);
visitBlockWithoutChildScope(repeatScope, r->body); visitBlockWithoutChildScope(repeatScope, r->body);
visitExpr(repeatScope, r->condition); visitExpr(repeatScope, r->condition);
@ -427,8 +471,17 @@ ControlFlow DataFlowGraphBuilder::visit(DfgScope* scope, AstStatLocal* l)
// make sure that the non-aliased defs are also marked as a subscript for refinements. // make sure that the non-aliased defs are also marked as a subscript for refinements.
bool subscripted = i < defs.size() && containsSubscriptedDefinition(defs[i]); bool subscripted = i < defs.size() && containsSubscriptedDefinition(defs[i]);
DefId def = defArena->freshCell(subscripted); DefId def = defArena->freshCell(subscripted);
if (i < l->values.size)
{
AstExpr* e = l->values.data[i];
if (const AstExprTable* tbl = e->as<AstExprTable>())
{
def = defs[i];
}
}
graph.localDefs[local] = def; graph.localDefs[local] = def;
scope->bindings[local] = def; scope->bindings[local] = def;
captures[local].allVersions.push_back(def);
} }
return ControlFlow::None; return ControlFlow::None;
@ -436,7 +489,7 @@ ControlFlow DataFlowGraphBuilder::visit(DfgScope* scope, AstStatLocal* l)
ControlFlow DataFlowGraphBuilder::visit(DfgScope* scope, AstStatFor* f) ControlFlow DataFlowGraphBuilder::visit(DfgScope* scope, AstStatFor* f)
{ {
DfgScope* forScope = childScope(scope, /*isLoopScope=*/true); DfgScope* forScope = childScope(scope, DfgScope::Loop);
visitExpr(scope, f->from); visitExpr(scope, f->from);
visitExpr(scope, f->to); visitExpr(scope, f->to);
@ -449,6 +502,7 @@ ControlFlow DataFlowGraphBuilder::visit(DfgScope* scope, AstStatFor* f)
DefId def = defArena->freshCell(); DefId def = defArena->freshCell();
graph.localDefs[f->var] = def; graph.localDefs[f->var] = def;
scope->bindings[f->var] = def; scope->bindings[f->var] = def;
captures[f->var].allVersions.push_back(def);
// TODO(controlflow): entry point has a back edge from exit point // TODO(controlflow): entry point has a back edge from exit point
visit(forScope, f->body); visit(forScope, f->body);
@ -460,7 +514,7 @@ ControlFlow DataFlowGraphBuilder::visit(DfgScope* scope, AstStatFor* f)
ControlFlow DataFlowGraphBuilder::visit(DfgScope* scope, AstStatForIn* f) ControlFlow DataFlowGraphBuilder::visit(DfgScope* scope, AstStatForIn* f)
{ {
DfgScope* forScope = childScope(scope, /*isLoopScope=*/true); DfgScope* forScope = childScope(scope, DfgScope::Loop);
for (AstLocal* local : f->vars) for (AstLocal* local : f->vars)
{ {
@ -470,6 +524,7 @@ ControlFlow DataFlowGraphBuilder::visit(DfgScope* scope, AstStatForIn* f)
DefId def = defArena->freshCell(); DefId def = defArena->freshCell();
graph.localDefs[local] = def; graph.localDefs[local] = def;
forScope->bindings[local] = def; forScope->bindings[local] = def;
captures[local].allVersions.push_back(def);
} }
// TODO(controlflow): entry point has a back edge from exit point // TODO(controlflow): entry point has a back edge from exit point
@ -527,10 +582,21 @@ ControlFlow DataFlowGraphBuilder::visit(DfgScope* scope, AstStatFunction* f)
// //
// which is evidence that references to variables must be a phi node of all possible definitions, // which is evidence that references to variables must be a phi node of all possible definitions,
// but for bug compatibility, we'll assume the same thing here. // but for bug compatibility, we'll assume the same thing here.
DefId prototype = defArena->freshCell(); visitLValue(scope, f->name, defArena->freshCell());
visitLValue(scope, f->name, prototype);
visitExpr(scope, f->func); visitExpr(scope, f->func);
if (auto local = f->name->as<AstExprLocal>())
{
// local f
// function f()
// if cond() then
// f() -- should reference only the function version and other future version, and nothing prior
// end
// end
FunctionCapture& capture = captures[local->local];
capture.versionOffset = capture.allVersions.size() - 1;
}
return ControlFlow::None; return ControlFlow::None;
} }
@ -539,6 +605,7 @@ ControlFlow DataFlowGraphBuilder::visit(DfgScope* scope, AstStatLocalFunction* l
DefId def = defArena->freshCell(); DefId def = defArena->freshCell();
graph.localDefs[l->name] = def; graph.localDefs[l->name] = def;
scope->bindings[l->name] = def; scope->bindings[l->name] = def;
captures[l->name].allVersions.push_back(def);
visitExpr(scope, l->func); visitExpr(scope, l->func);
return ControlFlow::None; return ControlFlow::None;
@ -559,6 +626,7 @@ ControlFlow DataFlowGraphBuilder::visit(DfgScope* scope, AstStatDeclareGlobal* d
DefId def = defArena->freshCell(); DefId def = defArena->freshCell();
graph.declaredDefs[d] = def; graph.declaredDefs[d] = def;
scope->bindings[d->name] = def; scope->bindings[d->name] = def;
captures[d->name].allVersions.push_back(def);
visitType(scope, d->type); visitType(scope, d->type);
@ -570,6 +638,7 @@ ControlFlow DataFlowGraphBuilder::visit(DfgScope* scope, AstStatDeclareFunction*
DefId def = defArena->freshCell(); DefId def = defArena->freshCell();
graph.declaredDefs[d] = def; graph.declaredDefs[d] = def;
scope->bindings[d->name] = def; scope->bindings[d->name] = def;
captures[d->name].allVersions.push_back(def);
DfgScope* unreachable = childScope(scope); DfgScope* unreachable = childScope(scope);
visitGenerics(unreachable, d->generics); visitGenerics(unreachable, d->generics);
@ -669,14 +738,9 @@ DataFlowResult DataFlowGraphBuilder::visitExpr(DfgScope* scope, AstExprGroup* gr
DataFlowResult DataFlowGraphBuilder::visitExpr(DfgScope* scope, AstExprLocal* l) DataFlowResult DataFlowGraphBuilder::visitExpr(DfgScope* scope, AstExprLocal* l)
{ {
// DfgScope::lookup is intentional here: we want to be able to ice. DefId def = lookup(scope, l->local);
if (auto def = scope->lookup(l->local)) const RefinementKey* key = keyArena->leaf(def);
{ return {def, key};
const RefinementKey* key = keyArena->leaf(*def);
return {*def, key};
}
handle->ice("DFG: AstExprLocal came before its declaration?");
} }
DataFlowResult DataFlowGraphBuilder::visitExpr(DfgScope* scope, AstExprGlobal* g) DataFlowResult DataFlowGraphBuilder::visitExpr(DfgScope* scope, AstExprGlobal* g)
@ -718,12 +782,12 @@ DataFlowResult DataFlowGraphBuilder::visitExpr(DfgScope* scope, AstExprIndexExpr
return {def, keyArena->node(parentKey, def, index)}; return {def, keyArena->node(parentKey, def, index)};
} }
return {defArena->freshCell(/* subscripted= */true), nullptr}; return {defArena->freshCell(/* subscripted= */ true), nullptr};
} }
DataFlowResult DataFlowGraphBuilder::visitExpr(DfgScope* scope, AstExprFunction* f) DataFlowResult DataFlowGraphBuilder::visitExpr(DfgScope* scope, AstExprFunction* f)
{ {
DfgScope* signatureScope = childScope(scope); DfgScope* signatureScope = childScope(scope, DfgScope::Function);
if (AstLocal* self = f->self) if (AstLocal* self = f->self)
{ {
@ -733,6 +797,7 @@ DataFlowResult DataFlowGraphBuilder::visitExpr(DfgScope* scope, AstExprFunction*
DefId def = defArena->freshCell(); DefId def = defArena->freshCell();
graph.localDefs[self] = def; graph.localDefs[self] = def;
signatureScope->bindings[self] = def; signatureScope->bindings[self] = def;
captures[self].allVersions.push_back(def);
} }
for (AstLocal* param : f->args) for (AstLocal* param : f->args)
@ -743,6 +808,7 @@ DataFlowResult DataFlowGraphBuilder::visitExpr(DfgScope* scope, AstExprFunction*
DefId def = defArena->freshCell(); DefId def = defArena->freshCell();
graph.localDefs[param] = def; graph.localDefs[param] = def;
signatureScope->bindings[param] = def; signatureScope->bindings[param] = def;
captures[param].allVersions.push_back(def);
} }
if (f->varargAnnotation) if (f->varargAnnotation)
@ -766,14 +832,20 @@ DataFlowResult DataFlowGraphBuilder::visitExpr(DfgScope* scope, AstExprFunction*
DataFlowResult DataFlowGraphBuilder::visitExpr(DfgScope* scope, AstExprTable* t) DataFlowResult DataFlowGraphBuilder::visitExpr(DfgScope* scope, AstExprTable* t)
{ {
DefId tableCell = defArena->freshCell();
scope->props[tableCell] = {};
for (AstExprTable::Item item : t->items) for (AstExprTable::Item item : t->items)
{ {
DataFlowResult result = visitExpr(scope, item.value);
if (item.key) if (item.key)
{
visitExpr(scope, item.key); visitExpr(scope, item.key);
visitExpr(scope, item.value); if (auto string = item.key->as<AstExprConstantString>())
scope->props[tableCell][string->value.data] = result.def;
}
} }
return {defArena->freshCell(), nullptr}; return {tableCell, nullptr};
} }
DataFlowResult DataFlowGraphBuilder::visitExpr(DfgScope* scope, AstExprUnary* u) DataFlowResult DataFlowGraphBuilder::visitExpr(DfgScope* scope, AstExprUnary* u)
@ -827,41 +899,46 @@ DataFlowResult DataFlowGraphBuilder::visitExpr(DfgScope* scope, AstExprError* er
void DataFlowGraphBuilder::visitLValue(DfgScope* scope, AstExpr* e, DefId incomingDef, bool isCompoundAssignment) void DataFlowGraphBuilder::visitLValue(DfgScope* scope, AstExpr* e, DefId incomingDef, bool isCompoundAssignment)
{ {
if (auto l = e->as<AstExprLocal>()) auto go = [&]() {
return visitLValue(scope, l, incomingDef, isCompoundAssignment); if (auto l = e->as<AstExprLocal>())
else if (auto g = e->as<AstExprGlobal>()) return visitLValue(scope, l, incomingDef, isCompoundAssignment);
return visitLValue(scope, g, incomingDef, isCompoundAssignment); else if (auto g = e->as<AstExprGlobal>())
else if (auto i = e->as<AstExprIndexName>()) return visitLValue(scope, g, incomingDef, isCompoundAssignment);
return visitLValue(scope, i, incomingDef); else if (auto i = e->as<AstExprIndexName>())
else if (auto i = e->as<AstExprIndexExpr>()) return visitLValue(scope, i, incomingDef);
return visitLValue(scope, i, incomingDef); else if (auto i = e->as<AstExprIndexExpr>())
else if (auto error = e->as<AstExprError>()) return visitLValue(scope, i, incomingDef);
return visitLValue(scope, error, incomingDef); else if (auto error = e->as<AstExprError>())
else return visitLValue(scope, error, incomingDef);
handle->ice("Unknown AstExpr in DataFlowGraphBuilder::visitLValue"); else
handle->ice("Unknown AstExpr in DataFlowGraphBuilder::visitLValue");
};
graph.astDefs[e] = go();
} }
void DataFlowGraphBuilder::visitLValue(DfgScope* scope, AstExprLocal* l, DefId incomingDef, bool isCompoundAssignment) DefId DataFlowGraphBuilder::visitLValue(DfgScope* scope, AstExprLocal* l, DefId incomingDef, bool isCompoundAssignment)
{ {
// We need to keep the previous def around for a compound assignment. // We need to keep the previous def around for a compound assignment.
if (isCompoundAssignment) if (isCompoundAssignment)
{ {
if (auto def = scope->lookup(l->local)) DefId def = lookup(scope, l->local);
graph.compoundAssignDefs[l] = *def; graph.compoundAssignDefs[l] = def;
} }
// In order to avoid alias tracking, we need to clip the reference to the parent def. // In order to avoid alias tracking, we need to clip the reference to the parent def.
if (scope->canUpdateDefinition(l->local)) if (scope->canUpdateDefinition(l->local))
{ {
DefId updated = defArena->freshCell(containsSubscriptedDefinition(incomingDef)); DefId updated = defArena->freshCell(containsSubscriptedDefinition(incomingDef));
graph.astDefs[l] = updated;
scope->bindings[l->local] = updated; scope->bindings[l->local] = updated;
captures[l->local].allVersions.push_back(updated);
return updated;
} }
else else
visitExpr(scope, static_cast<AstExpr*>(l)); return visitExpr(scope, static_cast<AstExpr*>(l)).def;
} }
void DataFlowGraphBuilder::visitLValue(DfgScope* scope, AstExprGlobal* g, DefId incomingDef, bool isCompoundAssignment) DefId DataFlowGraphBuilder::visitLValue(DfgScope* scope, AstExprGlobal* g, DefId incomingDef, bool isCompoundAssignment)
{ {
// We need to keep the previous def around for a compound assignment. // We need to keep the previous def around for a compound assignment.
if (isCompoundAssignment) if (isCompoundAssignment)
@ -874,28 +951,29 @@ void DataFlowGraphBuilder::visitLValue(DfgScope* scope, AstExprGlobal* g, DefId
if (scope->canUpdateDefinition(g->name)) if (scope->canUpdateDefinition(g->name))
{ {
DefId updated = defArena->freshCell(containsSubscriptedDefinition(incomingDef)); DefId updated = defArena->freshCell(containsSubscriptedDefinition(incomingDef));
graph.astDefs[g] = updated;
scope->bindings[g->name] = updated; scope->bindings[g->name] = updated;
captures[g->name].allVersions.push_back(updated);
return updated;
} }
else else
visitExpr(scope, static_cast<AstExpr*>(g)); return visitExpr(scope, static_cast<AstExpr*>(g)).def;
} }
void DataFlowGraphBuilder::visitLValue(DfgScope* scope, AstExprIndexName* i, DefId incomingDef) DefId DataFlowGraphBuilder::visitLValue(DfgScope* scope, AstExprIndexName* i, DefId incomingDef)
{ {
DefId parentDef = visitExpr(scope, i->expr).def; DefId parentDef = visitExpr(scope, i->expr).def;
if (scope->canUpdateDefinition(parentDef, i->index.value)) if (scope->canUpdateDefinition(parentDef, i->index.value))
{ {
DefId updated = defArena->freshCell(containsSubscriptedDefinition(incomingDef)); DefId updated = defArena->freshCell(containsSubscriptedDefinition(incomingDef));
graph.astDefs[i] = updated;
scope->props[parentDef][i->index.value] = updated; scope->props[parentDef][i->index.value] = updated;
return updated;
} }
else else
visitExpr(scope, static_cast<AstExpr*>(i)); return visitExpr(scope, static_cast<AstExpr*>(i)).def;
} }
void DataFlowGraphBuilder::visitLValue(DfgScope* scope, AstExprIndexExpr* i, DefId incomingDef) DefId DataFlowGraphBuilder::visitLValue(DfgScope* scope, AstExprIndexExpr* i, DefId incomingDef)
{ {
DefId parentDef = visitExpr(scope, i->expr).def; DefId parentDef = visitExpr(scope, i->expr).def;
visitExpr(scope, i->index); visitExpr(scope, i->index);
@ -905,20 +983,19 @@ void DataFlowGraphBuilder::visitLValue(DfgScope* scope, AstExprIndexExpr* i, Def
if (scope->canUpdateDefinition(parentDef, string->value.data)) if (scope->canUpdateDefinition(parentDef, string->value.data))
{ {
DefId updated = defArena->freshCell(containsSubscriptedDefinition(incomingDef)); DefId updated = defArena->freshCell(containsSubscriptedDefinition(incomingDef));
graph.astDefs[i] = updated;
scope->props[parentDef][string->value.data] = updated; scope->props[parentDef][string->value.data] = updated;
return updated;
} }
else else
visitExpr(scope, static_cast<AstExpr*>(i)); return visitExpr(scope, static_cast<AstExpr*>(i)).def;
} }
else
graph.astDefs[i] = defArena->freshCell(); return defArena->freshCell(/*subscripted=*/true);
} }
void DataFlowGraphBuilder::visitLValue(DfgScope* scope, AstExprError* error, DefId incomingDef) DefId DataFlowGraphBuilder::visitLValue(DfgScope* scope, AstExprError* error, DefId incomingDef)
{ {
DefId def = visitExpr(scope, error).def; return visitExpr(scope, error).def;
graph.astDefs[error] = def;
} }
void DataFlowGraphBuilder::visitType(DfgScope* scope, AstType* t) void DataFlowGraphBuilder::visitType(DfgScope* scope, AstType* t)

View file

@ -19,17 +19,13 @@ bool containsSubscriptedDefinition(DefId def)
return false; return false;
} }
DefId DefArena::freshCell(bool subscripted) void collectOperands(DefId def, std::vector<DefId>* operands)
{ {
return NotNull{allocator.allocate(Def{Cell{subscripted}})}; LUAU_ASSERT(operands);
} if (std::find(operands->begin(), operands->end(), def) != operands->end())
static void collectOperands(DefId def, std::vector<DefId>& operands)
{
if (std::find(operands.begin(), operands.end(), def) != operands.end())
return; return;
else if (get<Cell>(def)) else if (get<Cell>(def))
operands.push_back(def); operands->push_back(def);
else if (auto phi = get<Phi>(def)) else if (auto phi = get<Phi>(def))
{ {
for (const Def* operand : phi->operands) for (const Def* operand : phi->operands)
@ -37,6 +33,11 @@ static void collectOperands(DefId def, std::vector<DefId>& operands)
} }
} }
DefId DefArena::freshCell(bool subscripted)
{
return NotNull{allocator.allocate(Def{Cell{subscripted}})};
}
DefId DefArena::phi(DefId a, DefId b) DefId DefArena::phi(DefId a, DefId b)
{ {
return phi({a, b}); return phi({a, b});
@ -46,7 +47,7 @@ DefId DefArena::phi(const std::vector<DefId>& defs)
{ {
std::vector<DefId> operands; std::vector<DefId> operands;
for (DefId operand : defs) for (DefId operand : defs)
collectOperands(operand, operands); collectOperands(operand, &operands);
// There's no need to allocate a Phi node for a singleton set. // There's no need to allocate a Phi node for a singleton set.
if (operands.size() == 1) if (operands.size() == 1)

View file

@ -32,13 +32,12 @@ LUAU_FASTINT(LuauTypeInferRecursionLimit)
LUAU_FASTINT(LuauTarjanChildLimit) LUAU_FASTINT(LuauTarjanChildLimit)
LUAU_FASTFLAG(LuauInferInNoCheckMode) LUAU_FASTFLAG(LuauInferInNoCheckMode)
LUAU_FASTFLAGVARIABLE(LuauKnowsTheDataModel3, false) LUAU_FASTFLAGVARIABLE(LuauKnowsTheDataModel3, false)
LUAU_FASTINTVARIABLE(LuauAutocompleteCheckTimeoutMs, 100) // TODO: Remove with FFlagLuauTypecheckLimitControls
LUAU_FASTFLAGVARIABLE(DebugLuauDeferredConstraintResolution, false) LUAU_FASTFLAGVARIABLE(DebugLuauDeferredConstraintResolution, false)
LUAU_FASTFLAGVARIABLE(DebugLuauLogSolverToJson, false) LUAU_FASTFLAGVARIABLE(DebugLuauLogSolverToJson, false)
LUAU_FASTFLAGVARIABLE(DebugLuauReadWriteProperties, false) LUAU_FASTFLAGVARIABLE(DebugLuauReadWriteProperties, false)
LUAU_FASTFLAGVARIABLE(LuauTypecheckLimitControls, false)
LUAU_FASTFLAGVARIABLE(CorrectEarlyReturnInMarkDirty, false) LUAU_FASTFLAGVARIABLE(CorrectEarlyReturnInMarkDirty, false)
LUAU_FASTFLAGVARIABLE(LuauDefinitionFileSetModuleName, false) LUAU_FASTFLAGVARIABLE(LuauDefinitionFileSetModuleName, false)
LUAU_FASTFLAGVARIABLE(LuauRethrowSingleModuleIce, false)
namespace Luau namespace Luau
{ {
@ -681,8 +680,7 @@ std::vector<ModuleName> Frontend::checkQueuedModules(std::optional<FrontendOptio
sendItemTask(i); sendItemTask(i);
nextItems.clear(); nextItems.clear();
// If we aren't done, but don't have anything processing, we hit a cycle if (FFlag::LuauRethrowSingleModuleIce && processing == 0)
if (remaining != 0 && processing == 0)
{ {
// Typechecking might have been cancelled by user, don't return partial results // Typechecking might have been cancelled by user, don't return partial results
if (cancelled) if (cancelled)
@ -690,9 +688,24 @@ std::vector<ModuleName> Frontend::checkQueuedModules(std::optional<FrontendOptio
// We might have stopped because of a pending exception // We might have stopped because of a pending exception
if (itemWithException) if (itemWithException)
{
recordItemResult(buildQueueItems[*itemWithException]); recordItemResult(buildQueueItems[*itemWithException]);
break; }
// If we aren't done, but don't have anything processing, we hit a cycle
if (remaining != 0 && processing == 0)
{
if (!FFlag::LuauRethrowSingleModuleIce)
{
// Typechecking might have been cancelled by user, don't return partial results
if (cancelled)
return {};
// We might have stopped because of a pending exception
if (itemWithException)
{
recordItemResult(buildQueueItems[*itemWithException]);
break;
}
} }
sendCycleItemTask(); sendCycleItemTask();
@ -902,82 +915,41 @@ void Frontend::checkBuildQueueItem(BuildQueueItem& item)
TypeCheckLimits typeCheckLimits; TypeCheckLimits typeCheckLimits;
if (FFlag::LuauTypecheckLimitControls) if (item.options.moduleTimeLimitSec)
typeCheckLimits.finishTime = TimeTrace::getClock() + *item.options.moduleTimeLimitSec;
else
typeCheckLimits.finishTime = std::nullopt;
// TODO: This is a dirty ad hoc solution for autocomplete timeouts
// We are trying to dynamically adjust our existing limits to lower total typechecking time under the limit
// so that we'll have type information for the whole file at lower quality instead of a full abort in the middle
if (item.options.applyInternalLimitScaling)
{ {
if (item.options.moduleTimeLimitSec) if (FInt::LuauTarjanChildLimit > 0)
typeCheckLimits.finishTime = TimeTrace::getClock() + *item.options.moduleTimeLimitSec; typeCheckLimits.instantiationChildLimit = std::max(1, int(FInt::LuauTarjanChildLimit * sourceNode.autocompleteLimitsMult));
else else
typeCheckLimits.finishTime = std::nullopt; typeCheckLimits.instantiationChildLimit = std::nullopt;
// TODO: This is a dirty ad hoc solution for autocomplete timeouts if (FInt::LuauTypeInferIterationLimit > 0)
// We are trying to dynamically adjust our existing limits to lower total typechecking time under the limit typeCheckLimits.unifierIterationLimit = std::max(1, int(FInt::LuauTypeInferIterationLimit * sourceNode.autocompleteLimitsMult));
// so that we'll have type information for the whole file at lower quality instead of a full abort in the middle else
if (item.options.applyInternalLimitScaling) typeCheckLimits.unifierIterationLimit = std::nullopt;
{
if (FInt::LuauTarjanChildLimit > 0)
typeCheckLimits.instantiationChildLimit = std::max(1, int(FInt::LuauTarjanChildLimit * sourceNode.autocompleteLimitsMult));
else
typeCheckLimits.instantiationChildLimit = std::nullopt;
if (FInt::LuauTypeInferIterationLimit > 0)
typeCheckLimits.unifierIterationLimit = std::max(1, int(FInt::LuauTypeInferIterationLimit * sourceNode.autocompleteLimitsMult));
else
typeCheckLimits.unifierIterationLimit = std::nullopt;
}
typeCheckLimits.cancellationToken = item.options.cancellationToken;
} }
typeCheckLimits.cancellationToken = item.options.cancellationToken;
if (item.options.forAutocomplete) if (item.options.forAutocomplete)
{ {
double autocompleteTimeLimit = FInt::LuauAutocompleteCheckTimeoutMs / 1000.0;
if (!FFlag::LuauTypecheckLimitControls)
{
// The autocomplete typecheck is always in strict mode with DM awareness
// to provide better type information for IDE features
if (autocompleteTimeLimit != 0.0)
typeCheckLimits.finishTime = TimeTrace::getClock() + autocompleteTimeLimit;
else
typeCheckLimits.finishTime = std::nullopt;
// TODO: This is a dirty ad hoc solution for autocomplete timeouts
// We are trying to dynamically adjust our existing limits to lower total typechecking time under the limit
// so that we'll have type information for the whole file at lower quality instead of a full abort in the middle
if (FInt::LuauTarjanChildLimit > 0)
typeCheckLimits.instantiationChildLimit = std::max(1, int(FInt::LuauTarjanChildLimit * sourceNode.autocompleteLimitsMult));
else
typeCheckLimits.instantiationChildLimit = std::nullopt;
if (FInt::LuauTypeInferIterationLimit > 0)
typeCheckLimits.unifierIterationLimit = std::max(1, int(FInt::LuauTypeInferIterationLimit * sourceNode.autocompleteLimitsMult));
else
typeCheckLimits.unifierIterationLimit = std::nullopt;
typeCheckLimits.cancellationToken = item.options.cancellationToken;
}
// The autocomplete typecheck is always in strict mode with DM awareness to provide better type information for IDE features // The autocomplete typecheck is always in strict mode with DM awareness to provide better type information for IDE features
ModulePtr moduleForAutocomplete = check(sourceModule, Mode::Strict, requireCycles, environmentScope, /*forAutocomplete*/ true, ModulePtr moduleForAutocomplete = check(sourceModule, Mode::Strict, requireCycles, environmentScope, /*forAutocomplete*/ true,
/*recordJsonLog*/ false, typeCheckLimits); /*recordJsonLog*/ false, typeCheckLimits);
double duration = getTimestamp() - timestamp; double duration = getTimestamp() - timestamp;
if (FFlag::LuauTypecheckLimitControls) moduleForAutocomplete->checkDurationSec = duration;
{
moduleForAutocomplete->checkDurationSec = duration;
if (item.options.moduleTimeLimitSec && item.options.applyInternalLimitScaling) if (item.options.moduleTimeLimitSec && item.options.applyInternalLimitScaling)
applyInternalLimitScaling(sourceNode, moduleForAutocomplete, *item.options.moduleTimeLimitSec); applyInternalLimitScaling(sourceNode, moduleForAutocomplete, *item.options.moduleTimeLimitSec);
}
else
{
if (moduleForAutocomplete->timeout)
sourceNode.autocompleteLimitsMult = sourceNode.autocompleteLimitsMult / 2.0;
else if (duration < autocompleteTimeLimit / 2.0)
sourceNode.autocompleteLimitsMult = std::min(sourceNode.autocompleteLimitsMult * 2.0, 1.0);
}
item.stats.timeCheck += duration; item.stats.timeCheck += duration;
item.stats.filesStrict += 1; item.stats.filesStrict += 1;
@ -986,29 +958,16 @@ void Frontend::checkBuildQueueItem(BuildQueueItem& item)
return; return;
} }
if (!FFlag::LuauTypecheckLimitControls)
{
typeCheckLimits.cancellationToken = item.options.cancellationToken;
}
ModulePtr module = check(sourceModule, mode, requireCycles, environmentScope, /*forAutocomplete*/ false, item.recordJsonLog, typeCheckLimits); ModulePtr module = check(sourceModule, mode, requireCycles, environmentScope, /*forAutocomplete*/ false, item.recordJsonLog, typeCheckLimits);
if (FFlag::LuauTypecheckLimitControls) double duration = getTimestamp() - timestamp;
{
double duration = getTimestamp() - timestamp;
module->checkDurationSec = duration; module->checkDurationSec = duration;
if (item.options.moduleTimeLimitSec && item.options.applyInternalLimitScaling) if (item.options.moduleTimeLimitSec && item.options.applyInternalLimitScaling)
applyInternalLimitScaling(sourceNode, module, *item.options.moduleTimeLimitSec); applyInternalLimitScaling(sourceNode, module, *item.options.moduleTimeLimitSec);
item.stats.timeCheck += duration;
}
else
{
item.stats.timeCheck += getTimestamp() - timestamp;
}
item.stats.timeCheck += duration;
item.stats.filesStrict += mode == Mode::Strict; item.stats.filesStrict += mode == Mode::Strict;
item.stats.filesNonstrict += mode == Mode::Nonstrict; item.stats.filesNonstrict += mode == Mode::Nonstrict;

View file

@ -12,6 +12,7 @@
#include "Luau/TypeArena.h" #include "Luau/TypeArena.h"
#include "Luau/TypeFamily.h" #include "Luau/TypeFamily.h"
#include "Luau/Def.h" #include "Luau/Def.h"
#include "Luau/TypeFwd.h"
#include <iostream> #include <iostream>
#include <iterator> #include <iterator>
@ -57,8 +58,6 @@ struct StackPusher
struct NonStrictContext struct NonStrictContext
{ {
std::unordered_map<const Def*, TypeId> context;
NonStrictContext() = default; NonStrictContext() = default;
NonStrictContext(const NonStrictContext&) = delete; NonStrictContext(const NonStrictContext&) = delete;
@ -109,7 +108,12 @@ struct NonStrictContext
// Returns true if the removal was successful // Returns true if the removal was successful
bool remove(const DefId& def) bool remove(const DefId& def)
{ {
return context.erase(def.get()) == 1; std::vector<DefId> defs;
collectOperands(def, &defs);
bool result = true;
for (DefId def : defs)
result = result && context.erase(def.get()) == 1;
return result;
} }
std::optional<TypeId> find(const DefId& def) const std::optional<TypeId> find(const DefId& def) const
@ -118,6 +122,14 @@ struct NonStrictContext
return find(d); return find(d);
} }
void addContext(const DefId& def, TypeId ty)
{
std::vector<DefId> defs;
collectOperands(def, &defs);
for (DefId def : defs)
context[def.get()] = ty;
}
private: private:
std::optional<TypeId> find(const Def* d) const std::optional<TypeId> find(const Def* d) const
{ {
@ -126,6 +138,9 @@ private:
return {it->second}; return {it->second};
return {}; return {};
} }
std::unordered_map<const Def*, TypeId> context;
}; };
struct NonStrictTypeChecker struct NonStrictTypeChecker
@ -508,8 +523,25 @@ struct NonStrictTypeChecker
// ... // ...
// (unknown^N-1, ~S_N) -> error // (unknown^N-1, ~S_N) -> error
std::vector<TypeId> argTypes; std::vector<TypeId> argTypes;
for (TypeId ty : fn->argTypes) argTypes.reserve(call->args.size);
argTypes.push_back(ty); // Pad out the arg types array with the types you would expect to see
TypePackIterator curr = begin(fn->argTypes);
TypePackIterator fin = end(fn->argTypes);
while (curr != fin)
{
argTypes.push_back(*curr);
++curr;
}
if (auto argTail = curr.tail())
{
if (const VariadicTypePack* vtp = get<VariadicTypePack>(follow(*argTail)))
{
while (argTypes.size() < call->args.size)
{
argTypes.push_back(vtp->ty);
}
}
}
// For a checked function, these gotta be the same size // For a checked function, these gotta be the same size
LUAU_ASSERT(call->args.size == argTypes.size()); LUAU_ASSERT(call->args.size == argTypes.size());
for (size_t i = 0; i < call->args.size; i++) for (size_t i = 0; i < call->args.size; i++)
@ -523,7 +555,7 @@ struct NonStrictTypeChecker
TypeId expectedArgType = argTypes[i]; TypeId expectedArgType = argTypes[i];
DefId def = dfg->getDef(arg); DefId def = dfg->getDef(arg);
TypeId runTimeErrorTy = getOrCreateNegation(expectedArgType); TypeId runTimeErrorTy = getOrCreateNegation(expectedArgType);
fresh.context[def.get()] = runTimeErrorTy; fresh.addContext(def, runTimeErrorTy);
} }
// Populate the context and now iterate through each of the arguments to the call to find out if we satisfy the types // Populate the context and now iterate through each of the arguments to the call to find out if we satisfy the types
@ -613,15 +645,20 @@ struct NonStrictTypeChecker
std::optional<TypeId> willRunTimeError(AstExpr* fragment, const NonStrictContext& context) std::optional<TypeId> willRunTimeError(AstExpr* fragment, const NonStrictContext& context)
{ {
DefId def = dfg->getDef(fragment); DefId def = dfg->getDef(fragment);
if (std::optional<TypeId> contextTy = context.find(def)) std::vector<DefId> defs;
collectOperands(def, &defs);
for (DefId def : defs)
{ {
if (std::optional<TypeId> contextTy = context.find(def))
{
TypeId actualType = lookupType(fragment); TypeId actualType = lookupType(fragment);
SubtypingResult r = subtyping.isSubtype(actualType, *contextTy); SubtypingResult r = subtyping.isSubtype(actualType, *contextTy);
if (r.normalizationTooComplex) if (r.normalizationTooComplex)
reportError(NormalizationTooComplex{}, fragment->location); reportError(NormalizationTooComplex{}, fragment->location);
if (r.isSubtype) if (r.isSubtype)
return {actualType}; return {actualType};
}
} }
return {}; return {};
@ -630,15 +667,20 @@ struct NonStrictTypeChecker
std::optional<TypeId> willRunTimeErrorFunctionDefinition(AstLocal* fragment, const NonStrictContext& context) std::optional<TypeId> willRunTimeErrorFunctionDefinition(AstLocal* fragment, const NonStrictContext& context)
{ {
DefId def = dfg->getDef(fragment); DefId def = dfg->getDef(fragment);
if (std::optional<TypeId> contextTy = context.find(def)) std::vector<DefId> defs;
collectOperands(def, &defs);
for (DefId def : defs)
{ {
SubtypingResult r1 = subtyping.isSubtype(builtinTypes->unknownType, *contextTy); if (std::optional<TypeId> contextTy = context.find(def))
SubtypingResult r2 = subtyping.isSubtype(*contextTy, builtinTypes->unknownType); {
if (r1.normalizationTooComplex || r2.normalizationTooComplex) SubtypingResult r1 = subtyping.isSubtype(builtinTypes->unknownType, *contextTy);
reportError(NormalizationTooComplex{}, fragment->location); SubtypingResult r2 = subtyping.isSubtype(*contextTy, builtinTypes->unknownType);
bool isUnknown = r1.isSubtype && r2.isSubtype; if (r1.normalizationTooComplex || r2.normalizationTooComplex)
if (isUnknown) reportError(NormalizationTooComplex{}, fragment->location);
return {builtinTypes->unknownType}; bool isUnknown = r1.isSubtype && r2.isSubtype;
if (isUnknown)
return {builtinTypes->unknownType};
}
} }
return {}; return {};
} }

View file

@ -3,8 +3,10 @@
#include "Luau/Common.h" #include "Luau/Common.h"
#include "Luau/Constraint.h" #include "Luau/Constraint.h"
#include "Luau/DenseHash.h"
#include "Luau/Location.h" #include "Luau/Location.h"
#include "Luau/Scope.h" #include "Luau/Scope.h"
#include "Luau/Set.h"
#include "Luau/TxnLog.h" #include "Luau/TxnLog.h"
#include "Luau/TypeInfer.h" #include "Luau/TypeInfer.h"
#include "Luau/TypePack.h" #include "Luau/TypePack.h"
@ -53,8 +55,8 @@ struct FindCyclicTypes final : TypeVisitor
FindCyclicTypes& operator=(const FindCyclicTypes&) = delete; FindCyclicTypes& operator=(const FindCyclicTypes&) = delete;
bool exhaustive = false; bool exhaustive = false;
std::unordered_set<TypeId> visited; Luau::Set<TypeId> visited{{}};
std::unordered_set<TypePackId> visitedPacks; Luau::Set<TypePackId> visitedPacks{{}};
std::set<TypeId> cycles; std::set<TypeId> cycles;
std::set<TypePackId> cycleTPs; std::set<TypePackId> cycleTPs;
@ -70,17 +72,17 @@ struct FindCyclicTypes final : TypeVisitor
bool visit(TypeId ty) override bool visit(TypeId ty) override
{ {
return visited.insert(ty).second; return visited.insert(ty);
} }
bool visit(TypePackId tp) override bool visit(TypePackId tp) override
{ {
return visitedPacks.insert(tp).second; return visitedPacks.insert(tp);
} }
bool visit(TypeId ty, const FreeType& ft) override bool visit(TypeId ty, const FreeType& ft) override
{ {
if (!visited.insert(ty).second) if (!visited.insert(ty))
return false; return false;
if (FFlag::DebugLuauDeferredConstraintResolution) if (FFlag::DebugLuauDeferredConstraintResolution)
@ -102,7 +104,7 @@ struct FindCyclicTypes final : TypeVisitor
bool visit(TypeId ty, const LocalType& lt) override bool visit(TypeId ty, const LocalType& lt) override
{ {
if (!visited.insert(ty).second) if (!visited.insert(ty))
return false; return false;
traverse(lt.domain); traverse(lt.domain);
@ -112,7 +114,7 @@ struct FindCyclicTypes final : TypeVisitor
bool visit(TypeId ty, const TableType& ttv) override bool visit(TypeId ty, const TableType& ttv) override
{ {
if (!visited.insert(ty).second) if (!visited.insert(ty))
return false; return false;
if (ttv.name || ttv.syntheticName) if (ttv.name || ttv.syntheticName)
@ -175,10 +177,11 @@ struct StringifierState
ToStringOptions& opts; ToStringOptions& opts;
ToStringResult& result; ToStringResult& result;
std::unordered_map<TypeId, std::string> cycleNames; DenseHashMap<TypeId, std::string> cycleNames{{}};
std::unordered_map<TypePackId, std::string> cycleTpNames; DenseHashMap<TypePackId, std::string> cycleTpNames{{}};
std::unordered_set<void*> seen; Set<void*> seen{{}};
std::unordered_set<std::string> usedNames; // `$$$` was chosen as the tombstone for `usedNames` since it is not a valid name syntactically and is relatively short for string comparison reasons.
DenseHashSet<std::string> usedNames{"$$$"};
size_t indentation = 0; size_t indentation = 0;
bool exhaustive; bool exhaustive;
@ -197,7 +200,7 @@ struct StringifierState
bool hasSeen(const void* tv) bool hasSeen(const void* tv)
{ {
void* ttv = const_cast<void*>(tv); void* ttv = const_cast<void*>(tv);
if (seen.find(ttv) != seen.end()) if (seen.contains(ttv))
return true; return true;
seen.insert(ttv); seen.insert(ttv);
@ -207,9 +210,9 @@ struct StringifierState
void unsee(const void* tv) void unsee(const void* tv)
{ {
void* ttv = const_cast<void*>(tv); void* ttv = const_cast<void*>(tv);
auto iter = seen.find(ttv);
if (iter != seen.end()) if (seen.contains(ttv))
seen.erase(iter); seen.erase(ttv);
} }
std::string getName(TypeId ty) std::string getName(TypeId ty)
@ -222,7 +225,7 @@ struct StringifierState
for (int count = 0; count < 256; ++count) for (int count = 0; count < 256; ++count)
{ {
std::string candidate = generateName(usedNames.size() + count); std::string candidate = generateName(usedNames.size() + count);
if (!usedNames.count(candidate)) if (!usedNames.contains(candidate))
{ {
usedNames.insert(candidate); usedNames.insert(candidate);
n = candidate; n = candidate;
@ -245,7 +248,7 @@ struct StringifierState
for (int count = 0; count < 256; ++count) for (int count = 0; count < 256; ++count)
{ {
std::string candidate = generateName(previousNameIndex + count); std::string candidate = generateName(previousNameIndex + count);
if (!usedNames.count(candidate)) if (!usedNames.contains(candidate))
{ {
previousNameIndex += count; previousNameIndex += count;
usedNames.insert(candidate); usedNames.insert(candidate);
@ -358,10 +361,9 @@ struct TypeStringifier
return; return;
} }
auto it = state.cycleNames.find(tv); if (auto p = state.cycleNames.find(tv))
if (it != state.cycleNames.end())
{ {
state.emit(it->second); state.emit(*p);
return; return;
} }
@ -886,7 +888,7 @@ struct TypeStringifier
std::string saved = std::move(state.result.name); std::string saved = std::move(state.result.name);
bool needParens = !state.cycleNames.count(el) && (get<IntersectionType>(el) || get<FunctionType>(el)); bool needParens = !state.cycleNames.contains(el) && (get<IntersectionType>(el) || get<FunctionType>(el));
if (needParens) if (needParens)
state.emit("("); state.emit("(");
@ -953,7 +955,7 @@ struct TypeStringifier
std::string saved = std::move(state.result.name); std::string saved = std::move(state.result.name);
bool needParens = !state.cycleNames.count(el) && (get<UnionType>(el) || get<FunctionType>(el)); bool needParens = !state.cycleNames.contains(el) && (get<UnionType>(el) || get<FunctionType>(el));
if (needParens) if (needParens)
state.emit("("); state.emit("(");
@ -1101,10 +1103,9 @@ struct TypePackStringifier
return; return;
} }
auto it = state.cycleTpNames.find(tp); if (auto p = state.cycleTpNames.find(tp))
if (it != state.cycleTpNames.end())
{ {
state.emit(it->second); state.emit(*p);
return; return;
} }
@ -1278,7 +1279,7 @@ void TypeStringifier::stringify(TypePackId tpid, const std::vector<std::optional
} }
static void assignCycleNames(const std::set<TypeId>& cycles, const std::set<TypePackId>& cycleTPs, static void assignCycleNames(const std::set<TypeId>& cycles, const std::set<TypePackId>& cycleTPs,
std::unordered_map<TypeId, std::string>& cycleNames, std::unordered_map<TypePackId, std::string>& cycleTpNames, bool exhaustive) DenseHashMap<TypeId, std::string>& cycleNames, DenseHashMap<TypePackId, std::string>& cycleTpNames, bool exhaustive)
{ {
int nextIndex = 1; int nextIndex = 1;
@ -1372,9 +1373,8 @@ ToStringResult toStringDetailed(TypeId ty, ToStringOptions& opts)
* *
* t1 where t1 = the_whole_root_type * t1 where t1 = the_whole_root_type
*/ */
auto it = state.cycleNames.find(ty); if (auto p = state.cycleNames.find(ty))
if (it != state.cycleNames.end()) state.emit(*p);
state.emit(it->second);
else else
tvs.stringify(ty); tvs.stringify(ty);
@ -1466,9 +1466,8 @@ ToStringResult toStringDetailed(TypePackId tp, ToStringOptions& opts)
* *
* t1 where t1 = the_whole_root_type * t1 where t1 = the_whole_root_type
*/ */
auto it = state.cycleTpNames.find(tp); if (auto p = state.cycleTpNames.find(tp))
if (it != state.cycleTpNames.end()) state.emit(*p);
state.emit(it->second);
else else
tvs.stringify(tp); tvs.stringify(tp);
@ -1766,11 +1765,6 @@ std::string toString(const Constraint& constraint, ToStringOptions& opts)
} }
else if constexpr (std::is_same_v<T, UnpackConstraint>) else if constexpr (std::is_same_v<T, UnpackConstraint>)
return tos(c.resultPack) + " ~ unpack " + tos(c.sourcePack); return tos(c.resultPack) + " ~ unpack " + tos(c.sourcePack);
else if constexpr (std::is_same_v<T, RefineConstraint>)
{
const char* op = c.mode == RefineConstraint::Union ? "union" : "intersect";
return tos(c.resultType) + " ~ refine " + tos(c.type) + " " + op + " " + tos(c.discriminant);
}
else if constexpr (std::is_same_v<T, SetOpConstraint>) else if constexpr (std::is_same_v<T, SetOpConstraint>)
{ {
const char* op = c.mode == SetOpConstraint::Union ? " | " : " & "; const char* op = c.mode == SetOpConstraint::Union ? " | " : " & ";

View file

@ -106,7 +106,12 @@ public:
return allocator->alloc<AstTypeReference>(Location(), std::nullopt, AstName("thread"), std::nullopt, Location()); return allocator->alloc<AstTypeReference>(Location(), std::nullopt, AstName("thread"), std::nullopt, Location());
case PrimitiveType::Buffer: case PrimitiveType::Buffer:
return allocator->alloc<AstTypeReference>(Location(), std::nullopt, AstName("buffer"), std::nullopt, Location()); return allocator->alloc<AstTypeReference>(Location(), std::nullopt, AstName("buffer"), std::nullopt, Location());
case PrimitiveType::Function:
return allocator->alloc<AstTypeReference>(Location(), std::nullopt, AstName("function"), std::nullopt, Location());
case PrimitiveType::Table:
return allocator->alloc<AstTypeReference>(Location(), std::nullopt, AstName("table"), std::nullopt, Location());
default: default:
LUAU_ASSERT(false); // this should be unreachable.
return nullptr; return nullptr;
} }
} }

View file

@ -1603,8 +1603,8 @@ struct TypeChecker2
visit(indexExpr->expr, ValueContext::RValue); visit(indexExpr->expr, ValueContext::RValue);
visit(indexExpr->index, ValueContext::RValue); visit(indexExpr->index, ValueContext::RValue);
TypeId exprType = lookupType(indexExpr->expr); TypeId exprType = follow(lookupType(indexExpr->expr));
TypeId indexType = lookupType(indexExpr->index); TypeId indexType = follow(lookupType(indexExpr->index));
if (auto tt = get<TableType>(exprType)) if (auto tt = get<TableType>(exprType))
{ {

View file

@ -349,7 +349,8 @@ TypeFamilyReductionResult<TypeId> lenFamilyFn(const std::vector<TypeId>& typePar
TypeId operandTy = follow(typeParams.at(0)); TypeId operandTy = follow(typeParams.at(0));
// check to see if the operand type is resolved enough, and wait to reduce if not // check to see if the operand type is resolved enough, and wait to reduce if not
if (isPending(operandTy, ctx->solver)) // the use of `typeFromNormal` later necessitates blocking on local types.
if (isPending(operandTy, ctx->solver) || get<LocalType>(operandTy))
return {std::nullopt, false, {operandTy}, {}}; return {std::nullopt, false, {operandTy}, {}};
const NormalizedType* normTy = ctx->normalizer->normalize(operandTy); const NormalizedType* normTy = ctx->normalizer->normalize(operandTy);
@ -964,6 +965,92 @@ TypeFamilyReductionResult<TypeId> eqFamilyFn(const std::vector<TypeId>& typePara
return {ctx->builtins->booleanType, false, {}, {}}; return {ctx->builtins->booleanType, false, {}, {}};
} }
// Collect types that prevent us from reducing a particular refinement.
struct FindRefinementBlockers : TypeOnceVisitor
{
DenseHashSet<TypeId> found{nullptr};
bool visit(TypeId ty, const BlockedType&) override
{
found.insert(ty);
return false;
}
bool visit(TypeId ty, const PendingExpansionType&) override
{
found.insert(ty);
return false;
}
bool visit(TypeId ty, const LocalType&) override
{
found.insert(ty);
return false;
}
bool visit(TypeId ty, const ClassType&) override
{
return false;
}
};
TypeFamilyReductionResult<TypeId> refineFamilyFn(const std::vector<TypeId>& typeParams, const std::vector<TypePackId>& packParams, NotNull<TypeFamilyContext> ctx)
{
if (typeParams.size() != 2 || !packParams.empty())
{
ctx->ice->ice("refine type family: encountered a type family instance without the required argument structure");
LUAU_ASSERT(false);
}
TypeId targetTy = follow(typeParams.at(0));
TypeId discriminantTy = follow(typeParams.at(1));
// check to see if both operand types are resolved enough, and wait to reduce if not
if (isPending(targetTy, ctx->solver))
return {std::nullopt, false, {targetTy}, {}};
else if (isPending(discriminantTy, ctx->solver))
return {std::nullopt, false, {discriminantTy}, {}};
// we need a more complex check for blocking on the discriminant in particular
FindRefinementBlockers frb;
frb.traverse(discriminantTy);
if (!frb.found.empty())
return {std::nullopt, false, {frb.found.begin(), frb.found.end()}, {}};
/* HACK: Refinements sometimes produce a type T & ~any under the assumption
* that ~any is the same as any. This is so so weird, but refinements needs
* some way to say "I may refine this, but I'm not sure."
*
* It does this by refining on a blocked type and deferring the decision
* until it is unblocked.
*
* Refinements also get negated, so we wind up with types like T & ~*blocked*
*
* We need to treat T & ~any as T in this case.
*/
if (auto nt = get<NegationType>(discriminantTy))
if (get<AnyType>(follow(nt->ty)))
return {targetTy, false, {}, {}};
TypeId intersection = ctx->arena->addType(IntersectionType{{targetTy, discriminantTy}});
const NormalizedType* normIntersection = ctx->normalizer->normalize(intersection);
const NormalizedType* normType = ctx->normalizer->normalize(targetTy);
// if the intersection failed to normalize, we can't reduce, but know nothing about inhabitance.
if (!normIntersection || !normType)
return {std::nullopt, false, {}, {}};
TypeId resultTy = ctx->normalizer->typeFromNormal(*normIntersection);
// include the error type if the target type is error-suppressing and the intersection we computed is not
if (normType->shouldSuppressErrors() && !normIntersection->shouldSuppressErrors())
resultTy = ctx->arena->addType(UnionType{{resultTy, ctx->builtins->errorType}});
return {resultTy, false, {}, {}};
}
BuiltinTypeFamilies::BuiltinTypeFamilies() BuiltinTypeFamilies::BuiltinTypeFamilies()
: notFamily{"not", notFamilyFn} : notFamily{"not", notFamilyFn}
, lenFamily{"len", lenFamilyFn} , lenFamily{"len", lenFamilyFn}
@ -981,6 +1068,7 @@ BuiltinTypeFamilies::BuiltinTypeFamilies()
, ltFamily{"lt", ltFamilyFn} , ltFamily{"lt", ltFamilyFn}
, leFamily{"le", leFamilyFn} , leFamily{"le", leFamilyFn}
, eqFamily{"eq", eqFamilyFn} , eqFamily{"eq", eqFamilyFn}
, refineFamily{"refine", refineFamilyFn}
{ {
} }

View file

@ -325,7 +325,7 @@ int main(int argc, char** argv)
else if (strncmp(argv[i], "--fflags=", 9) == 0) else if (strncmp(argv[i], "--fflags=", 9) == 0)
setLuauFlags(argv[i] + 9); setLuauFlags(argv[i] + 9);
else if (strncmp(argv[i], "-j", 2) == 0) else if (strncmp(argv[i], "-j", 2) == 0)
threadCount = strtol(argv[i] + 2, nullptr, 10); threadCount = int(strtol(argv[i] + 2, nullptr, 10));
} }
#if !defined(LUAU_ENABLE_TIME_TRACE) #if !defined(LUAU_ENABLE_TIME_TRACE)
@ -363,6 +363,7 @@ int main(int argc, char** argv)
if (threadCount <= 0) if (threadCount <= 0)
threadCount = std::min(TaskScheduler::getThreadCount(), 8u); threadCount = std::min(TaskScheduler::getThreadCount(), 8u);
try
{ {
TaskScheduler scheduler(threadCount); TaskScheduler scheduler(threadCount);
@ -370,6 +371,19 @@ int main(int argc, char** argv)
scheduler.push(std::move(f)); scheduler.push(std::move(f));
}); });
} }
catch (const Luau::InternalCompilerError& ice)
{
Luau::Location location = ice.location ? *ice.location : Luau::Location();
std::string moduleName = ice.moduleName ? *ice.moduleName : "<unknown module>";
std::string humanReadableName = frontend.fileResolver->getHumanReadableModuleName(moduleName);
Luau::TypeError error(location, moduleName, Luau::InternalError{ice.message});
report(format, humanReadableName.c_str(), location, "InternalCompilerError",
Luau::toString(error, Luau::TypeErrorToStringOptions{frontend.fileResolver}).c_str());
return 1;
}
int failed = 0; int failed = 0;

View file

@ -124,7 +124,7 @@ static bool analyzeFile(const char* name, const unsigned nestingLimit, std::vect
{ {
Luau::BytecodeBuilder bcb; Luau::BytecodeBuilder bcb;
compileOrThrow(bcb, source.value(), copts()); compileOrThrow(bcb, *source, copts());
const std::string& bytecode = bcb.getBytecode(); const std::string& bytecode = bcb.getBytecode();

View file

@ -22,16 +22,9 @@ RequireResolver::RequireResolver(lua_State* L, std::string path)
if (isAbsolutePath(pathToResolve)) if (isAbsolutePath(pathToResolve))
luaL_argerrorL(L, 1, "cannot require an absolute path"); luaL_argerrorL(L, 1, "cannot require an absolute path");
bool isAlias = !pathToResolve.empty() && pathToResolve[0] == '@';
if (!isAlias && !isExplicitlyRelative(pathToResolve))
luaL_argerrorL(L, 1, "must require an alias prepended with '@' or an explicitly relative path");
std::replace(pathToResolve.begin(), pathToResolve.end(), '\\', '/'); std::replace(pathToResolve.begin(), pathToResolve.end(), '\\', '/');
if (isAlias)
{ substituteAliasIfPresent(pathToResolve);
pathToResolve = pathToResolve.substr(1);
substituteAliasIfPresent(pathToResolve);
}
} }
[[nodiscard]] RequireResolver::ResolvedRequire RequireResolver::resolveRequire(lua_State* L, std::string path) [[nodiscard]] RequireResolver::ResolvedRequire RequireResolver::resolveRequire(lua_State* L, std::string path)
@ -209,16 +202,22 @@ std::string RequireResolver::getRequiringContextRelative()
void RequireResolver::substituteAliasIfPresent(std::string& path) void RequireResolver::substituteAliasIfPresent(std::string& path)
{ {
std::string potentialAlias = path.substr(0, path.find_first_of("\\/")); if (path.size() < 1 || path[0] != '@')
return;
std::string potentialAlias = path.substr(1, path.find_first_of("\\/"));
// Not worth searching when potentialAlias cannot be an alias // Not worth searching when potentialAlias cannot be an alias
if (!Luau::isValidAlias(potentialAlias)) if (!Luau::isValidAlias(potentialAlias))
return; luaL_errorL(L, "@%s is not a valid alias", potentialAlias.c_str());
std::optional<std::string> alias = getAlias(potentialAlias); std::optional<std::string> alias = getAlias(potentialAlias);
if (alias) if (alias)
{ {
path = *alias + path.substr(potentialAlias.size()); path = *alias + path.substr(potentialAlias.size() + 1);
}
else
{
luaL_errorL(L, "@%s is not a valid alias", potentialAlias.c_str());
} }
} }

View file

@ -5,6 +5,7 @@
#include "Luau/UnwindBuilder.h" #include "Luau/UnwindBuilder.h"
#include <string.h> #include <string.h>
#include <stdlib.h>
#if defined(_WIN32) && defined(_M_X64) #if defined(_WIN32) && defined(_M_X64)

View file

@ -42,8 +42,18 @@
LUAU_FASTFLAGVARIABLE(DebugCodegenNoOpt, false) LUAU_FASTFLAGVARIABLE(DebugCodegenNoOpt, false)
LUAU_FASTFLAGVARIABLE(DebugCodegenOptSize, false) LUAU_FASTFLAGVARIABLE(DebugCodegenOptSize, false)
LUAU_FASTFLAGVARIABLE(DebugCodegenSkipNumbering, false) LUAU_FASTFLAGVARIABLE(DebugCodegenSkipNumbering, false)
// Per-module IR instruction count limit
LUAU_FASTINTVARIABLE(CodegenHeuristicsInstructionLimit, 1'048'576) // 1 M LUAU_FASTINTVARIABLE(CodegenHeuristicsInstructionLimit, 1'048'576) // 1 M
LUAU_FASTINTVARIABLE(CodegenHeuristicsBlockLimit, 65'536) // 64 K
// Per-function IR block limit
// Current value is based on some member variables being limited to 16 bits
// Because block check is made before optimization passes and optimization can generate new blocks, limit is lowered 2x
// The limit will probably be adjusted in the future to avoid performance issues with analysis that's more complex than O(n)
LUAU_FASTINTVARIABLE(CodegenHeuristicsBlockLimit, 32'768) // 32 K
// Per-function IR instruction limit
// Current value is based on some member variables being limited to 16 bits
LUAU_FASTINTVARIABLE(CodegenHeuristicsBlockInstructionLimit, 65'536) // 64 K LUAU_FASTINTVARIABLE(CodegenHeuristicsBlockInstructionLimit, 65'536) // 64 K
namespace Luau namespace Luau
@ -104,11 +114,18 @@ static void logPerfFunction(Proto* p, uintptr_t addr, unsigned size)
} }
template<typename AssemblyBuilder> template<typename AssemblyBuilder>
static std::optional<NativeProto> createNativeFunction(AssemblyBuilder& build, ModuleHelpers& helpers, Proto* proto) static std::optional<NativeProto> createNativeFunction(AssemblyBuilder& build, ModuleHelpers& helpers, Proto* proto, uint32_t& totalIrInstCount)
{ {
IrBuilder ir; IrBuilder ir;
ir.buildFunctionIr(proto); ir.buildFunctionIr(proto);
unsigned instCount = unsigned(ir.function.instructions.size());
if (totalIrInstCount + instCount >= unsigned(FInt::CodegenHeuristicsInstructionLimit.value))
return std::nullopt;
totalIrInstCount += instCount;
if (!lowerFunction(ir, build, helpers, proto, {}, /* stats */ nullptr)) if (!lowerFunction(ir, build, helpers, proto, {}, /* stats */ nullptr))
return std::nullopt; return std::nullopt;
@ -291,9 +308,13 @@ CodeGenCompilationResult compile(lua_State* L, int idx, unsigned int flags, Comp
std::vector<NativeProto> results; std::vector<NativeProto> results;
results.reserve(protos.size()); results.reserve(protos.size());
uint32_t totalIrInstCount = 0;
for (Proto* p : protos) for (Proto* p : protos)
if (std::optional<NativeProto> np = createNativeFunction(build, helpers, p)) {
if (std::optional<NativeProto> np = createNativeFunction(build, helpers, p, totalIrInstCount))
results.push_back(*np); results.push_back(*np);
}
// Very large modules might result in overflowing a jump offset; in this case we currently abandon the entire module // Very large modules might result in overflowing a jump offset; in this case we currently abandon the entire module
if (!build.finalize()) if (!build.finalize())

View file

@ -253,11 +253,6 @@ inline bool lowerIr(A64::AssemblyBuilderA64& build, IrBuilder& ir, const std::ve
template<typename AssemblyBuilder> template<typename AssemblyBuilder>
inline bool lowerFunction(IrBuilder& ir, AssemblyBuilder& build, ModuleHelpers& helpers, Proto* proto, AssemblyOptions options, LoweringStats* stats) inline bool lowerFunction(IrBuilder& ir, AssemblyBuilder& build, ModuleHelpers& helpers, Proto* proto, AssemblyOptions options, LoweringStats* stats)
{ {
helpers.bytecodeInstructionCount += unsigned(ir.function.instructions.size());
if (helpers.bytecodeInstructionCount >= unsigned(FInt::CodegenHeuristicsInstructionLimit.value))
return false;
killUnusedBlocks(ir.function); killUnusedBlocks(ir.function);
unsigned preOptBlockCount = 0; unsigned preOptBlockCount = 0;
@ -268,9 +263,7 @@ inline bool lowerFunction(IrBuilder& ir, AssemblyBuilder& build, ModuleHelpers&
preOptBlockCount += (block.kind != IrBlockKind::Dead); preOptBlockCount += (block.kind != IrBlockKind::Dead);
unsigned blockInstructions = block.finish - block.start; unsigned blockInstructions = block.finish - block.start;
maxBlockInstructions = std::max(maxBlockInstructions, blockInstructions); maxBlockInstructions = std::max(maxBlockInstructions, blockInstructions);
}; }
helpers.preOptBlockCount += preOptBlockCount;
// we update stats before checking the heuristic so that even if we bail out // we update stats before checking the heuristic so that even if we bail out
// our stats include information about the limit that was exceeded. // our stats include information about the limit that was exceeded.
@ -280,9 +273,7 @@ inline bool lowerFunction(IrBuilder& ir, AssemblyBuilder& build, ModuleHelpers&
stats->maxBlockInstructions = maxBlockInstructions; stats->maxBlockInstructions = maxBlockInstructions;
} }
// we use helpers.blocksPreOpt instead of stats.blocksPreOpt since if (preOptBlockCount >= unsigned(FInt::CodegenHeuristicsBlockLimit.value))
// stats can be null across some code paths.
if (helpers.preOptBlockCount >= unsigned(FInt::CodegenHeuristicsBlockLimit.value))
return false; return false;
if (maxBlockInstructions >= unsigned(FInt::CodegenHeuristicsBlockInstructionLimit.value)) if (maxBlockInstructions >= unsigned(FInt::CodegenHeuristicsBlockInstructionLimit.value))

View file

@ -31,9 +31,6 @@ struct ModuleHelpers
// A64 // A64
Label continueCall; // x0: closure Label continueCall; // x0: closure
unsigned bytecodeInstructionCount = 0;
unsigned preOptBlockCount = 0;
}; };
} // namespace CodeGen } // namespace CodeGen

View file

@ -285,9 +285,8 @@ public:
using value_type = Item; using value_type = Item;
using reference = Item&; using reference = Item&;
using pointer = Item*; using pointer = Item*;
using iterator = pointer; using difference_type = ptrdiff_t;
using difference_type = size_t; using iterator_category = std::forward_iterator_tag;
using iterator_category = std::input_iterator_tag;
const_iterator() const_iterator()
: set(0) : set(0)
@ -348,6 +347,12 @@ public:
class iterator class iterator
{ {
public: public:
using value_type = MutableItem;
using reference = MutableItem&;
using pointer = MutableItem*;
using difference_type = ptrdiff_t;
using iterator_category = std::forward_iterator_tag;
iterator() iterator()
: set(0) : set(0)
, index(0) , index(0)

View file

@ -14,6 +14,7 @@ inline bool isFlagExperimental(const char* flag)
"LuauInstantiateInSubtyping", // requires some fixes to lua-apps code "LuauInstantiateInSubtyping", // requires some fixes to lua-apps code
"LuauTinyControlFlowAnalysis", // waiting for updates to packages depended by internal builtin plugins "LuauTinyControlFlowAnalysis", // waiting for updates to packages depended by internal builtin plugins
"LuauFixIndexerSubtypingOrdering", // requires some small fixes to lua-apps code since this fixes a false negative "LuauFixIndexerSubtypingOrdering", // requires some small fixes to lua-apps code since this fixes a false negative
"LuauUpdatedRequireByStringSemantics", // requires some small fixes to fully implement some proposed changes
// makes sure we always have at least one entry // makes sure we always have at least one entry
nullptr, nullptr,
}; };

View file

@ -66,7 +66,8 @@ end
-- and 'false' otherwise. -- and 'false' otherwise.
-- --
-- Example usage: -- Example usage:
-- local bench = script and require(script.Parent.bench_support) or require("bench_support") -- local function prequire(name) local success, result = pcall(require, name); return if success then result else nil end
-- local bench = script and require(script.Parent.bench_support) or prequire("bench_support") or require("../bench_support")
-- function testFunc() -- function testFunc()
-- ... -- ...
-- end -- end

View file

@ -1,4 +1,5 @@
local bench = script and require(script.Parent.bench_support) or require("bench_support") local function prequire(name) local success, result = pcall(require, name); return if success then result else nil end
local bench = script and require(script.Parent.bench_support) or prequire("bench_support") or require("../bench_support")
function test() function test()

View file

@ -1,5 +1,6 @@
--!nonstrict --!nonstrict
local bench = script and require(script.Parent.bench_support) or require("bench_support") local function prequire(name) local success, result = pcall(require, name); return if success then result else nil end
local bench = script and require(script.Parent.bench_support) or prequire("bench_support") or require("../bench_support")
local stretchTreeDepth = 18 -- about 16Mb local stretchTreeDepth = 18 -- about 16Mb
local longLivedTreeDepth = 16 -- about 4Mb local longLivedTreeDepth = 16 -- about 4Mb

View file

@ -1,4 +1,5 @@
local bench = script and require(script.Parent.bench_support) or require("bench_support") local function prequire(name) local success, result = pcall(require, name); return if success then result else nil end
local bench = script and require(script.Parent.bench_support) or prequire("bench_support") or require("../bench_support")
function test() function test()
local count = 1 local count = 1

View file

@ -1,4 +1,5 @@
local bench = script and require(script.Parent.bench_support) or require("bench_support") local function prequire(name) local success, result = pcall(require, name); return if success then result else nil end
local bench = script and require(script.Parent.bench_support) or prequire("bench_support") or require("../bench_support")
function test() function test()
local count = 1 local count = 1

View file

@ -1,4 +1,5 @@
local bench = script and require(script.Parent.bench_support) or require("bench_support") local function prequire(name) local success, result = pcall(require, name); return if success then result else nil end
local bench = script and require(script.Parent.bench_support) or prequire("bench_support") or require("../bench_support")
function test() function test()
local count = 1 local count = 1

View file

@ -1,4 +1,5 @@
local bench = script and require(script.Parent.bench_support) or require("bench_support") local function prequire(name) local success, result = pcall(require, name); return if success then result else nil end
local bench = script and require(script.Parent.bench_support) or prequire("bench_support") or require("../bench_support")
function test() function test()
local t = {} local t = {}

View file

@ -21,7 +21,8 @@ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE. SOFTWARE.
]] ]]
local bench = script and require(script.Parent.bench_support) or require("bench_support") local function prequire(name) local success, result = pcall(require, name); return if success then result else nil end
local bench = script and require(script.Parent.bench_support) or prequire("bench_support") or require("../bench_support")
function test() function test()

View file

@ -1,4 +1,5 @@
local bench = script and require(script.Parent.bench_support) or require("bench_support") local function prequire(name) local success, result = pcall(require, name); return if success then result else nil end
local bench = script and require(script.Parent.bench_support) or prequire("bench_support") or require("../bench_support")
function test() function test()

View file

@ -1,4 +1,5 @@
local bench = script and require(script.Parent.bench_support) or require("bench_support") local function prequire(name) local success, result = pcall(require, name); return if success then result else nil end
local bench = script and require(script.Parent.bench_support) or prequire("bench_support") or require("../bench_support")
function test() function test()

View file

@ -1,4 +1,5 @@
local bench = script and require(script.Parent.bench_support) or require("bench_support") local function prequire(name) local success, result = pcall(require, name); return if success then result else nil end
local bench = script and require(script.Parent.bench_support) or prequire("bench_support") or require("../bench_support")
function test() function test()

View file

@ -22,7 +22,8 @@
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
]] ]]
local bench = script and require(script.Parent.bench_support) or require("bench_support") local function prequire(name) local success, result = pcall(require, name); return if success then result else nil end
local bench = script and require(script.Parent.bench_support) or prequire("bench_support") or require("../bench_support")
function test() function test()

View file

@ -1,4 +1,5 @@
local bench = script and require(script.Parent.bench_support) or require("bench_support") local function prequire(name) local success, result = pcall(require, name); return if success then result else nil end
local bench = script and require(script.Parent.bench_support) or prequire("bench_support") or require("../bench_support")
function test() function test()

View file

@ -1,4 +1,5 @@
local bench = script and require(script.Parent.bench_support) or require("bench_support") local function prequire(name) local success, result = pcall(require, name); return if success then result else nil end
local bench = script and require(script.Parent.bench_support) or prequire("bench_support") or require("../bench_support")
function test() function test()

View file

@ -1,4 +1,5 @@
local bench = script and require(script.Parent.bench_support) or require("bench_support") local function prequire(name) local success, result = pcall(require, name); return if success then result else nil end
local bench = script and require(script.Parent.bench_support) or prequire("bench_support") or require("../bench_support")
function test() function test()

View file

@ -1,4 +1,5 @@
local bench = script and require(script.Parent.bench_support) or require("bench_support") local function prequire(name) local success, result = pcall(require, name); return if success then result else nil end
local bench = script and require(script.Parent.bench_support) or prequire("bench_support") or require("../bench_support")
function test() function test()

View file

@ -1,4 +1,5 @@
local bench = script and require(script.Parent.bench_support) or require("bench_support") local function prequire(name) local success, result = pcall(require, name); return if success then result else nil end
local bench = script and require(script.Parent.bench_support) or prequire("bench_support") or require("../bench_support")
function test() function test()

View file

@ -1,4 +1,5 @@
local bench = script and require(script.Parent.bench_support) or require("bench_support") local function prequire(name) local success, result = pcall(require, name); return if success then result else nil end
local bench = script and require(script.Parent.bench_support) or prequire("bench_support") or require("../bench_support")
function test() function test()

View file

@ -1,4 +1,5 @@
local bench = script and require(script.Parent.bench_support) or require("bench_support") local function prequire(name) local success, result = pcall(require, name); return if success then result else nil end
local bench = script and require(script.Parent.bench_support) or prequire("bench_support") or require("../bench_support")
function test() function test()

View file

@ -1,4 +1,5 @@
local bench = script and require(script.Parent.bench_support) or require("bench_support") local function prequire(name) local success, result = pcall(require, name); return if success then result else nil end
local bench = script and require(script.Parent.bench_support) or prequire("bench_support") or require("../bench_support")
function test() function test()

View file

@ -1,4 +1,5 @@
local bench = script and require(script.Parent.bench_support) or require("bench_support") local function prequire(name) local success, result = pcall(require, name); return if success then result else nil end
local bench = script and require(script.Parent.bench_support) or prequire("bench_support") or require("../bench_support")
function test() function test()

View file

@ -1,4 +1,5 @@
local bench = script and require(script.Parent.bench_support) or require("bench_support") local function prequire(name) local success, result = pcall(require, name); return if success then result else nil end
local bench = script and require(script.Parent.bench_support) or prequire("bench_support") or require("../bench_support")
function test() function test()

View file

@ -1,4 +1,5 @@
local bench = script and require(script.Parent.bench_support) or require("bench_support") local function prequire(name) local success, result = pcall(require, name); return if success then result else nil end
local bench = script and require(script.Parent.bench_support) or prequire("bench_support") or require("../bench_support")
function test() function test()

View file

@ -1,4 +1,5 @@
local bench = script and require(script.Parent.bench_support) or require("bench_support") local function prequire(name) local success, result = pcall(require, name); return if success then result else nil end
local bench = script and require(script.Parent.bench_support) or prequire("bench_support") or require("../bench_support")
function test() function test()

View file

@ -1,4 +1,5 @@
local bench = script and require(script.Parent.bench_support) or require("bench_support") local function prequire(name) local success, result = pcall(require, name); return if success then result else nil end
local bench = script and require(script.Parent.bench_support) or prequire("bench_support") or require("../bench_support")
function test() function test()

View file

@ -1,4 +1,5 @@
local bench = script and require(script.Parent.bench_support) or require("bench_support") local function prequire(name) local success, result = pcall(require, name); return if success then result else nil end
local bench = script and require(script.Parent.bench_support) or prequire("bench_support") or require("../bench_support")
function test() function test()

View file

@ -1,4 +1,5 @@
local bench = script and require(script.Parent.bench_support) or require("bench_support") local function prequire(name) local success, result = pcall(require, name); return if success then result else nil end
local bench = script and require(script.Parent.bench_support) or prequire("bench_support") or require("../bench_support")
function test() function test()

View file

@ -1,4 +1,5 @@
local bench = script and require(script.Parent.bench_support) or require("bench_support") local function prequire(name) local success, result = pcall(require, name); return if success then result else nil end
local bench = script and require(script.Parent.bench_support) or prequire("bench_support") or require("../bench_support")
function test() function test()

View file

@ -1,4 +1,5 @@
local bench = script and require(script.Parent.bench_support) or require("bench_support") local function prequire(name) local success, result = pcall(require, name); return if success then result else nil end
local bench = script and require(script.Parent.bench_support) or prequire("bench_support") or require("../bench_support")
function test() function test()

View file

@ -1,4 +1,5 @@
local bench = script and require(script.Parent.bench_support) or require("bench_support") local function prequire(name) local success, result = pcall(require, name); return if success then result else nil end
local bench = script and require(script.Parent.bench_support) or prequire("bench_support") or require("../bench_support")
function test() function test()

View file

@ -1,4 +1,5 @@
local bench = script and require(script.Parent.bench_support) or require("bench_support") local function prequire(name) local success, result = pcall(require, name); return if success then result else nil end
local bench = script and require(script.Parent.bench_support) or prequire("bench_support") or require("../bench_support")
function test() function test()

View file

@ -1,4 +1,5 @@
local bench = script and require(script.Parent.bench_support) or require("bench_support") local function prequire(name) local success, result = pcall(require, name); return if success then result else nil end
local bench = script and require(script.Parent.bench_support) or prequire("bench_support") or require("../bench_support")
function test() function test()

View file

@ -1,4 +1,5 @@
local bench = script and require(script.Parent.bench_support) or require("bench_support") local function prequire(name) local success, result = pcall(require, name); return if success then result else nil end
local bench = script and require(script.Parent.bench_support) or prequire("bench_support") or require("../bench_support")
function test() function test()

View file

@ -1,4 +1,5 @@
local bench = script and require(script.Parent.bench_support) or require("bench_support") local function prequire(name) local success, result = pcall(require, name); return if success then result else nil end
local bench = script and require(script.Parent.bench_support) or prequire("bench_support") or require("../bench_support")
function test() function test()

View file

@ -1,4 +1,5 @@
local bench = script and require(script.Parent.bench_support) or require("bench_support") local function prequire(name) local success, result = pcall(require, name); return if success then result else nil end
local bench = script and require(script.Parent.bench_support) or prequire("bench_support") or require("../bench_support")
function test() function test()

View file

@ -1,4 +1,5 @@
local bench = script and require(script.Parent.bench_support) or require("bench_support") local function prequire(name) local success, result = pcall(require, name); return if success then result else nil end
local bench = script and require(script.Parent.bench_support) or prequire("bench_support") or require("../bench_support")
function test() function test()

View file

@ -1,4 +1,5 @@
local bench = script and require(script.Parent.bench_support) or require("bench_support") local function prequire(name) local success, result = pcall(require, name); return if success then result else nil end
local bench = script and require(script.Parent.bench_support) or prequire("bench_support") or require("../bench_support")
function test() function test()

View file

@ -1,4 +1,5 @@
local bench = script and require(script.Parent.bench_support) or require("bench_support") local function prequire(name) local success, result = pcall(require, name); return if success then result else nil end
local bench = script and require(script.Parent.bench_support) or prequire("bench_support") or require("../bench_support")
function test() function test()

View file

@ -1,4 +1,5 @@
local bench = script and require(script.Parent.bench_support) or require("bench_support") local function prequire(name) local success, result = pcall(require, name); return if success then result else nil end
local bench = script and require(script.Parent.bench_support) or prequire("bench_support") or require("../bench_support")
function test() function test()

View file

@ -1,4 +1,5 @@
local bench = script and require(script.Parent.bench_support) or require("bench_support") local function prequire(name) local success, result = pcall(require, name); return if success then result else nil end
local bench = script and require(script.Parent.bench_support) or prequire("bench_support") or require("../bench_support")
function test() function test()

View file

@ -1,4 +1,5 @@
local bench = script and require(script.Parent.bench_support) or require("bench_support") local function prequire(name) local success, result = pcall(require, name); return if success then result else nil end
local bench = script and require(script.Parent.bench_support) or prequire("bench_support") or require("../bench_support")
function test() function test()

View file

@ -1,4 +1,5 @@
local bench = script and require(script.Parent.bench_support) or require("bench_support") local function prequire(name) local success, result = pcall(require, name); return if success then result else nil end
local bench = script and require(script.Parent.bench_support) or prequire("bench_support") or require("../bench_support")
function test() function test()

View file

@ -1,4 +1,5 @@
local bench = script and require(script.Parent.bench_support) or require("bench_support") local function prequire(name) local success, result = pcall(require, name); return if success then result else nil end
local bench = script and require(script.Parent.bench_support) or prequire("bench_support") or require("../bench_support")
function test() function test()

View file

@ -1,4 +1,5 @@
local bench = script and require(script.Parent.bench_support) or require("bench_support") local function prequire(name) local success, result = pcall(require, name); return if success then result else nil end
local bench = script and require(script.Parent.bench_support) or prequire("bench_support") or require("../bench_support")
function test() function test()

View file

@ -1,4 +1,5 @@
local bench = script and require(script.Parent.bench_support) or require("bench_support") local function prequire(name) local success, result = pcall(require, name); return if success then result else nil end
local bench = script and require(script.Parent.bench_support) or prequire("bench_support") or require("../bench_support")
function test() function test()

View file

@ -1,4 +1,5 @@
local bench = script and require(script.Parent.bench_support) or require("bench_support") local function prequire(name) local success, result = pcall(require, name); return if success then result else nil end
local bench = script and require(script.Parent.bench_support) or prequire("bench_support") or require("../bench_support")
function test() function test()

View file

@ -1,4 +1,5 @@
local bench = script and require(script.Parent.bench_support) or require("bench_support") local function prequire(name) local success, result = pcall(require, name); return if success then result else nil end
local bench = script and require(script.Parent.bench_support) or prequire("bench_support") or require("../bench_support")
bench.runCode(function() bench.runCode(function()
for j=1,1e6 do for j=1,1e6 do

View file

@ -1,4 +1,5 @@
local bench = script and require(script.Parent.bench_support) or require("bench_support") local function prequire(name) local success, result = pcall(require, name); return if success then result else nil end
local bench = script and require(script.Parent.bench_support) or prequire("bench_support") or require("../bench_support")
function test() function test()

View file

@ -1,4 +1,5 @@
local bench = script and require(script.Parent.bench_support) or require("bench_support") local function prequire(name) local success, result = pcall(require, name); return if success then result else nil end
local bench = script and require(script.Parent.bench_support) or prequire("bench_support") or require("../bench_support")
function test() function test()

View file

@ -1,4 +1,5 @@
local bench = script and require(script.Parent.bench_support) or require("bench_support") local function prequire(name) local success, result = pcall(require, name); return if success then result else nil end
local bench = script and require(script.Parent.bench_support) or prequire("bench_support") or require("../bench_support")
function test() function test()

View file

@ -1,4 +1,5 @@
local bench = script and require(script.Parent.bench_support) or require("bench_support") local function prequire(name) local success, result = pcall(require, name); return if success then result else nil end
local bench = script and require(script.Parent.bench_support) or prequire("bench_support") or require("../bench_support")
function test() function test()

View file

@ -1,4 +1,5 @@
local bench = script and require(script.Parent.bench_support) or require("bench_support") local function prequire(name) local success, result = pcall(require, name); return if success then result else nil end
local bench = script and require(script.Parent.bench_support) or prequire("bench_support") or require("../bench_support")
function test() function test()

View file

@ -1,4 +1,5 @@
local bench = script and require(script.Parent.bench_support) or require("bench_support") local function prequire(name) local success, result = pcall(require, name); return if success then result else nil end
local bench = script and require(script.Parent.bench_support) or prequire("bench_support") or require("../bench_support")
function test() function test()

View file

@ -1,4 +1,5 @@
local bench = script and require(script.Parent.bench_support) or require("bench_support") local function prequire(name) local success, result = pcall(require, name); return if success then result else nil end
local bench = script and require(script.Parent.bench_support) or prequire("bench_support") or require("../bench_support")
function test() function test()

View file

@ -1,4 +1,5 @@
local bench = script and require(script.Parent.bench_support) or require("bench_support") local function prequire(name) local success, result = pcall(require, name); return if success then result else nil end
local bench = script and require(script.Parent.bench_support) or prequire("bench_support") or require("../bench_support")
function test() function test()

View file

@ -1,4 +1,5 @@
local bench = script and require(script.Parent.bench_support) or require("bench_support") local function prequire(name) local success, result = pcall(require, name); return if success then result else nil end
local bench = script and require(script.Parent.bench_support) or prequire("bench_support") or require("../bench_support")
function test() function test()

View file

@ -1,4 +1,5 @@
local bench = script and require(script.Parent.bench_support) or require("bench_support") local function prequire(name) local success, result = pcall(require, name); return if success then result else nil end
local bench = script and require(script.Parent.bench_support) or prequire("bench_support") or require("../bench_support")
function test() function test()

View file

@ -1,4 +1,5 @@
local bench = script and require(script.Parent.bench_support) or require("bench_support") local function prequire(name) local success, result = pcall(require, name); return if success then result else nil end
local bench = script and require(script.Parent.bench_support) or prequire("bench_support") or require("../bench_support")
function test() function test()

View file

@ -1,4 +1,5 @@
local bench = script and require(script.Parent.bench_support) or require("bench_support") local function prequire(name) local success, result = pcall(require, name); return if success then result else nil end
local bench = script and require(script.Parent.bench_support) or prequire("bench_support") or require("../bench_support")
function test() function test()

View file

@ -1,4 +1,5 @@
local bench = script and require(script.Parent.bench_support) or require("bench_support") local function prequire(name) local success, result = pcall(require, name); return if success then result else nil end
local bench = script and require(script.Parent.bench_support) or prequire("bench_support") or require("../bench_support")
function test() function test()

View file

@ -1,4 +1,5 @@
local bench = script and require(script.Parent.bench_support) or require("bench_support") local function prequire(name) local success, result = pcall(require, name); return if success then result else nil end
local bench = script and require(script.Parent.bench_support) or prequire("bench_support") or require("../bench_support")
function test() function test()

View file

@ -1,4 +1,5 @@
local bench = script and require(script.Parent.bench_support) or require("bench_support") local function prequire(name) local success, result = pcall(require, name); return if success then result else nil end
local bench = script and require(script.Parent.bench_support) or prequire("bench_support") or require("../bench_support")
function test() function test()

View file

@ -1,4 +1,5 @@
local bench = script and require(script.Parent.bench_support) or require("bench_support") local function prequire(name) local success, result = pcall(require, name); return if success then result else nil end
local bench = script and require(script.Parent.bench_support) or prequire("bench_support") or require("../bench_support")
function test() function test()
local t = table.create(250001, 0) local t = table.create(250001, 0)

View file

@ -1,4 +1,5 @@
local bench = script and require(script.Parent.bench_support) or require("bench_support") local function prequire(name) local success, result = pcall(require, name); return if success then result else nil end
local bench = script and require(script.Parent.bench_support) or prequire("bench_support") or require("../bench_support")
function test() function test()
local t = table.create(5000001, 0) local t = table.create(5000001, 0)

View file

@ -1,4 +1,5 @@
local bench = script and require(script.Parent.bench_support) or require("bench_support") local function prequire(name) local success, result = pcall(require, name); return if success then result else nil end
local bench = script and require(script.Parent.bench_support) or prequire("bench_support") or require("../bench_support")
function test() function test()
local t = table.create(250001, 0) local t = table.create(250001, 0)

View file

@ -1,4 +1,5 @@
local bench = script and require(script.Parent.bench_support) or require("bench_support") local function prequire(name) local success, result = pcall(require, name); return if success then result else nil end
local bench = script and require(script.Parent.bench_support) or prequire("bench_support") or require("../bench_support")
function test() function test()

View file

@ -1,4 +1,5 @@
local bench = script and require(script.Parent.bench_support) or require("bench_support") local function prequire(name) local success, result = pcall(require, name); return if success then result else nil end
local bench = script and require(script.Parent.bench_support) or prequire("bench_support") or require("../bench_support")
local arr_months = {"Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"} local arr_months = {"Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"}

View file

@ -1,4 +1,5 @@
local bench = script and require(script.Parent.bench_support) or require("bench_support") local function prequire(name) local success, result = pcall(require, name); return if success then result else nil end
local bench = script and require(script.Parent.bench_support) or prequire("bench_support") or require("../bench_support")
bench.runCode(function() bench.runCode(function()
for j=1,1e6 do for j=1,1e6 do

View file

@ -1,4 +1,5 @@
local bench = script and require(script.Parent.bench_support) or require("bench_support") local function prequire(name) local success, result = pcall(require, name); return if success then result else nil end
local bench = script and require(script.Parent.bench_support) or prequire("bench_support") or require("../bench_support")
function test() function test()

View file

@ -1,4 +1,5 @@
local bench = script and require(script.Parent.bench_support) or require("bench_support") local function prequire(name) local success, result = pcall(require, name); return if success then result else nil end
local bench = script and require(script.Parent.bench_support) or prequire("bench_support") or require("../bench_support")
function test() function test()

View file

@ -1,4 +1,5 @@
local bench = script and require(script.Parent.bench_support) or require("bench_support") local function prequire(name) local success, result = pcall(require, name); return if success then result else nil end
local bench = script and require(script.Parent.bench_support) or prequire("bench_support") or require("../bench_support")
bench.runCode(function() bench.runCode(function()
local src = string.rep("abcdefghijklmnopqrstuvwxyz", 100) local src = string.rep("abcdefghijklmnopqrstuvwxyz", 100)

View file

@ -1,4 +1,5 @@
local bench = script and require(script.Parent.bench_support) or require("bench_support") local function prequire(name) local success, result = pcall(require, name); return if success then result else nil end
local bench = script and require(script.Parent.bench_support) or prequire("bench_support") or require("../bench_support")
bench.runCode(function() bench.runCode(function()
for outer=1,28,3 do for outer=1,28,3 do

Some files were not shown because too many files have changed in this diff Show more