Sync to upstream/release/623 (#1236)

# What's changed?

### New Type Solver

- Unification of two fresh types no longer binds them together.
- Replaced uses of raw `emplace` with `emplaceType` to catch cyclic
bound types when they are created.
- `SetIndexerConstraint` is blocked until the indexer result type is not
blocked.
- Fix a case where a blocked type got past the constraint solver.
- Searching for free types should no longer traverse into `ClassType`s.
- Fix a corner case that could result in the non-testable type `~{}`.
- Fix incorrect flagging when `any` was a parameter of some checked
function in nonstrict type checker.
- `IterableConstraint` now consider tables without `__iter` to be
iterables.

### Native Code Generation

- Improve register type info lookup by program counter.
- Generate type information for locals and upvalues

---

### Internal Contributors

Co-authored-by: Aaron Weiss <aaronweiss@roblox.com>
Co-authored-by: Alexander McCord <amccord@roblox.com>
Co-authored-by: Andy Friesen <afriesen@roblox.com>
Co-authored-by: James McNellis <jmcnellis@roblox.com>
Co-authored-by: Vighnesh Vijay <vvijay@roblox.com>
Co-authored-by: Vyacheslav Egorov <vegorov@roblox.com>

---------

Co-authored-by: Aaron Weiss <aaronweiss@roblox.com>
Co-authored-by: Andy Friesen <afriesen@roblox.com>
Co-authored-by: Vighnesh <vvijay@roblox.com>
Co-authored-by: Aviral Goel <agoel@roblox.com>
Co-authored-by: David Cope <dcope@roblox.com>
Co-authored-by: Lily Brown <lbrown@roblox.com>
Co-authored-by: Vyacheslav Egorov <vegorov@roblox.com>
This commit is contained in:
Alexander McCord 2024-04-25 15:26:09 -07:00 committed by GitHub
parent 68bd1b2349
commit 259e509038
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
63 changed files with 1832 additions and 554 deletions

View File

@ -165,7 +165,7 @@ private:
*/
ScopePtr childScope(AstNode* node, const ScopePtr& parent);
std::optional<TypeId> lookup(const ScopePtr& scope, DefId def, bool prototype = true);
std::optional<TypeId> lookup(const ScopePtr& scope, Location location, DefId def, bool prototype = true);
/**
* Adds a new constraint with no dependencies to a given scope.
@ -242,7 +242,7 @@ private:
Inference check(const ScopePtr& scope, AstExprConstantBool* bool_, std::optional<TypeId> expectedType, bool forceSingleton);
Inference check(const ScopePtr& scope, AstExprLocal* local);
Inference check(const ScopePtr& scope, AstExprGlobal* global);
Inference checkIndexName(const ScopePtr& scope, const RefinementKey* key, AstExpr* indexee, std::string index);
Inference checkIndexName(const ScopePtr& scope, const RefinementKey* key, AstExpr* indexee, const std::string& index, Location indexLocation);
Inference check(const ScopePtr& scope, AstExprIndexName* indexName);
Inference check(const ScopePtr& scope, AstExprIndexExpr* indexExpr);
Inference check(const ScopePtr& scope, AstExprFunction* func, std::optional<TypeId> expectedType, bool generalize);

View File

@ -12,7 +12,6 @@
LUAU_FASTINT(LuauTableTypeMaximumStringifierLength)
LUAU_FASTINT(LuauTypeMaximumStringifierLength)
LUAU_FASTFLAG(LuauToStringSimpleCompositeTypesSingleLine)
namespace Luau
{
@ -36,7 +35,6 @@ struct ToStringOptions
{
ToStringOptions(bool exhaustive = false)
: exhaustive(exhaustive)
, compositeTypesSingleLineLimit(FFlag::LuauToStringSimpleCompositeTypesSingleLine ? 5 : 0)
{
}

View File

@ -1103,4 +1103,7 @@ LUAU_NOINLINE T* emplaceType(Type* ty, Args&&... args)
return &ty->ty.emplace<T>(std::forward<Args>(args)...);
}
template<>
LUAU_NOINLINE Unifiable::Bound<TypeId>* emplaceType<BoundType>(Type* ty, TypeId& tyArg);
} // namespace Luau

View File

@ -233,4 +233,18 @@ bool isVariadicTail(TypePackId tp, const TxnLog& log, bool includeHiddenVariadic
bool containsNever(TypePackId tp);
/*
* Use this to change the kind of a particular type pack.
*
* LUAU_NOINLINE so that the calling frame doesn't have to pay the stack storage for the new variant.
*/
template<typename T, typename... Args>
LUAU_NOINLINE T* emplaceTypePack(TypePackVar* ty, Args&&... args)
{
return &ty->ty.emplace<T>(std::forward<Args>(args)...);
}
template<>
LUAU_NOINLINE Unifiable::Bound<TypePackId>* emplaceTypePack<BoundTypePack>(TypePackVar* ty, TypePackId& tyArg);
} // namespace Luau

View File

@ -201,6 +201,15 @@ const T* get(std::optional<Ty> ty)
return nullptr;
}
template<typename T, typename Ty>
T* getMutable(std::optional<Ty> ty)
{
if (ty)
return getMutable<T>(*ty);
else
return nullptr;
}
template<typename Ty>
std::optional<Ty> follow(std::optional<Ty> ty)
{

View File

@ -28,6 +28,12 @@ struct FreeTypeCollector : TypeOnceVisitor
result->insert(ty);
return false;
}
bool visit(TypeId ty, const ClassType&) override
{
// ClassTypes never contain free types.
return false;
}
};
DenseHashSet<TypeId> Constraint::getFreeTypes() const

View File

@ -287,7 +287,7 @@ ScopePtr ConstraintGenerator::childScope(AstNode* node, const ScopePtr& parent)
return scope;
}
std::optional<TypeId> ConstraintGenerator::lookup(const ScopePtr& scope, DefId def, bool prototype)
std::optional<TypeId> ConstraintGenerator::lookup(const ScopePtr& scope, Location location, DefId def, bool prototype)
{
if (get<Cell>(def))
return scope->lookup(def);
@ -296,7 +296,7 @@ std::optional<TypeId> ConstraintGenerator::lookup(const ScopePtr& scope, DefId d
if (auto found = scope->lookup(def))
return *found;
else if (!prototype && phi->operands.size() == 1)
return lookup(scope, phi->operands.at(0), prototype);
return lookup(scope, location, phi->operands.at(0), prototype);
else if (!prototype)
return std::nullopt;
@ -307,14 +307,14 @@ std::optional<TypeId> ConstraintGenerator::lookup(const ScopePtr& scope, DefId d
// `scope->lookup(operand)` may return nothing because we only bind a type to that operand
// once we've seen that particular `DefId`. In this case, we need to prototype those types
// and use those at a later time.
std::optional<TypeId> ty = lookup(scope, operand, /*prototype*/ false);
std::optional<TypeId> ty = lookup(scope, location, operand, /*prototype*/ false);
if (!ty)
{
ty = arena->addType(BlockedType{});
rootScope->lvalueTypes[operand] = *ty;
}
res = makeUnion(scope, Location{} /* TODO: can we provide a real location here? */, res, *ty);
res = makeUnion(scope, location, res, *ty);
}
scope->lvalueTypes[def] = res;
@ -512,7 +512,7 @@ void ConstraintGenerator::applyRefinements(const ScopePtr& scope, Location locat
for (auto& [def, partition] : refinements)
{
if (std::optional<TypeId> defTy = lookup(scope, def))
if (std::optional<TypeId> defTy = lookup(scope, location, def))
{
TypeId ty = *defTy;
if (partition.shouldAppendNilType)
@ -918,7 +918,7 @@ ControlFlow ConstraintGenerator::visit(const ScopePtr& scope, AstStatLocalFuncti
bool sigFullyDefined = !hasFreeType(sig.signature);
if (sigFullyDefined)
asMutable(functionType)->ty.emplace<BoundType>(sig.signature);
emplaceType<BoundType>(asMutable(functionType), sig.signature);
DefId def = dfg->getDef(function->name);
scope->lvalueTypes[def] = functionType;
@ -969,15 +969,15 @@ ControlFlow ConstraintGenerator::visit(const ScopePtr& scope, AstStatFunction* f
bool sigFullyDefined = !hasFreeType(sig.signature);
if (sigFullyDefined)
asMutable(generalizedType)->ty.emplace<BoundType>(sig.signature);
emplaceType<BoundType>(asMutable(generalizedType), sig.signature);
DenseHashSet<Constraint*> excludeList{nullptr};
DefId def = dfg->getDef(function->name);
std::optional<TypeId> existingFunctionTy = lookup(scope, def);
std::optional<TypeId> existingFunctionTy = follow(lookup(scope, function->name->location, def));
if (existingFunctionTy && (sigFullyDefined || function->name->is<AstExprLocal>()) && get<BlockedType>(*existingFunctionTy))
asMutable(*existingFunctionTy)->ty.emplace<BoundType>(sig.signature);
if (get<BlockedType>(existingFunctionTy) && sigFullyDefined)
emplaceType<BoundType>(asMutable(*existingFunctionTy), sig.signature);
if (AstExprLocal* localName = function->name->as<AstExprLocal>())
{
@ -1071,6 +1071,12 @@ ControlFlow ConstraintGenerator::visit(const ScopePtr& scope, AstStatFunction* f
blocked->setOwner(addConstraint(scope, std::move(c)));
}
if (BlockedType* bt = getMutable<BlockedType>(follow(existingFunctionTy)); bt && !bt->getOwner())
{
auto uc = addConstraint(scope, function->name->location, Unpack1Constraint{*existingFunctionTy, generalizedType});
bt->setOwner(uc);
}
return ControlFlow::None;
}
@ -1113,13 +1119,13 @@ static void bindFreeType(TypeId a, TypeId b)
LUAU_ASSERT(af || bf);
if (!bf)
asMutable(a)->ty.emplace<BoundType>(b);
emplaceType<BoundType>(asMutable(a), b);
else if (!af)
asMutable(b)->ty.emplace<BoundType>(a);
emplaceType<BoundType>(asMutable(b), a);
else if (subsumes(bf->scope, af->scope))
asMutable(a)->ty.emplace<BoundType>(b);
emplaceType<BoundType>(asMutable(a), b);
else if (subsumes(af->scope, bf->scope))
asMutable(b)->ty.emplace<BoundType>(a);
emplaceType<BoundType>(asMutable(b), a);
}
ControlFlow ConstraintGenerator::visit(const ScopePtr& scope, AstStatAssign* assign)
@ -1153,6 +1159,7 @@ ControlFlow ConstraintGenerator::visit(const ScopePtr& scope, AstStatAssign* ass
for (TypeId assignee : typeStates)
{
auto blocked = getMutable<BlockedType>(assignee);
if (blocked && !blocked->getOwner())
blocked->setOwner(uc);
}
@ -1254,11 +1261,11 @@ ControlFlow ConstraintGenerator::visit(const ScopePtr& scope, AstStatTypeAlias*
LUAU_ASSERT(get<BlockedType>(aliasTy));
if (occursCheck(aliasTy, ty))
{
asMutable(aliasTy)->ty.emplace<BoundType>(builtinTypes->anyType);
emplaceType<BoundType>(asMutable(aliasTy), builtinTypes->anyType);
reportError(alias->nameLocation, OccursCheckFailed{});
}
else
asMutable(aliasTy)->ty.emplace<BoundType>(ty);
emplaceType<BoundType>(asMutable(aliasTy), ty);
std::vector<TypeId> typeParams;
for (auto tyParam : createGenerics(*defnScope, alias->generics, /* useCache */ true, /* addTypes */ false))
@ -1856,12 +1863,12 @@ Inference ConstraintGenerator::check(const ScopePtr& scope, AstExprLocal* local)
// if we have a refinement key, we can look up its type.
if (key)
maybeTy = lookup(scope, key->def);
maybeTy = lookup(scope, local->location, key->def);
// if the current def doesn't have a type, we might be doing a compound assignment
// and therefore might need to look at the rvalue def instead.
if (!maybeTy && rvalueDef)
maybeTy = lookup(scope, *rvalueDef);
maybeTy = lookup(scope, local->location, *rvalueDef);
if (maybeTy)
{
@ -1887,7 +1894,7 @@ Inference ConstraintGenerator::check(const ScopePtr& scope, AstExprGlobal* globa
/* 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.
*/
if (auto ty = lookup(scope, def, /*prototype=*/false))
if (auto ty = lookup(scope, global->location, def, /*prototype=*/false))
{
rootScope->lvalueTypes[def] = *ty;
return Inference{*ty, refinementArena.proposition(key, builtinTypes->truthyType)};
@ -1899,14 +1906,14 @@ Inference ConstraintGenerator::check(const ScopePtr& scope, AstExprGlobal* globa
}
}
Inference ConstraintGenerator::checkIndexName(const ScopePtr& scope, const RefinementKey* key, AstExpr* indexee, std::string index)
Inference ConstraintGenerator::checkIndexName(const ScopePtr& scope, const RefinementKey* key, AstExpr* indexee, const std::string& index, Location indexLocation)
{
TypeId obj = check(scope, indexee).ty;
TypeId result = arena->addType(BlockedType{});
if (key)
{
if (auto ty = lookup(scope, key->def))
if (auto ty = lookup(scope, indexLocation, key->def))
return Inference{*ty, refinementArena.proposition(key, builtinTypes->truthyType)};
scope->rvalueRefinements[key->def] = result;
@ -1925,7 +1932,7 @@ Inference ConstraintGenerator::checkIndexName(const ScopePtr& scope, const Refin
Inference ConstraintGenerator::check(const ScopePtr& scope, AstExprIndexName* indexName)
{
const RefinementKey* key = dfg->getRefinementKey(indexName);
return checkIndexName(scope, key, indexName->expr, indexName->index.value);
return checkIndexName(scope, key, indexName->expr, indexName->index.value, indexName->indexLocation);
}
Inference ConstraintGenerator::check(const ScopePtr& scope, AstExprIndexExpr* indexExpr)
@ -1933,7 +1940,7 @@ Inference ConstraintGenerator::check(const ScopePtr& scope, AstExprIndexExpr* in
if (auto constantString = indexExpr->index->as<AstExprConstantString>())
{
const RefinementKey* key = dfg->getRefinementKey(indexExpr);
return checkIndexName(scope, key, indexExpr->expr, constantString->value.data);
return checkIndexName(scope, key, indexExpr->expr, constantString->value.data, indexExpr->location);
}
TypeId obj = check(scope, indexExpr->expr).ty;
@ -1944,7 +1951,7 @@ Inference ConstraintGenerator::check(const ScopePtr& scope, AstExprIndexExpr* in
const RefinementKey* key = dfg->getRefinementKey(indexExpr);
if (key)
{
if (auto ty = lookup(scope, key->def))
if (auto ty = lookup(scope, indexExpr->location, key->def))
return Inference{*ty, refinementArena.proposition(key, builtinTypes->truthyType)};
scope->rvalueRefinements[key->def] = result;
@ -2326,7 +2333,7 @@ std::tuple<TypeId, TypeId, RefinementId> ConstraintGenerator::checkBinary(
TypeId ty = follow(typeFun->type);
// We're only interested in the root class of any classes.
if (auto ctv = get<ClassType>(ty); !ctv || ctv->parent == builtinTypes->classType)
if (auto ctv = get<ClassType>(ty); ctv && ctv->parent == builtinTypes->classType)
discriminantTy = ty;
}
@ -2427,9 +2434,7 @@ ConstraintGenerator::LValueBounds ConstraintGenerator::checkLValue(const ScopePt
// TODO: Need to clip this, but this requires more code to be reworked first before we can clip this.
std::optional<TypeId> assignedTy = arena->addType(BlockedType{});
auto unpackC = addConstraint(scope, local->location,
UnpackConstraint{arena->addTypePack({*ty}), arena->addTypePack({*assignedTy}),
/*resultIsLValue*/ true});
auto unpackC = addConstraint(scope, local->location, Unpack1Constraint{*ty, *assignedTy, /*resultIsLValue*/ true});
if (auto blocked = get<BlockedType>(*ty))
{
@ -2834,11 +2839,11 @@ ConstraintGenerator::FunctionSignature ConstraintGenerator::checkFunctionSignatu
// generate constraints for return types, we have a guarantee that we
// know the annotated return type already, if one was provided.
LUAU_ASSERT(get<FreeTypePack>(returnType));
asMutable(returnType)->ty.emplace<BoundTypePack>(annotatedRetType);
emplaceTypePack<BoundTypePack>(asMutable(returnType), annotatedRetType);
}
else if (expectedFunction)
{
asMutable(returnType)->ty.emplace<BoundTypePack>(expectedFunction->retTypes);
emplaceTypePack<BoundTypePack>(asMutable(returnType), expectedFunction->retTypes);
}
// TODO: Preserve argument names in the function's type.
@ -3386,7 +3391,7 @@ void ConstraintGenerator::fillInInferredBindings(const ScopePtr& globalScope, As
std::move(tys),
{},
},
globalScope, Location{});
globalScope, location);
scope->bindings[symbol] = Binding{ty, location};
}

View File

@ -591,7 +591,7 @@ bool ConstraintSolver::tryDispatch(const GeneralizationConstraint& c, NotNull<co
else
{
reportError(CodeTooComplex{}, constraint->location);
asMutable(c.generalizedType)->ty.emplace<BoundType>(builtinTypes->errorRecoveryType());
emplaceType<BoundType>(asMutable(c.generalizedType), builtinTypes->errorType);
}
unblock(c.generalizedType, constraint->location);
@ -771,7 +771,7 @@ bool ConstraintSolver::tryDispatch(const TypeAliasExpansionConstraint& c, NotNul
auto bindResult = [this, &c, constraint](TypeId result) {
LUAU_ASSERT(get<PendingExpansionType>(c.target));
asMutable(c.target)->ty.emplace<BoundType>(result);
emplaceType<BoundType>(asMutable(c.target), result);
unblock(c.target, constraint->location);
};
@ -953,7 +953,7 @@ bool ConstraintSolver::tryDispatch(const FunctionCallConstraint& c, NotNull<cons
// if we're calling an error type, the result is an error type, and that's that.
if (get<ErrorType>(fn))
{
asMutable(c.result)->ty.emplace<BoundTypePack>(builtinTypes->errorTypePack);
emplaceTypePack<BoundTypePack>(asMutable(c.result), builtinTypes->errorTypePack);
unblock(c.result, constraint->location);
return true;
@ -961,7 +961,7 @@ bool ConstraintSolver::tryDispatch(const FunctionCallConstraint& c, NotNull<cons
if (get<NeverType>(fn))
{
asMutable(c.result)->ty.emplace<BoundTypePack>(builtinTypes->neverTypePack);
emplaceTypePack<BoundTypePack>(asMutable(c.result), builtinTypes->neverTypePack);
unblock(c.result, constraint->location);
return true;
}
@ -1019,7 +1019,7 @@ bool ConstraintSolver::tryDispatch(const FunctionCallConstraint& c, NotNull<cons
argsPack = arena->addTypePack(TypePack{std::move(argsHead), argsTail});
fn = follow(*callMm);
asMutable(c.result)->ty.emplace<FreeTypePack>(constraint->scope);
emplaceTypePack<FreeTypePack>(asMutable(c.result), constraint->scope);
}
else
{
@ -1036,7 +1036,7 @@ bool ConstraintSolver::tryDispatch(const FunctionCallConstraint& c, NotNull<cons
}
if (!usedMagic)
asMutable(c.result)->ty.emplace<FreeTypePack>(constraint->scope);
emplaceTypePack<FreeTypePack>(asMutable(c.result), constraint->scope);
}
for (std::optional<TypeId> ty : c.discriminantTypes)
@ -1079,7 +1079,7 @@ bool ConstraintSolver::tryDispatch(const FunctionCallConstraint& c, NotNull<cons
result = *subst;
if (c.result != result)
asMutable(c.result)->ty.emplace<BoundTypePack>(result);
emplaceTypePack<BoundTypePack>(asMutable(c.result), result);
}
for (const auto& [expanded, additions] : u2.expandedFreeTypes)
@ -1190,7 +1190,7 @@ bool ConstraintSolver::tryDispatch(const FunctionCheckConstraint& c, NotNull<con
{
if (!lambdaExpr->args.data[j]->annotation && get<FreeType>(follow(lambdaArgTys[j])))
{
asMutable(lambdaArgTys[j])->ty.emplace<BoundType>(expectedLambdaArgTys[j]);
emplaceType<BoundType>(asMutable(lambdaArgTys[j]), expectedLambdaArgTys[j]);
}
}
}
@ -1242,7 +1242,7 @@ bool ConstraintSolver::tryDispatch(const PrimitiveTypeConstraint& c, NotNull<con
else if (expectedType && maybeSingleton(*expectedType))
bindTo = freeType->lowerBound;
asMutable(c.freeType)->ty.emplace<BoundType>(bindTo);
emplaceType<BoundType>(asMutable(c.freeType), bindTo);
return true;
}
@ -1442,7 +1442,7 @@ bool ConstraintSolver::tryDispatchHasIndexer(
if (auto ft = get<FreeType>(subjectType))
{
FreeType freeResult{ft->scope, builtinTypes->neverType, builtinTypes->unknownType};
asMutable(resultType)->ty.emplace<FreeType>(freeResult);
emplaceType<FreeType>(asMutable(resultType), freeResult);
TypeId upperBound = arena->addType(TableType{/* props */ {}, TableIndexer{indexType, resultType}, TypeLevel{}, TableState::Unsealed});
@ -1464,7 +1464,7 @@ bool ConstraintSolver::tryDispatchHasIndexer(
// FIXME this is greedy.
FreeType freeResult{tt->scope, builtinTypes->neverType, builtinTypes->unknownType};
asMutable(resultType)->ty.emplace<FreeType>(freeResult);
emplaceType<FreeType>(asMutable(resultType), freeResult);
tt->indexer = TableIndexer{indexType, resultType};
return true;
@ -1521,7 +1521,7 @@ bool ConstraintSolver::tryDispatchHasIndexer(
else if (1 == results.size())
bindBlockedType(resultType, *results.begin(), subjectType, constraint);
else
asMutable(resultType)->ty.emplace<IntersectionType>(std::vector(results.begin(), results.end()));
emplaceType<IntersectionType>(asMutable(resultType), std::vector(results.begin(), results.end()));
return true;
}
@ -1549,11 +1549,11 @@ bool ConstraintSolver::tryDispatchHasIndexer(
}
if (0 == results.size())
asMutable(resultType)->ty.emplace<BoundType>(builtinTypes->errorType);
emplaceType<BoundType>(asMutable(resultType), builtinTypes->errorType);
else if (1 == results.size())
asMutable(resultType)->ty.emplace<BoundType>(*results.begin());
emplaceType<BoundType>(asMutable(resultType), *results.begin());
else
asMutable(resultType)->ty.emplace<UnionType>(std::vector(results.begin(), results.end()));
emplaceType<UnionType>(asMutable(resultType), std::vector(results.begin(), results.end()));
return true;
}
@ -1619,6 +1619,11 @@ std::pair<bool, std::optional<TypeId>> ConstraintSolver::tryDispatchSetIndexer(
{
if (tt->indexer)
{
if (isBlocked(tt->indexer->indexType))
return {block(tt->indexer->indexType, constraint), std::nullopt};
else if (isBlocked(tt->indexer->indexResultType))
return {block(tt->indexer->indexResultType, constraint), std::nullopt};
unify(constraint, indexType, tt->indexer->indexType);
return {true, tt->indexer->indexResultType};
}
@ -1713,7 +1718,7 @@ bool ConstraintSolver::tryDispatchUnpack1(NotNull<const Constraint> constraint,
--lt->blockCount;
if (0 == lt->blockCount)
asMutable(ty)->ty.emplace<BoundType>(lt->domain);
emplaceType<BoundType>(asMutable(ty), lt->domain);
};
if (auto ut = get<UnionType>(resultTy))
@ -1729,7 +1734,7 @@ bool ConstraintSolver::tryDispatchUnpack1(NotNull<const Constraint> constraint,
// constitute any meaningful constraint, so we replace it
// with a free type.
TypeId f = freshType(arena, builtinTypes, constraint->scope);
asMutable(resultTy)->ty.emplace<BoundType>(f);
emplaceType<BoundType>(asMutable(resultTy), f);
}
else
bindBlockedType(resultTy, srcTy, srcTy, constraint);
@ -1756,7 +1761,7 @@ bool ConstraintSolver::tryDispatch(const UnpackConstraint& c, NotNull<const Cons
{
LUAU_ASSERT(canMutate(resultPack, constraint));
LUAU_ASSERT(resultPack != sourcePack);
asMutable(resultPack)->ty.emplace<BoundTypePack>(sourcePack);
emplaceTypePack<BoundTypePack>(asMutable(resultPack), sourcePack);
unblock(resultPack, constraint->location);
return true;
}
@ -1795,11 +1800,11 @@ bool ConstraintSolver::tryDispatch(const UnpackConstraint& c, NotNull<const Cons
--lt->blockCount;
if (0 == lt->blockCount)
asMutable(resultTy)->ty.emplace<BoundType>(lt->domain);
emplaceType<BoundType>(asMutable(resultTy), lt->domain);
}
else if (get<BlockedType>(resultTy) || get<PendingExpansionType>(resultTy))
{
asMutable(resultTy)->ty.emplace<BoundType>(builtinTypes->nilType);
emplaceType<BoundType>(asMutable(resultTy), builtinTypes->nilType);
unblock(resultTy, constraint->location);
}
@ -1974,7 +1979,7 @@ bool ConstraintSolver::tryDispatchIterableTable(TypeId iteratorTy, const Iterabl
LUAU_ASSERT(0 <= lt->blockCount);
if (0 == lt->blockCount)
asMutable(ty)->ty.emplace<BoundType>(lt->domain);
emplaceType<BoundType>(asMutable(ty), lt->domain);
}
}
}
@ -2032,11 +2037,8 @@ bool ConstraintSolver::tryDispatchIterableTable(TypeId iteratorTy, const Iterabl
}
else if (auto iteratorMetatable = get<MetatableType>(iteratorTy))
{
TypeId metaTy = follow(iteratorMetatable->metatable);
if (get<FreeType>(metaTy))
return block_(metaTy);
LUAU_ASSERT(false);
// If the metatable does not contain a `__iter` metamethod, then we iterate over the table part of the metatable.
return tryDispatchIterableTable(iteratorMetatable->table, c, constraint, force);
}
else if (auto primitiveTy = get<PrimitiveType>(iteratorTy); primitiveTy && primitiveTy->type == PrimitiveType::Type::Table)
unpack(builtinTypes->unknownType);
@ -2391,10 +2393,10 @@ void ConstraintSolver::bindBlockedType(TypeId blockedTy, TypeId resultTy, TypeId
LUAU_ASSERT(freeScope);
asMutable(blockedTy)->ty.emplace<BoundType>(arena->freshType(freeScope));
emplaceType<BoundType>(asMutable(blockedTy), arena->freshType(freeScope));
}
else
asMutable(blockedTy)->ty.emplace<BoundType>(resultTy);
emplaceType<BoundType>(asMutable(blockedTy), resultTy);
}
bool ConstraintSolver::block_(BlockedConstraintId target, NotNull<const Constraint> constraint)

View File

@ -23,7 +23,7 @@ bool Instantiation2::isDirty(TypePackId tp)
TypeId Instantiation2::clean(TypeId ty)
{
TypeId substTy = genericSubstitutions[ty];
TypeId substTy = follow(genericSubstitutions[ty]);
const FreeType* ft = get<FreeType>(substTy);
// violation of the substitution invariant if this is not a free type.

View File

@ -191,9 +191,9 @@ struct NonStrictTypeChecker
TypeId result = arena->addType(FreeType{ftp->scope});
TypePackId freeTail = arena->addTypePack(FreeTypePack{ftp->scope});
TypePack& resultPack = asMutable(pack)->ty.emplace<TypePack>();
resultPack.head.assign(1, result);
resultPack.tail = freeTail;
TypePack* resultPack = emplaceTypePack<TypePack>(asMutable(pack));
resultPack->head.assign(1, result);
resultPack->tail = freeTail;
return result;
}
@ -560,8 +560,19 @@ struct NonStrictTypeChecker
// We will compare arg and ~number
AstExpr* arg = call->args.data[i];
TypeId expectedArgType = argTypes[i];
std::shared_ptr<const NormalizedType> norm = normalizer.normalize(expectedArgType);
DefId def = dfg->getDef(arg);
TypeId runTimeErrorTy = getOrCreateNegation(expectedArgType);
TypeId runTimeErrorTy;
// If we're dealing with any, negating any will cause all subtype tests to fail, since ~any is any
// However, when someone calls this function, they're going to want to be able to pass it anything,
// for that reason, we manually inject never into the context so that the runtime test will always pass.
if (!norm)
reportError(NormalizationTooComplex{}, arg->location);
if (norm && get<AnyType>(norm->tops))
runTimeErrorTy = builtinTypes->neverType;
else
runTimeErrorTy = getOrCreateNegation(expectedArgType);
fresh.addContext(def, runTimeErrorTy);
}

View File

@ -148,7 +148,7 @@ TypeId matchLiteralType(NotNull<DenseHashMap<const AstExpr*, TypeId>> astTypes,
Relation upperBoundRelation = relate(ft->upperBound, expectedType);
if (upperBoundRelation == Relation::Subset || upperBoundRelation == Relation::Coincident)
{
asMutable(exprType)->ty.emplace<BoundType>(expectedType);
emplaceType<BoundType>(asMutable(exprType), expectedType);
return exprType;
}
@ -158,7 +158,7 @@ TypeId matchLiteralType(NotNull<DenseHashMap<const AstExpr*, TypeId>> astTypes,
Relation lowerBoundRelation = relate(ft->lowerBound, expectedType);
if (lowerBoundRelation == Relation::Subset || lowerBoundRelation == Relation::Coincident)
{
asMutable(exprType)->ty.emplace<BoundType>(expectedType);
emplaceType<BoundType>(asMutable(exprType), expectedType);
return exprType;
}
}
@ -173,7 +173,7 @@ TypeId matchLiteralType(NotNull<DenseHashMap<const AstExpr*, TypeId>> astTypes,
Relation upperBoundRelation = relate(ft->upperBound, expectedType);
if (upperBoundRelation == Relation::Subset || upperBoundRelation == Relation::Coincident)
{
asMutable(exprType)->ty.emplace<BoundType>(expectedType);
emplaceType<BoundType>(asMutable(exprType), expectedType);
return exprType;
}
@ -183,7 +183,7 @@ TypeId matchLiteralType(NotNull<DenseHashMap<const AstExpr*, TypeId>> astTypes,
Relation lowerBoundRelation = relate(ft->lowerBound, expectedType);
if (lowerBoundRelation == Relation::Subset || lowerBoundRelation == Relation::Coincident)
{
asMutable(exprType)->ty.emplace<BoundType>(expectedType);
emplaceType<BoundType>(asMutable(exprType), expectedType);
return exprType;
}
}
@ -193,7 +193,7 @@ TypeId matchLiteralType(NotNull<DenseHashMap<const AstExpr*, TypeId>> astTypes,
{
if (auto ft = get<FreeType>(exprType); ft && fastIsSubtype(ft->upperBound, expectedType))
{
asMutable(exprType)->ty.emplace<BoundType>(expectedType);
emplaceType<BoundType>(asMutable(exprType), expectedType);
return exprType;
}

View File

@ -20,8 +20,7 @@
#include <string>
LUAU_FASTFLAG(DebugLuauDeferredConstraintResolution)
LUAU_FASTFLAGVARIABLE(LuauToStringSimpleCompositeTypesSingleLine, false)
LUAU_FASTFLAGVARIABLE(LuauStringifyCyclesRootedAtPacks, false)
LUAU_FASTFLAGVARIABLE(LuauToStringiteTypesSingleLine, false)
/*
* Enables increasing levels of verbosity for Luau type names when stringifying.
@ -1492,21 +1491,10 @@ ToStringResult toStringDetailed(TypePackId tp, ToStringOptions& opts)
else
tvs.stringify(tp);
if (FFlag::LuauStringifyCyclesRootedAtPacks)
if (!cycles.empty() || !cycleTPs.empty())
{
if (!cycles.empty() || !cycleTPs.empty())
{
result.cycle = true;
state.emit(" where ");
}
}
else
{
if (!cycles.empty())
{
result.cycle = true;
state.emit(" where ");
}
result.cycle = true;
state.emit(" where ");
}
state.exhaustive = true;
@ -1533,30 +1521,27 @@ ToStringResult toStringDetailed(TypePackId tp, ToStringOptions& opts)
semi = true;
}
if (FFlag::LuauStringifyCyclesRootedAtPacks)
std::vector<std::pair<TypePackId, std::string>> sortedCycleTpNames{state.cycleTpNames.begin(), state.cycleTpNames.end()};
std::sort(sortedCycleTpNames.begin(), sortedCycleTpNames.end(), [](const auto& a, const auto& b) {
return a.second < b.second;
});
TypePackStringifier tps{tvs.state};
for (const auto& [cycleTp, name] : sortedCycleTpNames)
{
std::vector<std::pair<TypePackId, std::string>> sortedCycleTpNames{state.cycleTpNames.begin(), state.cycleTpNames.end()};
std::sort(sortedCycleTpNames.begin(), sortedCycleTpNames.end(), [](const auto& a, const auto& b) {
return a.second < b.second;
});
if (semi)
state.emit(" ; ");
TypePackStringifier tps{tvs.state};
state.emit(name);
state.emit(" = ");
Luau::visit(
[&tps, cycleTp = cycleTp](auto t) {
return tps(cycleTp, t);
},
cycleTp->ty);
for (const auto& [cycleTp, name] : sortedCycleTpNames)
{
if (semi)
state.emit(" ; ");
state.emit(name);
state.emit(" = ");
Luau::visit(
[&tps, cycleTp = cycleTp](auto t) {
return tps(cycleTp, t);
},
cycleTp->ty);
semi = true;
}
semi = true;
}
if (opts.maxTypeLength > 0 && result.name.length() > opts.maxTypeLength)

View File

@ -1324,4 +1324,11 @@ bool GenericTypePackDefinition::operator==(const GenericTypePackDefinition& rhs)
return tp == rhs.tp && defaultValue == rhs.defaultValue;
}
template<>
LUAU_NOINLINE Unifiable::Bound<TypeId>* emplaceType<BoundType>(Type* ty, TypeId& tyArg)
{
LUAU_ASSERT(ty != follow(tyArg));
return &ty->ty.emplace<BoundType>(tyArg);
}
} // namespace Luau

View File

@ -1016,9 +1016,9 @@ struct TypeChecker2
reportError(UnificationTooComplex{}, forInStatement->values.data[0]->location);
}
}
else if (iteratorNorm && iteratorNorm->hasTopTable())
else if (iteratorNorm && iteratorNorm->hasTables())
{
// nothing
// Ok. All tables can be iterated.
}
else if (!iteratorNorm || !iteratorNorm->shouldSuppressErrors())
{
@ -2045,25 +2045,28 @@ struct TypeChecker2
case AstExprBinary::Op::CompareLt:
{
if (normLeft && normLeft->shouldSuppressErrors())
return builtinTypes->numberType;
return builtinTypes->booleanType;
// if we're comparing against an uninhabited type, it's unobservable that the comparison did not run
if (normLeft && normalizer.isInhabited(normLeft.get()) == NormalizationResult::False)
return builtinTypes->booleanType;
if (normLeft && normLeft->isExactlyNumber())
{
testIsSubtype(rightType, builtinTypes->numberType, expr->right->location);
return builtinTypes->numberType;
return builtinTypes->booleanType;
}
else if (normLeft && normLeft->isSubtypeOfString())
if (normLeft && normLeft->isSubtypeOfString())
{
testIsSubtype(rightType, builtinTypes->stringType, expr->right->location);
return builtinTypes->stringType;
}
else
{
reportError(GenericError{format("Types '%s' and '%s' cannot be compared with relational operator %s", toString(leftType).c_str(),
toString(rightType).c_str(), toString(expr->op).c_str())},
expr->location);
return builtinTypes->errorRecoveryType();
return builtinTypes->booleanType;
}
reportError(GenericError{format("Types '%s' and '%s' cannot be compared with relational operator %s", toString(leftType).c_str(),
toString(rightType).c_str(), toString(expr->op).c_str())},
expr->location);
return builtinTypes->errorRecoveryType();
}
case AstExprBinary::Op::And:
@ -2144,9 +2147,9 @@ struct TypeChecker2
TypeId result = module->internalTypes.addType(FreeType{ftp->scope});
TypePackId freeTail = module->internalTypes.addTypePack(FreeTypePack{ftp->scope});
TypePack& resultPack = asMutable(pack)->ty.emplace<TypePack>();
resultPack.head.assign(1, result);
resultPack.tail = freeTail;
TypePack* resultPack = emplaceTypePack<TypePack>(asMutable(pack));
resultPack->head.assign(1, result);
resultPack->tail = freeTail;
return result;
}

View File

@ -1069,15 +1069,15 @@ static TypeFamilyReductionResult<TypeId> comparisonFamilyFn(TypeId instance, Not
// lt <number, t> implies t is number
// lt <t, number> implies t is number
if (lhsFree && isNumber(rhsTy))
asMutable(lhsTy)->ty.emplace<BoundType>(ctx->builtins->numberType);
emplaceType<BoundType>(asMutable(lhsTy), ctx->builtins->numberType);
else if (rhsFree && isNumber(lhsTy))
asMutable(rhsTy)->ty.emplace<BoundType>(ctx->builtins->numberType);
else if (lhsFree && get<NeverType>(rhsTy) == nullptr)
emplaceType<BoundType>(asMutable(rhsTy), ctx->builtins->numberType);
else if (lhsFree && ctx->normalizer->isInhabited(rhsTy) != NormalizationResult::False)
{
auto c1 = ctx->pushConstraint(EqualityConstraint{lhsTy, rhsTy});
const_cast<Constraint*>(ctx->constraint)->dependencies.emplace_back(c1);
}
else if (rhsFree && get<NeverType>(lhsTy) == nullptr)
else if (rhsFree && ctx->normalizer->isInhabited(lhsTy) != NormalizationResult::False)
{
auto c1 = ctx->pushConstraint(EqualityConstraint{rhsTy, lhsTy});
const_cast<Constraint*>(ctx->constraint)->dependencies.emplace_back(c1);

View File

@ -465,4 +465,11 @@ bool containsNever(TypePackId tp)
return false;
}
template<>
LUAU_NOINLINE Unifiable::Bound<TypePackId>* emplaceTypePack<BoundTypePack>(TypePackVar* ty, TypePackId& tyArg)
{
LUAU_ASSERT(ty != follow(tyArg));
return &ty->ty.emplace<BoundTypePack>(tyArg);
}
} // namespace Luau

View File

@ -145,33 +145,16 @@ bool Unifier2::unify(TypeId subTy, TypeId superTy)
FreeType* subFree = getMutable<FreeType>(subTy);
FreeType* superFree = getMutable<FreeType>(superTy);
if (subFree && superFree)
{
DenseHashSet<TypeId> seen{nullptr};
if (OccursCheckResult::Fail == occursCheck(seen, subTy, superTy))
{
asMutable(subTy)->ty.emplace<BoundType>(builtinTypes->errorRecoveryType());
return false;
}
else if (OccursCheckResult::Fail == occursCheck(seen, superTy, subTy))
{
asMutable(subTy)->ty.emplace<BoundType>(builtinTypes->errorRecoveryType());
return false;
}
superFree->lowerBound = mkUnion(subFree->lowerBound, superFree->lowerBound);
superFree->upperBound = mkIntersection(subFree->upperBound, superFree->upperBound);
asMutable(subTy)->ty.emplace<BoundType>(superTy);
}
else if (subFree)
{
return unifyFreeWithType(subTy, superTy);
}
else if (superFree)
if (superFree)
{
superFree->lowerBound = mkUnion(superFree->lowerBound, subTy);
}
if (subFree)
{
return unifyFreeWithType(subTy, superTy);
}
if (subFree || superFree)
return true;
@ -516,11 +499,11 @@ bool Unifier2::unify(TypePackId subTp, TypePackId superTp)
DenseHashSet<TypePackId> seen{nullptr};
if (OccursCheckResult::Fail == occursCheck(seen, subTp, superTp))
{
asMutable(subTp)->ty.emplace<BoundTypePack>(builtinTypes->errorRecoveryTypePack());
emplaceTypePack<BoundTypePack>(asMutable(subTp), builtinTypes->errorTypePack);
return false;
}
asMutable(subTp)->ty.emplace<BoundTypePack>(superTp);
emplaceTypePack<BoundTypePack>(asMutable(subTp), superTp);
return true;
}
@ -529,11 +512,11 @@ bool Unifier2::unify(TypePackId subTp, TypePackId superTp)
DenseHashSet<TypePackId> seen{nullptr};
if (OccursCheckResult::Fail == occursCheck(seen, superTp, subTp))
{
asMutable(superTp)->ty.emplace<BoundTypePack>(builtinTypes->errorRecoveryTypePack());
emplaceTypePack<BoundTypePack>(asMutable(superTp), builtinTypes->errorTypePack);
return false;
}
asMutable(superTp)->ty.emplace<BoundTypePack>(subTp);
emplaceTypePack<BoundTypePack>(asMutable(superTp), subTp);
return true;
}
@ -567,13 +550,13 @@ bool Unifier2::unify(TypePackId subTp, TypePackId superTp)
{
TypePackId followedSubTail = follow(*subTail);
if (get<FreeTypePack>(followedSubTail))
asMutable(followedSubTail)->ty.emplace<BoundTypePack>(builtinTypes->emptyTypePack);
emplaceTypePack<BoundTypePack>(asMutable(followedSubTail), builtinTypes->emptyTypePack);
}
else if (superTail)
{
TypePackId followedSuperTail = follow(*superTail);
if (get<FreeTypePack>(followedSuperTail))
asMutable(followedSuperTail)->ty.emplace<BoundTypePack>(builtinTypes->emptyTypePack);
emplaceTypePack<BoundTypePack>(asMutable(followedSuperTail), builtinTypes->emptyTypePack);
}
return true;
@ -759,28 +742,90 @@ struct MutatingGeneralizer : TypeOnceVisitor
return;
seen.insert(haystack);
std::vector<TypeId>* parts = nullptr;
if (UnionType* ut = getMutable<UnionType>(haystack))
parts = &ut->options;
else if (IntersectionType* it = getMutable<IntersectionType>(needle))
parts = &it->parts;
else
return;
LUAU_ASSERT(parts);
for (TypeId& option : *parts)
{
// FIXME: I bet this function has reentrancy problems
option = follow(option);
if (option == needle)
option = replacement;
for (auto iter = ut->options.begin(); iter != ut->options.end();)
{
// FIXME: I bet this function has reentrancy problems
TypeId option = follow(*iter);
// TODO seen set
else if (get<UnionType>(option))
replace(seen, option, needle, haystack);
else if (get<IntersectionType>(option))
replace(seen, option, needle, haystack);
if (option == needle && get<NeverType>(replacement))
{
iter = ut->options.erase(iter);
continue;
}
if (option == needle)
{
*iter = replacement;
iter++;
continue;
}
// advance the iterator, nothing after this can use it.
iter++;
if (seen.find(option))
continue;
seen.insert(option);
if (get<UnionType>(option))
replace(seen, option, needle, haystack);
else if (get<IntersectionType>(option))
replace(seen, option, needle, haystack);
}
if (ut->options.size() == 1)
{
TypeId onlyType = ut->options[0];
LUAU_ASSERT(onlyType != haystack);
emplaceType<BoundType>(asMutable(haystack), onlyType);
}
return;
}
if (IntersectionType* it = getMutable<IntersectionType>(needle))
{
for (auto iter = it->parts.begin(); iter != it->parts.end();)
{
// FIXME: I bet this function has reentrancy problems
TypeId part = follow(*iter);
if (part == needle && get<UnknownType>(replacement))
{
iter = it->parts.erase(iter);
continue;
}
if (part == needle)
{
*iter = replacement;
iter++;
continue;
}
// advance the iterator, nothing after this can use it.
iter++;
if (seen.find(part))
continue;
seen.insert(part);
if (get<UnionType>(part))
replace(seen, part, needle, haystack);
else if (get<IntersectionType>(part))
replace(seen, part, needle, haystack);
}
if (it->parts.size() == 1)
{
TypeId onlyType = it->parts[0];
LUAU_ASSERT(onlyType != needle);
emplaceType<BoundType>(asMutable(needle), onlyType);
}
return;
}
}
@ -807,7 +852,7 @@ struct MutatingGeneralizer : TypeOnceVisitor
traverse(ft->upperBound);
// It is possible for the above traverse() calls to cause ty to be
// transmuted. We must reaquire ft if this happens.
// transmuted. We must reacquire ft if this happens.
ty = follow(ty);
ft = get<FreeType>(ty);
if (!ft)
@ -852,7 +897,17 @@ struct MutatingGeneralizer : TypeOnceVisitor
DenseHashSet<TypeId> replaceSeen{nullptr};
replace(replaceSeen, lb, ty, builtinTypes->unknownType);
}
emplaceType<BoundType>(asMutable(ty), lb);
if (lb != ty)
emplaceType<BoundType>(asMutable(ty), lb);
else if (!isWithinFunction || (positiveCount + negativeCount == 1))
emplaceType<BoundType>(asMutable(ty), builtinTypes->unknownType);
else
{
// if the lower bound is the type in question, we don't actually have a lower bound.
emplaceType<GenericType>(asMutable(ty), scope);
generics.push_back(ty);
}
}
else
{
@ -864,7 +919,17 @@ struct MutatingGeneralizer : TypeOnceVisitor
DenseHashSet<TypeId> replaceSeen{nullptr};
replace(replaceSeen, ub, ty, builtinTypes->neverType);
}
emplaceType<BoundType>(asMutable(ty), ub);
if (ub != ty)
emplaceType<BoundType>(asMutable(ty), ub);
else if (!isWithinFunction || (positiveCount + negativeCount == 1))
emplaceType<BoundType>(asMutable(ty), builtinTypes->unknownType);
else
{
// if the upper bound is the type in question, we don't actually have an upper bound.
emplaceType<GenericType>(asMutable(ty), scope);
generics.push_back(ty);
}
}
return false;
@ -909,10 +974,10 @@ struct MutatingGeneralizer : TypeOnceVisitor
const size_t negativeCount = getCount(negativeTypes, tp);
if (1 == positiveCount + negativeCount)
asMutable(tp)->ty.emplace<BoundTypePack>(builtinTypes->unknownTypePack);
emplaceTypePack<BoundTypePack>(asMutable(tp), builtinTypes->unknownTypePack);
else
{
asMutable(tp)->ty.emplace<GenericTypePack>(scope);
emplaceTypePack<GenericTypePack>(asMutable(tp), scope);
genericPacks.push_back(tp);
}

View File

@ -25,6 +25,7 @@ static Luau::CompileOptions copts()
Luau::CompileOptions result = {};
result.optimizationLevel = globalOptions.optimizationLevel;
result.debugLevel = globalOptions.debugLevel;
result.typeInfoLevel = 1;
return result;
}

View File

@ -45,6 +45,7 @@ struct GlobalOptions
{
int optimizationLevel = 1;
int debugLevel = 1;
int typeInfoLevel = 0;
const char* vectorLib = nullptr;
const char* vectorCtor = nullptr;
@ -56,6 +57,7 @@ static Luau::CompileOptions copts()
Luau::CompileOptions result = {};
result.optimizationLevel = globalOptions.optimizationLevel;
result.debugLevel = globalOptions.debugLevel;
result.typeInfoLevel = globalOptions.typeInfoLevel;
result.vectorLib = globalOptions.vectorLib;
result.vectorCtor = globalOptions.vectorCtor;
@ -324,7 +326,7 @@ static bool compileFile(const char* name, CompileFormat format, Luau::CodeGen::A
if (format == CompileFormat::Text)
{
bcb.setDumpFlags(Luau::BytecodeBuilder::Dump_Code | Luau::BytecodeBuilder::Dump_Source | Luau::BytecodeBuilder::Dump_Locals |
Luau::BytecodeBuilder::Dump_Remarks);
Luau::BytecodeBuilder::Dump_Remarks | Luau::BytecodeBuilder::Dump_Types);
bcb.setDumpSource(*source);
}
else if (format == CompileFormat::Remarks)
@ -487,6 +489,16 @@ int main(int argc, char** argv)
}
globalOptions.debugLevel = level;
}
else if (strncmp(argv[i], "-t", 2) == 0)
{
int level = atoi(argv[i] + 2);
if (level < 0 || level > 1)
{
fprintf(stderr, "Error: Type info level must be between 0 and 1 inclusive.\n");
return 1;
}
globalOptions.typeInfoLevel = level;
}
else if (strncmp(argv[i], "--target=", 9) == 0)
{
const char* value = argv[i] + 9;

View File

@ -88,6 +88,7 @@ static Luau::CompileOptions copts()
Luau::CompileOptions result = {};
result.optimizationLevel = globalOptions.optimizationLevel;
result.debugLevel = globalOptions.debugLevel;
result.typeInfoLevel = 1;
result.coverageLevel = coverageActive() ? 2 : 0;
return result;

View File

@ -14,6 +14,7 @@ namespace CodeGen
struct IrFunction;
void loadBytecodeTypeInfo(IrFunction& function);
void buildBytecodeBlocks(IrFunction& function, const std::vector<uint8_t>& jumpTargets);
void analyzeBytecodeTypes(IrFunction& function);

View File

@ -114,7 +114,6 @@ void setNativeExecutionEnabled(lua_State* L, bool enabled);
using ModuleId = std::array<uint8_t, 16>;
// Builds target function and all inner functions
CodeGenCompilationResult compile_DEPRECATED(lua_State* L, int idx, unsigned int flags = 0, CompilationStats* stats = nullptr);
CompilationResult compile(lua_State* L, int idx, unsigned int flags = 0, CompilationStats* stats = nullptr);
CompilationResult compile(const ModuleId& moduleId, lua_State* L, int idx, unsigned int flags = 0, CompilationStats* stats = nullptr);
@ -168,6 +167,7 @@ struct AssemblyOptions
bool includeAssembly = false;
bool includeIr = false;
bool includeOutlinedCode = false;
bool includeIrTypes = false;
IncludeIrPrefix includeIrPrefix = IncludeIrPrefix::Yes;
IncludeUseInfo includeUseInfo = IncludeUseInfo::Yes;

View File

@ -977,6 +977,25 @@ struct BytecodeTypes
uint8_t c = LBC_TYPE_ANY;
};
struct BytecodeRegTypeInfo
{
uint8_t type = LBC_TYPE_ANY;
uint8_t reg = 0; // Register slot where variable is stored
int startpc = 0; // First point where variable is alive (could be before variable has been assigned a value)
int endpc = 0; // First point where variable is dead
};
struct BytecodeTypeInfo
{
std::vector<uint8_t> argumentTypes;
std::vector<BytecodeRegTypeInfo> regTypes;
std::vector<uint8_t> upvalueTypes;
// Offsets into regTypes for each individual register
// One extra element at the end contains the vector size for easier arr[Rn], arr[Rn + 1] range access
std::vector<uint32_t> regTypeOffsets;
};
struct IrFunction
{
std::vector<IrBlock> blocks;
@ -994,6 +1013,8 @@ struct IrFunction
std::vector<IrOp> valueRestoreOps;
std::vector<uint32_t> validRestoreOpBlocks;
BytecodeTypeInfo bcTypeInfo;
Proto* proto = nullptr;
bool variadic = false;

View File

@ -30,6 +30,9 @@ void toString(IrToStringContext& ctx, const IrBlock& block, uint32_t index); //
void toString(IrToStringContext& ctx, IrOp op);
void toString(std::string& result, IrConst constant);
const char* getBytecodeTypeName(uint8_t type);
void toString(std::string& result, const BytecodeTypes& bcTypes);
void toStringDetailed(

View File

@ -7,7 +7,12 @@
#include "lobject.h"
#include <algorithm>
LUAU_FASTFLAG(LuauCodegenDirectUserdataFlow)
LUAU_FASTFLAG(LuauLoadTypeInfo) // Because new VM typeinfo load changes the format used by Codegen, same flag is used
LUAU_FASTFLAGVARIABLE(LuauCodegenTypeInfo, false) // New analysis is flagged separately
LUAU_FASTFLAG(LuauTypeInfoLookupImprovement)
namespace Luau
{
@ -16,9 +21,227 @@ namespace CodeGen
static bool hasTypedParameters(Proto* proto)
{
CODEGEN_ASSERT(!FFlag::LuauLoadTypeInfo);
return proto->typeinfo && proto->numparams != 0;
}
template<typename T>
static T read(uint8_t* data, size_t& offset)
{
CODEGEN_ASSERT(FFlag::LuauLoadTypeInfo);
T result;
memcpy(&result, data + offset, sizeof(T));
offset += sizeof(T);
return result;
}
static uint32_t readVarInt(uint8_t* data, size_t& offset)
{
CODEGEN_ASSERT(FFlag::LuauLoadTypeInfo);
uint32_t result = 0;
uint32_t shift = 0;
uint8_t byte;
do
{
byte = read<uint8_t>(data, offset);
result |= (byte & 127) << shift;
shift += 7;
} while (byte & 128);
return result;
}
void loadBytecodeTypeInfo(IrFunction& function)
{
CODEGEN_ASSERT(FFlag::LuauLoadTypeInfo);
Proto* proto = function.proto;
if (FFlag::LuauTypeInfoLookupImprovement)
{
if (!proto)
return;
}
else
{
if (!proto || !proto->typeinfo)
return;
}
BytecodeTypeInfo& typeInfo = function.bcTypeInfo;
// If there is no typeinfo, we generate default values for arguments and upvalues
if (FFlag::LuauTypeInfoLookupImprovement && !proto->typeinfo)
{
typeInfo.argumentTypes.resize(proto->numparams, LBC_TYPE_ANY);
typeInfo.upvalueTypes.resize(proto->nups, LBC_TYPE_ANY);
return;
}
uint8_t* data = proto->typeinfo;
size_t offset = 0;
uint32_t typeSize = readVarInt(data, offset);
uint32_t upvalCount = readVarInt(data, offset);
uint32_t localCount = readVarInt(data, offset);
CODEGEN_ASSERT(upvalCount == unsigned(proto->nups));
if (typeSize != 0)
{
uint8_t* types = (uint8_t*)data + offset;
CODEGEN_ASSERT(typeSize == uint32_t(2 + proto->numparams));
CODEGEN_ASSERT(types[0] == LBC_TYPE_FUNCTION);
CODEGEN_ASSERT(types[1] == proto->numparams);
typeInfo.argumentTypes.resize(proto->numparams);
// Skip two bytes of function type introduction
memcpy(typeInfo.argumentTypes.data(), types + 2, proto->numparams);
offset += typeSize;
}
if (upvalCount != 0)
{
typeInfo.upvalueTypes.resize(upvalCount);
uint8_t* types = (uint8_t*)data + offset;
memcpy(typeInfo.upvalueTypes.data(), types, upvalCount);
offset += upvalCount;
}
if (localCount != 0)
{
typeInfo.regTypes.resize(localCount);
for (uint32_t i = 0; i < localCount; i++)
{
BytecodeRegTypeInfo& info = typeInfo.regTypes[i];
info.type = read<uint8_t>(data, offset);
info.reg = read<uint8_t>(data, offset);
info.startpc = readVarInt(data, offset);
info.endpc = info.startpc + readVarInt(data, offset);
}
}
CODEGEN_ASSERT(offset == size_t(proto->sizetypeinfo));
}
static void prepareRegTypeInfoLookups(BytecodeTypeInfo& typeInfo)
{
CODEGEN_ASSERT(FFlag::LuauTypeInfoLookupImprovement);
// Sort by register first, then by end PC
std::sort(typeInfo.regTypes.begin(), typeInfo.regTypes.end(), [](const BytecodeRegTypeInfo& a, const BytecodeRegTypeInfo& b) {
if (a.reg != b.reg)
return a.reg < b.reg;
return a.endpc < b.endpc;
});
// Prepare data for all registers as 'regTypes' might be missing temporaries
typeInfo.regTypeOffsets.resize(256 + 1);
for (size_t i = 0; i < typeInfo.regTypes.size(); i++)
{
const BytecodeRegTypeInfo& el = typeInfo.regTypes[i];
// Data is sorted by register order, so when we visit register Rn last time
// If means that register Rn+1 starts one after the slot where Rn ends
typeInfo.regTypeOffsets[el.reg + 1] = uint32_t(i + 1);
}
// Fill in holes with the offset of the previous register
for (size_t i = 1; i < typeInfo.regTypeOffsets.size(); i++)
{
uint32_t& el = typeInfo.regTypeOffsets[i];
if (el == 0)
el = typeInfo.regTypeOffsets[i - 1];
}
}
static BytecodeRegTypeInfo* findRegType(BytecodeTypeInfo& info, uint8_t reg, int pc)
{
CODEGEN_ASSERT(FFlag::LuauCodegenTypeInfo);
if (FFlag::LuauTypeInfoLookupImprovement)
{
auto b = info.regTypes.begin() + info.regTypeOffsets[reg];
auto e = info.regTypes.begin() + info.regTypeOffsets[reg + 1];
// Doen't have info
if (b == e)
return nullptr;
// No info after the last live range
if (pc >= (e - 1)->endpc)
return nullptr;
for (auto it = b; it != e; ++it)
{
CODEGEN_ASSERT(it->reg == reg);
if (pc >= it->startpc && pc < it->endpc)
return &*it;
}
return nullptr;
}
else
{
for (BytecodeRegTypeInfo& el : info.regTypes)
{
if (reg == el.reg && pc >= el.startpc && pc < el.endpc)
return &el;
}
return nullptr;
}
}
static void refineRegType(BytecodeTypeInfo& info, uint8_t reg, int pc, uint8_t ty)
{
CODEGEN_ASSERT(FFlag::LuauCodegenTypeInfo);
if (ty != LBC_TYPE_ANY)
{
if (BytecodeRegTypeInfo* regType = findRegType(info, reg, pc))
{
// Right now, we only refine register types that were unknown
if (regType->type == LBC_TYPE_ANY)
regType->type = ty;
}
else if (FFlag::LuauTypeInfoLookupImprovement && reg < info.argumentTypes.size())
{
if (info.argumentTypes[reg] == LBC_TYPE_ANY)
info.argumentTypes[reg] = ty;
}
}
}
static void refineUpvalueType(BytecodeTypeInfo& info, int up, uint8_t ty)
{
CODEGEN_ASSERT(FFlag::LuauCodegenTypeInfo);
if (ty != LBC_TYPE_ANY)
{
if (size_t(up) < info.upvalueTypes.size())
{
if (info.upvalueTypes[up] == LBC_TYPE_ANY)
info.upvalueTypes[up] = ty;
}
}
}
static uint8_t getBytecodeConstantTag(Proto* proto, unsigned ki)
{
TValue protok = proto->k[ki];
@ -389,6 +612,11 @@ void analyzeBytecodeTypes(IrFunction& function)
Proto* proto = function.proto;
CODEGEN_ASSERT(proto);
BytecodeTypeInfo& bcTypeInfo = function.bcTypeInfo;
if (FFlag::LuauTypeInfoLookupImprovement)
prepareRegTypeInfoLookups(bcTypeInfo);
// Setup our current knowledge of type tags based on arguments
uint8_t regTags[256];
memset(regTags, LBC_TYPE_ANY, 256);
@ -403,16 +631,29 @@ void analyzeBytecodeTypes(IrFunction& function)
// At the block start, reset or knowledge to the starting state
// In the future we might be able to propagate some info between the blocks as well
if (hasTypedParameters(proto))
if (FFlag::LuauLoadTypeInfo)
{
for (int i = 0; i < proto->numparams; ++i)
for (size_t i = 0; i < bcTypeInfo.argumentTypes.size(); i++)
{
uint8_t et = proto->typeinfo[2 + i];
uint8_t et = bcTypeInfo.argumentTypes[i];
// TODO: if argument is optional, this might force a VM exit unnecessarily
regTags[i] = et & ~LBC_TYPE_OPTIONAL_BIT;
}
}
else
{
if (hasTypedParameters(proto))
{
for (int i = 0; i < proto->numparams; ++i)
{
uint8_t et = proto->typeinfo[2 + i];
// TODO: if argument is optional, this might force a VM exit unnecessarily
regTags[i] = et & ~LBC_TYPE_OPTIONAL_BIT;
}
}
}
for (int i = proto->numparams; i < proto->maxstacksize; ++i)
regTags[i] = LBC_TYPE_ANY;
@ -422,6 +663,18 @@ void analyzeBytecodeTypes(IrFunction& function)
const Instruction* pc = &proto->code[i];
LuauOpcode op = LuauOpcode(LUAU_INSN_OP(*pc));
if (FFlag::LuauCodegenTypeInfo)
{
// Assign known register types from local type information
// TODO: this is an expensive walk for each instruction
// TODO: it's best to lookup when register is actually used in the instruction
for (BytecodeRegTypeInfo& el : bcTypeInfo.regTypes)
{
if (el.type != LBC_TYPE_ANY && i >= el.startpc && i < el.endpc)
regTags[el.reg] = el.type;
}
}
BytecodeTypes& bcType = function.bcTypes[i];
switch (op)
@ -440,6 +693,9 @@ void analyzeBytecodeTypes(IrFunction& function)
int ra = LUAU_INSN_A(*pc);
regTags[ra] = LBC_TYPE_BOOLEAN;
bcType.result = regTags[ra];
if (FFlag::LuauCodegenTypeInfo)
refineRegType(bcTypeInfo, ra, i, bcType.result);
break;
}
case LOP_LOADN:
@ -447,6 +703,9 @@ void analyzeBytecodeTypes(IrFunction& function)
int ra = LUAU_INSN_A(*pc);
regTags[ra] = LBC_TYPE_NUMBER;
bcType.result = regTags[ra];
if (FFlag::LuauCodegenTypeInfo)
refineRegType(bcTypeInfo, ra, i, bcType.result);
break;
}
case LOP_LOADK:
@ -456,6 +715,9 @@ void analyzeBytecodeTypes(IrFunction& function)
bcType.a = getBytecodeConstantTag(proto, kb);
regTags[ra] = bcType.a;
bcType.result = regTags[ra];
if (FFlag::LuauCodegenTypeInfo)
refineRegType(bcTypeInfo, ra, i, bcType.result);
break;
}
case LOP_LOADKX:
@ -465,6 +727,9 @@ void analyzeBytecodeTypes(IrFunction& function)
bcType.a = getBytecodeConstantTag(proto, kb);
regTags[ra] = bcType.a;
bcType.result = regTags[ra];
if (FFlag::LuauCodegenTypeInfo)
refineRegType(bcTypeInfo, ra, i, bcType.result);
break;
}
case LOP_MOVE:
@ -474,6 +739,9 @@ void analyzeBytecodeTypes(IrFunction& function)
bcType.a = regTags[rb];
regTags[ra] = regTags[rb];
bcType.result = regTags[ra];
if (FFlag::LuauCodegenTypeInfo)
refineRegType(bcTypeInfo, ra, i, bcType.result);
break;
}
case LOP_GETTABLE:
@ -767,6 +1035,9 @@ void analyzeBytecodeTypes(IrFunction& function)
regTags[ra + 2] = bcType.b;
regTags[ra + 3] = bcType.c;
regTags[ra] = bcType.result;
if (FFlag::LuauCodegenTypeInfo)
refineRegType(bcTypeInfo, ra, i, bcType.result);
break;
}
case LOP_FASTCALL1:
@ -783,6 +1054,9 @@ void analyzeBytecodeTypes(IrFunction& function)
regTags[LUAU_INSN_B(*pc)] = bcType.a;
regTags[ra] = bcType.result;
if (FFlag::LuauCodegenTypeInfo)
refineRegType(bcTypeInfo, ra, i, bcType.result);
break;
}
case LOP_FASTCALL2:
@ -799,6 +1073,9 @@ void analyzeBytecodeTypes(IrFunction& function)
regTags[LUAU_INSN_B(*pc)] = bcType.a;
regTags[int(pc[1])] = bcType.b;
regTags[ra] = bcType.result;
if (FFlag::LuauCodegenTypeInfo)
refineRegType(bcTypeInfo, ra, i, bcType.result);
break;
}
case LOP_FORNPREP:
@ -808,6 +1085,13 @@ void analyzeBytecodeTypes(IrFunction& function)
regTags[ra] = LBC_TYPE_NUMBER;
regTags[ra + 1] = LBC_TYPE_NUMBER;
regTags[ra + 2] = LBC_TYPE_NUMBER;
if (FFlag::LuauCodegenTypeInfo)
{
refineRegType(bcTypeInfo, ra, i, regTags[ra]);
refineRegType(bcTypeInfo, ra + 1, i, regTags[ra + 1]);
refineRegType(bcTypeInfo, ra + 2, i, regTags[ra + 2]);
}
break;
}
case LOP_FORNLOOP:
@ -856,6 +1140,39 @@ void analyzeBytecodeTypes(IrFunction& function)
}
break;
}
case LOP_GETUPVAL:
{
if (FFlag::LuauCodegenTypeInfo)
{
int ra = LUAU_INSN_A(*pc);
int up = LUAU_INSN_B(*pc);
bcType.a = LBC_TYPE_ANY;
if (size_t(up) < bcTypeInfo.upvalueTypes.size())
{
uint8_t et = bcTypeInfo.upvalueTypes[up];
// TODO: if argument is optional, this might force a VM exit unnecessarily
bcType.a = et & ~LBC_TYPE_OPTIONAL_BIT;
}
regTags[ra] = bcType.a;
bcType.result = regTags[ra];
}
break;
}
case LOP_SETUPVAL:
{
if (FFlag::LuauCodegenTypeInfo)
{
int ra = LUAU_INSN_A(*pc);
int up = LUAU_INSN_B(*pc);
refineUpvalueType(bcTypeInfo, up, regTags[ra]);
}
break;
}
case LOP_GETGLOBAL:
case LOP_SETGLOBAL:
case LOP_CALL:
@ -876,8 +1193,6 @@ void analyzeBytecodeTypes(IrFunction& function)
case LOP_JUMPXEQKN:
case LOP_JUMPXEQKS:
case LOP_SETLIST:
case LOP_GETUPVAL:
case LOP_SETUPVAL:
case LOP_CLOSEUPVALS:
case LOP_FORGLOOP:
case LOP_FORGPREP_NEXT:

View File

@ -44,7 +44,6 @@
LUAU_FASTFLAGVARIABLE(DebugCodegenNoOpt, false)
LUAU_FASTFLAGVARIABLE(DebugCodegenOptSize, false)
LUAU_FASTFLAGVARIABLE(DebugCodegenSkipNumbering, false)
LUAU_FASTFLAGVARIABLE(LuauCodegenDetailedCompilationResult, false)
// Per-module IR instruction count limit
LUAU_FASTINTVARIABLE(CodegenHeuristicsInstructionLimit, 1'048'576) // 1 M
@ -445,148 +444,8 @@ void setNativeExecutionEnabled(lua_State* L, bool enabled)
}
}
CodeGenCompilationResult compile_DEPRECATED(lua_State* L, int idx, unsigned int flags, CompilationStats* stats)
{
CODEGEN_ASSERT(!FFlag::LuauCodegenContext);
CODEGEN_ASSERT(!FFlag::LuauCodegenDetailedCompilationResult);
CODEGEN_ASSERT(lua_isLfunction(L, idx));
const TValue* func = luaA_toobject(L, idx);
Proto* root = clvalue(func)->l.p;
if ((flags & CodeGen_OnlyNativeModules) != 0 && (root->flags & LPF_NATIVE_MODULE) == 0)
return CodeGenCompilationResult::NotNativeModule;
// If initialization has failed, do not compile any functions
NativeState* data = getNativeState(L);
if (!data)
return CodeGenCompilationResult::CodeGenNotInitialized;
std::vector<Proto*> protos;
gatherFunctions(protos, root, flags);
// Skip protos that have been compiled during previous invocations of CodeGen::compile
protos.erase(std::remove_if(protos.begin(), protos.end(),
[](Proto* p) {
return p == nullptr || p->execdata != nullptr;
}),
protos.end());
if (protos.empty())
return CodeGenCompilationResult::NothingToCompile;
if (stats != nullptr)
stats->functionsTotal = uint32_t(protos.size());
#if defined(__aarch64__)
static unsigned int cpuFeatures = getCpuFeaturesA64();
A64::AssemblyBuilderA64 build(/* logText= */ false, cpuFeatures);
#else
X64::AssemblyBuilderX64 build(/* logText= */ false);
#endif
ModuleHelpers helpers;
#if defined(__aarch64__)
A64::assembleHelpers(build, helpers);
#else
X64::assembleHelpers(build, helpers);
#endif
std::vector<OldNativeProto> results;
results.reserve(protos.size());
uint32_t totalIrInstCount = 0;
CodeGenCompilationResult codeGenCompilationResult = CodeGenCompilationResult::Success;
for (Proto* p : protos)
{
// If compiling a proto fails, we want to propagate the failure via codeGenCompilationResult
// If multiple compilations fail, we only use the failure from the first unsuccessful compilation.
CodeGenCompilationResult temp = CodeGenCompilationResult::Success;
if (std::optional<OldNativeProto> np = createNativeFunction(build, helpers, p, totalIrInstCount, temp))
results.push_back(*np);
// second compilation failure onwards, this condition fails and codeGenCompilationResult is not assigned.
else if (codeGenCompilationResult == CodeGenCompilationResult::Success)
codeGenCompilationResult = temp;
}
// Very large modules might result in overflowing a jump offset; in this case we currently abandon the entire module
if (!build.finalize())
{
for (OldNativeProto result : results)
destroyExecData(result.execdata);
return CodeGenCompilationResult::CodeGenAssemblerFinalizationFailure;
}
// If no functions were assembled, we don't need to allocate/copy executable pages for helpers
if (results.empty())
{
LUAU_ASSERT(codeGenCompilationResult != CodeGenCompilationResult::Success);
return codeGenCompilationResult;
}
uint8_t* nativeData = nullptr;
size_t sizeNativeData = 0;
uint8_t* codeStart = nullptr;
if (!data->codeAllocator.allocate(build.data.data(), int(build.data.size()), reinterpret_cast<const uint8_t*>(build.code.data()),
int(build.code.size() * sizeof(build.code[0])), nativeData, sizeNativeData, codeStart))
{
for (OldNativeProto result : results)
destroyExecData(result.execdata);
return CodeGenCompilationResult::AllocationFailed;
}
if (gPerfLogFn && results.size() > 0)
gPerfLogFn(gPerfLogContext, uintptr_t(codeStart), uint32_t(results[0].exectarget), "<luau helpers>");
for (size_t i = 0; i < results.size(); ++i)
{
uint32_t begin = uint32_t(results[i].exectarget);
uint32_t end = i + 1 < results.size() ? uint32_t(results[i + 1].exectarget) : uint32_t(build.code.size() * sizeof(build.code[0]));
CODEGEN_ASSERT(begin < end);
if (gPerfLogFn)
logPerfFunction(results[i].p, uintptr_t(codeStart) + begin, end - begin);
ExtraExecData* extra = getExtraExecData(results[i].p, results[i].execdata);
extra->codeSize = end - begin;
}
for (const OldNativeProto& result : results)
{
// the memory is now managed by VM and will be freed via onDestroyFunction
result.p->execdata = result.execdata;
result.p->exectarget = uintptr_t(codeStart) + result.exectarget;
result.p->codeentry = &kCodeEntryInsn;
}
if (stats != nullptr)
{
for (const OldNativeProto& result : results)
{
stats->bytecodeSizeBytes += result.p->sizecode * sizeof(Instruction);
// Account for the native -> bytecode instruction offsets mapping:
stats->nativeMetadataSizeBytes += result.p->sizecode * sizeof(uint32_t);
}
stats->functionsCompiled += uint32_t(results.size());
stats->nativeCodeSizeBytes += build.code.size();
stats->nativeDataSizeBytes += build.data.size();
}
return codeGenCompilationResult;
}
static CompilationResult compile_OLD(lua_State* L, int idx, unsigned int flags, CompilationStats* stats)
{
CODEGEN_ASSERT(FFlag::LuauCodegenDetailedCompilationResult);
CompilationResult compilationResult;
CODEGEN_ASSERT(lua_isLfunction(L, idx));

View File

@ -1,7 +1,9 @@
// This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details
#include "Luau/CodeGen.h"
#include "Luau/BytecodeAnalysis.h"
#include "Luau/BytecodeUtils.h"
#include "Luau/BytecodeSummary.h"
#include "Luau/IrDump.h"
#include "CodeGenLower.h"
@ -10,6 +12,8 @@
#include "lapi.h"
LUAU_FASTFLAG(LuauCodegenTypeInfo)
namespace Luau
{
namespace CodeGen
@ -44,6 +48,35 @@ static void logFunctionHeader(AssemblyBuilder& build, Proto* proto)
build.logAppend("\n");
}
template<typename AssemblyBuilder>
static void logFunctionTypes(AssemblyBuilder& build, const IrFunction& function)
{
CODEGEN_ASSERT(FFlag::LuauCodegenTypeInfo);
const BytecodeTypeInfo& typeInfo = function.bcTypeInfo;
for (size_t i = 0; i < typeInfo.argumentTypes.size(); i++)
{
uint8_t ty = typeInfo.argumentTypes[i];
if (ty != LBC_TYPE_ANY)
build.logAppend("; R%d: %s [argument]\n", int(i), getBytecodeTypeName(ty));
}
for (size_t i = 0; i < typeInfo.upvalueTypes.size(); i++)
{
uint8_t ty = typeInfo.upvalueTypes[i];
if (ty != LBC_TYPE_ANY)
build.logAppend("; U%d: %s\n", int(i), getBytecodeTypeName(ty));
}
for (const BytecodeRegTypeInfo& el : typeInfo.regTypes)
{
build.logAppend("; R%d: %s from %d to %d\n", el.reg, getBytecodeTypeName(el.type), el.startpc, el.endpc);
}
}
unsigned getInstructionCount(const Instruction* insns, const unsigned size)
{
unsigned count = 0;
@ -100,6 +133,9 @@ static std::string getAssemblyImpl(AssemblyBuilder& build, const TValue* func, A
if (options.includeAssembly || options.includeIr)
logFunctionHeader(build, p);
if (FFlag::LuauCodegenTypeInfo && options.includeIrTypes)
logFunctionTypes(build, ir.function);
CodeGenCompilationResult result = CodeGenCompilationResult::Success;
if (!lowerFunction(ir, build, helpers, p, options, stats, result))

View File

@ -14,6 +14,7 @@
LUAU_FASTFLAGVARIABLE(LuauCodegenContext, false)
LUAU_FASTFLAGVARIABLE(LuauCodegenCheckNullContext, false)
LUAU_FASTINT(LuauCodeGenBlockSize)
LUAU_FASTINT(LuauCodeGenMaxTotalSize)
@ -403,6 +404,7 @@ static size_t getMemorySize(lua_State* L, Proto* proto)
static void initializeExecutionCallbacks(lua_State* L, BaseCodeGenContext* codeGenContext) noexcept
{
CODEGEN_ASSERT(FFlag::LuauCodegenContext);
CODEGEN_ASSERT(!FFlag::LuauCodegenCheckNullContext || codeGenContext != nullptr);
lua_ExecutionCallbacks* ecb = &L->global->ecb;

View File

@ -13,6 +13,9 @@
#include <string.h>
LUAU_FASTFLAG(LuauLoadTypeInfo) // Because new VM typeinfo load changes the format used by Codegen, same flag is used
LUAU_FASTFLAG(LuauTypeInfoLookupImprovement)
namespace Luau
{
namespace CodeGen
@ -24,15 +27,17 @@ IrBuilder::IrBuilder()
: constantMap({IrConstKind::Tag, ~0ull})
{
}
static bool hasTypedParameters(Proto* proto)
static bool hasTypedParameters_DEPRECATED(Proto* proto)
{
CODEGEN_ASSERT(!FFlag::LuauLoadTypeInfo);
return proto->typeinfo && proto->numparams != 0;
}
static void buildArgumentTypeChecks(IrBuilder& build, Proto* proto)
static void buildArgumentTypeChecks_DEPRECATED(IrBuilder& build, Proto* proto)
{
CODEGEN_ASSERT(hasTypedParameters(proto));
CODEGEN_ASSERT(!FFlag::LuauLoadTypeInfo);
CODEGEN_ASSERT(hasTypedParameters_DEPRECATED(proto));
for (int i = 0; i < proto->numparams; ++i)
{
@ -108,13 +113,117 @@ static void buildArgumentTypeChecks(IrBuilder& build, Proto* proto)
}
}
static bool hasTypedParameters(const BytecodeTypeInfo& typeInfo)
{
CODEGEN_ASSERT(FFlag::LuauLoadTypeInfo);
if (FFlag::LuauTypeInfoLookupImprovement)
{
for (auto el : typeInfo.argumentTypes)
{
if (el != LBC_TYPE_ANY)
return true;
}
return false;
}
else
{
return !typeInfo.argumentTypes.empty();
}
}
static void buildArgumentTypeChecks(IrBuilder& build)
{
CODEGEN_ASSERT(FFlag::LuauLoadTypeInfo);
const BytecodeTypeInfo& typeInfo = build.function.bcTypeInfo;
CODEGEN_ASSERT(hasTypedParameters(typeInfo));
for (size_t i = 0; i < typeInfo.argumentTypes.size(); i++)
{
uint8_t et = typeInfo.argumentTypes[i];
uint8_t tag = et & ~LBC_TYPE_OPTIONAL_BIT;
uint8_t optional = et & LBC_TYPE_OPTIONAL_BIT;
if (tag == LBC_TYPE_ANY)
continue;
IrOp load = build.inst(IrCmd::LOAD_TAG, build.vmReg(uint8_t(i)));
IrOp nextCheck;
if (optional)
{
nextCheck = build.block(IrBlockKind::Internal);
IrOp fallbackCheck = build.block(IrBlockKind::Internal);
build.inst(IrCmd::JUMP_EQ_TAG, load, build.constTag(LUA_TNIL), nextCheck, fallbackCheck);
build.beginBlock(fallbackCheck);
}
switch (tag)
{
case LBC_TYPE_NIL:
build.inst(IrCmd::CHECK_TAG, load, build.constTag(LUA_TNIL), build.vmExit(kVmExitEntryGuardPc));
break;
case LBC_TYPE_BOOLEAN:
build.inst(IrCmd::CHECK_TAG, load, build.constTag(LUA_TBOOLEAN), build.vmExit(kVmExitEntryGuardPc));
break;
case LBC_TYPE_NUMBER:
build.inst(IrCmd::CHECK_TAG, load, build.constTag(LUA_TNUMBER), build.vmExit(kVmExitEntryGuardPc));
break;
case LBC_TYPE_STRING:
build.inst(IrCmd::CHECK_TAG, load, build.constTag(LUA_TSTRING), build.vmExit(kVmExitEntryGuardPc));
break;
case LBC_TYPE_TABLE:
build.inst(IrCmd::CHECK_TAG, load, build.constTag(LUA_TTABLE), build.vmExit(kVmExitEntryGuardPc));
break;
case LBC_TYPE_FUNCTION:
build.inst(IrCmd::CHECK_TAG, load, build.constTag(LUA_TFUNCTION), build.vmExit(kVmExitEntryGuardPc));
break;
case LBC_TYPE_THREAD:
build.inst(IrCmd::CHECK_TAG, load, build.constTag(LUA_TTHREAD), build.vmExit(kVmExitEntryGuardPc));
break;
case LBC_TYPE_USERDATA:
build.inst(IrCmd::CHECK_TAG, load, build.constTag(LUA_TUSERDATA), build.vmExit(kVmExitEntryGuardPc));
break;
case LBC_TYPE_VECTOR:
build.inst(IrCmd::CHECK_TAG, load, build.constTag(LUA_TVECTOR), build.vmExit(kVmExitEntryGuardPc));
break;
case LBC_TYPE_BUFFER:
build.inst(IrCmd::CHECK_TAG, load, build.constTag(LUA_TBUFFER), build.vmExit(kVmExitEntryGuardPc));
break;
}
if (optional)
{
build.inst(IrCmd::JUMP, nextCheck);
build.beginBlock(nextCheck);
}
}
// If the last argument is optional, we can skip creating a new internal block since one will already have been created.
if (!(typeInfo.argumentTypes.back() & LBC_TYPE_OPTIONAL_BIT))
{
IrOp next = build.block(IrBlockKind::Internal);
build.inst(IrCmd::JUMP, next);
build.beginBlock(next);
}
}
void IrBuilder::buildFunctionIr(Proto* proto)
{
function.proto = proto;
function.variadic = proto->is_vararg != 0;
if (FFlag::LuauLoadTypeInfo)
loadBytecodeTypeInfo(function);
// Reserve entry block
bool generateTypeChecks = hasTypedParameters(proto);
bool generateTypeChecks = FFlag::LuauLoadTypeInfo ? hasTypedParameters(function.bcTypeInfo) : hasTypedParameters_DEPRECATED(proto);
IrOp entry = generateTypeChecks ? block(IrBlockKind::Internal) : IrOp{};
// Rebuild original control flow blocks
@ -128,7 +237,12 @@ void IrBuilder::buildFunctionIr(Proto* proto)
if (generateTypeChecks)
{
beginBlock(entry);
buildArgumentTypeChecks(*this, proto);
if (FFlag::LuauLoadTypeInfo)
buildArgumentTypeChecks(*this);
else
buildArgumentTypeChecks_DEPRECATED(*this, proto);
inst(IrCmd::JUMP, blockAtInst(0));
}
else

View File

@ -482,30 +482,30 @@ void toString(std::string& result, IrConst constant)
const char* getBytecodeTypeName(uint8_t type)
{
switch (type)
switch (type & ~LBC_TYPE_OPTIONAL_BIT)
{
case LBC_TYPE_NIL:
return "nil";
return (type & LBC_TYPE_OPTIONAL_BIT) != 0 ? "nil?" : "nil";
case LBC_TYPE_BOOLEAN:
return "boolean";
return (type & LBC_TYPE_OPTIONAL_BIT) != 0 ? "boolean?" : "boolean";
case LBC_TYPE_NUMBER:
return "number";
return (type & LBC_TYPE_OPTIONAL_BIT) != 0 ? "number?" : "number";
case LBC_TYPE_STRING:
return "string";
return (type & LBC_TYPE_OPTIONAL_BIT) != 0 ? "string?" : "string";
case LBC_TYPE_TABLE:
return "table";
return (type & LBC_TYPE_OPTIONAL_BIT) != 0 ? "table?" : "table";
case LBC_TYPE_FUNCTION:
return "function";
return (type & LBC_TYPE_OPTIONAL_BIT) != 0 ? "function?" : "function";
case LBC_TYPE_THREAD:
return "thread";
return (type & LBC_TYPE_OPTIONAL_BIT) != 0 ? "thread?" : "thread";
case LBC_TYPE_USERDATA:
return "userdata";
return (type & LBC_TYPE_OPTIONAL_BIT) != 0 ? "userdata?" : "userdata";
case LBC_TYPE_VECTOR:
return "vector";
return (type & LBC_TYPE_OPTIONAL_BIT) != 0 ? "vector?" : "vector";
case LBC_TYPE_BUFFER:
return "buffer";
return (type & LBC_TYPE_OPTIONAL_BIT) != 0 ? "buffer?" : "buffer";
case LBC_TYPE_ANY:
return "any";
return (type & LBC_TYPE_OPTIONAL_BIT) != 0 ? "any?" : "any";
}
CODEGEN_ASSERT(!"Unhandled type in getBytecodeTypeName");

View File

@ -12,7 +12,6 @@
#include "lgc.h"
LUAU_FASTFLAG(LuauCodegenRemoveDeadStores5)
LUAU_FASTFLAG(LuauCodegenCheckTruthyFormB)
namespace Luau
{
@ -1445,7 +1444,7 @@ void IrLoweringA64::lowerInst(IrInst& inst, uint32_t index, const IrBlock& next)
}
// fail to fallback on 'false' boolean value (falsy)
if (!FFlag::LuauCodegenCheckTruthyFormB || inst.b.kind != IrOpKind::Constant)
if (inst.b.kind != IrOpKind::Constant)
{
build.cbz(regOp(inst.b), target);
}

View File

@ -15,8 +15,6 @@
#include "lstate.h"
#include "lgc.h"
LUAU_FASTFLAGVARIABLE(LuauCodegenCheckTruthyFormB, false)
namespace Luau
{
namespace CodeGen
@ -1197,7 +1195,7 @@ void IrLoweringX64::lowerInst(IrInst& inst, uint32_t index, const IrBlock& next)
}
// fail to fallback on 'false' boolean value (falsy)
if (!FFlag::LuauCodegenCheckTruthyFormB || inst.b.kind != IrOpKind::Constant)
if (inst.b.kind != IrOpKind::Constant)
{
build.cmp(memRegUintOp(inst.b), 0);
jumpOrAbortOnUndef(ConditionX64::Equal, inst.c, next);

View File

@ -12,7 +12,6 @@
#include "lstate.h"
#include "ltm.h"
LUAU_FASTFLAGVARIABLE(LuauCodegenLoadTVTag, false)
LUAU_FASTFLAGVARIABLE(LuauCodegenDirectUserdataFlow, false)
namespace Luau
@ -111,20 +110,13 @@ static void translateInstLoadConstant(IrBuilder& build, int ra, int k)
build.inst(IrCmd::STORE_DOUBLE, build.vmReg(ra), build.constDouble(protok.value.n));
build.inst(IrCmd::STORE_TAG, build.vmReg(ra), build.constTag(LUA_TNUMBER));
}
else if (FFlag::LuauCodegenLoadTVTag)
else
{
// Tag could be LUA_TSTRING or LUA_TVECTOR; for TSTRING we could generate LOAD_POINTER/STORE_POINTER/STORE_TAG, but it's not profitable;
// however, it's still valuable to preserve the tag throughout the optimization pipeline to eliminate tag checks.
IrOp load = build.inst(IrCmd::LOAD_TVALUE, build.vmConst(k), build.constInt(0), build.constTag(protok.tt));
build.inst(IrCmd::STORE_TVALUE, build.vmReg(ra), load);
}
else
{
// Remaining tag here right now is LUA_TSTRING, while it can be transformed to LOAD_POINTER/STORE_POINTER/STORE_TAG, it's not profitable right
// now
IrOp load = build.inst(IrCmd::LOAD_TVALUE, build.vmConst(k));
build.inst(IrCmd::STORE_TVALUE, build.vmReg(ra), load);
}
}
void translateInstLoadK(IrBuilder& build, const Instruction* pc)

View File

@ -18,8 +18,6 @@ LUAU_FASTINTVARIABLE(LuauCodeGenMinLinearBlockPath, 3)
LUAU_FASTINTVARIABLE(LuauCodeGenReuseSlotLimit, 64)
LUAU_FASTFLAGVARIABLE(DebugLuauAbortingChecks, false)
LUAU_FASTFLAG(LuauCodegenRemoveDeadStores5)
LUAU_FASTFLAG(LuauCodegenLoadTVTag)
LUAU_FASTFLAGVARIABLE(LuauCodegenInferNumTag, false)
LUAU_FASTFLAGVARIABLE(LuauCodegenLoadPropCheckRegLinkInTv, false)
namespace Luau
@ -720,7 +718,7 @@ static void constPropInInst(ConstPropState& state, IrBuilder& build, IrFunction&
if (arg->cmd == IrCmd::TAG_VECTOR)
tag = LUA_TVECTOR;
if (FFlag::LuauCodegenLoadTVTag && arg->cmd == IrCmd::LOAD_TVALUE && arg->c.kind != IrOpKind::None)
if (arg->cmd == IrCmd::LOAD_TVALUE && arg->c.kind != IrOpKind::None)
tag = function.tagOp(arg->c);
}
}
@ -897,59 +895,34 @@ static void constPropInInst(ConstPropState& state, IrBuilder& build, IrFunction&
case IrCmd::CHECK_TAG:
{
uint8_t b = function.tagOp(inst.b);
uint8_t tag = state.tryGetTag(inst.a);
if (FFlag::LuauCodegenInferNumTag)
if (tag == 0xff)
{
uint8_t tag = state.tryGetTag(inst.a);
if (tag == 0xff)
if (IrOp value = state.tryGetValue(inst.a); value.kind == IrOpKind::Constant)
{
if (IrOp value = state.tryGetValue(inst.a); value.kind == IrOpKind::Constant)
{
if (function.constOp(value).kind == IrConstKind::Double)
tag = LUA_TNUMBER;
}
if (function.constOp(value).kind == IrConstKind::Double)
tag = LUA_TNUMBER;
}
}
if (tag != 0xff)
if (tag != 0xff)
{
if (tag == b)
{
if (tag == b)
{
if (FFlag::DebugLuauAbortingChecks)
replace(function, inst.c, build.undef());
else
kill(function, inst);
}
if (FFlag::DebugLuauAbortingChecks)
replace(function, inst.c, build.undef());
else
{
replace(function, block, index, {IrCmd::JUMP, inst.c}); // Shows a conflict in assumptions on this path
}
kill(function, inst);
}
else
{
state.updateTag(inst.a, b); // We can assume the tag value going forward
replace(function, block, index, {IrCmd::JUMP, inst.c}); // Shows a conflict in assumptions on this path
}
}
else
{
if (uint8_t tag = state.tryGetTag(inst.a); tag != 0xff)
{
if (tag == b)
{
if (FFlag::DebugLuauAbortingChecks)
replace(function, inst.c, build.undef());
else
kill(function, inst);
}
else
{
replace(function, block, index, {IrCmd::JUMP, inst.c}); // Shows a conflict in assumptions on this path
}
}
else
{
state.updateTag(inst.a, b); // We can assume the tag value going forward
}
state.updateTag(inst.a, b); // We can assume the tag value going forward
}
break;
}

View File

@ -10,7 +10,6 @@
#include "lobject.h"
LUAU_FASTFLAGVARIABLE(LuauCodegenRemoveDeadStores5, false)
LUAU_FASTFLAG(LuauCodegenLoadTVTag)
// TODO: optimization can be improved by knowing which registers are live in at each VM exit
@ -519,7 +518,7 @@ static void markDeadStoresInInst(RemoveDeadStoreState& state, IrBuilder& build,
if (arg->cmd == IrCmd::TAG_VECTOR)
regInfo.maybeGco = false;
if (FFlag::LuauCodegenLoadTVTag && arg->cmd == IrCmd::LOAD_TVALUE && arg->c.kind != IrOpKind::None)
if (arg->cmd == IrCmd::LOAD_TVALUE && arg->c.kind != IrOpKind::None)
regInfo.maybeGco = isGCO(function.tagOp(arg->c));
}

View File

@ -5,8 +5,6 @@
#include "lapi.h"
LUAU_FASTFLAG(LuauCodegenDetailedCompilationResult)
int luau_codegen_supported()
{
return Luau::CodeGen::isSupported();
@ -19,8 +17,5 @@ void luau_codegen_create(lua_State* L)
void luau_codegen_compile(lua_State* L, int idx)
{
if (FFlag::LuauCodegenDetailedCompilationResult)
Luau::CodeGen::compile(L, idx);
else
Luau::CodeGen::compile_DEPRECATED(L, idx);
Luau::CodeGen::compile(L, idx);
}

View File

@ -47,6 +47,10 @@
// Version 4: Adds Proto::flags, typeinfo, and floor division opcodes IDIV/IDIVK. Currently supported.
// Version 5: Adds SUBRK/DIVRK and vector constants. Currently supported.
// # Bytecode type information history
// Version 1: (from bytecode version 4) Type information for function signature. Currently supported.
// Version 2: (from bytecode version 4) Type information for arguments, upvalues, locals and some temporaries. Currently supported.
// Bytecode opcode, part of the instruction header
enum LuauOpcode
{
@ -433,7 +437,8 @@ enum LuauBytecodeTag
LBC_VERSION_MAX = 5,
LBC_VERSION_TARGET = 5,
// Type encoding version
LBC_TYPE_VERSION = 1,
LBC_TYPE_VERSION_DEPRECATED = 1,
LBC_TYPE_VERSION = 2,
// Types of constant table entries
LBC_CONSTANT_NIL = 0,
LBC_CONSTANT_BOOLEAN,

View File

@ -76,6 +76,8 @@ public:
void expandJumps();
void setFunctionTypeInfo(std::string value);
void pushLocalTypeInfo(LuauBytecodeType type, uint8_t reg, uint32_t startpc, uint32_t endpc);
void pushUpvalTypeInfo(LuauBytecodeType type);
void setDebugFunctionName(StringRef name);
void setDebugFunctionLineDefined(int line);
@ -98,6 +100,7 @@ public:
Dump_Source = 1 << 2,
Dump_Locals = 1 << 3,
Dump_Remarks = 1 << 4,
Dump_Types = 1 << 5,
};
void setDumpFlags(uint32_t flags)
@ -213,6 +216,19 @@ private:
unsigned int name;
};
struct TypedLocal
{
LuauBytecodeType type;
uint8_t reg;
uint32_t startpc;
uint32_t endpc;
};
struct TypedUpval
{
LuauBytecodeType type;
};
struct Jump
{
uint32_t source;
@ -258,6 +274,9 @@ private:
std::vector<DebugLocal> debugLocals;
std::vector<DebugUpval> debugUpvals;
std::vector<TypedLocal> typedLocals;
std::vector<TypedUpval> typedUpvals;
DenseHashMap<StringRef, unsigned int, StringRefHash> stringTable;
std::vector<StringRef> debugStrings;
@ -271,6 +290,8 @@ private:
std::vector<std::string> dumpSource;
std::vector<std::pair<int, std::string>> dumpRemarks;
std::string tempTypeInfo;
std::string (BytecodeBuilder::*dumpFunctionPtr)(std::vector<int>&) const = nullptr;
void validate() const;
@ -281,7 +302,7 @@ private:
void dumpConstant(std::string& result, int k) const;
void dumpInstruction(const uint32_t* opcode, std::string& output, int targetLabel) const;
void writeFunction(std::string& ss, uint32_t id, uint8_t flags) const;
void writeFunction(std::string& ss, uint32_t id, uint8_t flags);
void writeLineInfo(std::string& ss) const;
void writeStringTable(std::string& ss) const;

View File

@ -26,6 +26,12 @@ struct CompileOptions
// 2 - full debug info with local & upvalue names; necessary for debugger
int debugLevel = 1;
// type information is used to guide native code generation decisions
// information includes testable types for function arguments, locals, upvalues and some temporaries
// 0 - generate for native modules
// 1 - generate for all modules
int typeInfoLevel = 0;
// 0 - no code coverage support
// 1 - statement coverage
// 2 - statement and expression coverage (verbose)

View File

@ -22,6 +22,12 @@ struct lua_CompileOptions
// 2 - full debug info with local & upvalue names; necessary for debugger
int debugLevel; // default=1
// type information is used to guide native code generation decisions
// information includes testable types for function arguments, locals, upvalues and some temporaries
// 0 - generate for native modules
// 1 - generate for all modules
int typeInfoLevel; // default=0
// 0 - no code coverage support
// 1 - statement coverage
// 2 - statement and expression coverage (verbose)

View File

@ -9,6 +9,7 @@
LUAU_FASTFLAGVARIABLE(LuauCompileNoJumpLineRetarget, false)
LUAU_FASTFLAG(LuauCompileRepeatUntilSkippedLocals)
LUAU_FASTFLAGVARIABLE(LuauCompileTypeInfo, false)
namespace Luau
{
@ -281,6 +282,12 @@ void BytecodeBuilder::endFunction(uint8_t maxstacksize, uint8_t numupvalues, uin
debugLocals.clear();
debugUpvals.clear();
if (FFlag::LuauCompileTypeInfo)
{
typedLocals.clear();
typedUpvals.clear();
}
constantMap.clear();
tableShapeMap.clear();
protoMap.clear();
@ -537,6 +544,29 @@ void BytecodeBuilder::setFunctionTypeInfo(std::string value)
functions[currentFunction].typeinfo = std::move(value);
}
void BytecodeBuilder::pushLocalTypeInfo(LuauBytecodeType type, uint8_t reg, uint32_t startpc, uint32_t endpc)
{
LUAU_ASSERT(FFlag::LuauCompileTypeInfo);
TypedLocal local;
local.type = type;
local.reg = reg;
local.startpc = startpc;
local.endpc = endpc;
typedLocals.push_back(local);
}
void BytecodeBuilder::pushUpvalTypeInfo(LuauBytecodeType type)
{
LUAU_ASSERT(FFlag::LuauCompileTypeInfo);
TypedUpval upval;
upval.type = type;
typedUpvals.push_back(upval);
}
void BytecodeBuilder::setDebugFunctionName(StringRef name)
{
unsigned int index = addStringTableEntry(name);
@ -636,7 +666,6 @@ void BytecodeBuilder::finalize()
bytecode = char(version);
uint8_t typesversion = getTypeEncodingVersion();
LUAU_ASSERT(typesversion == 1);
writeByte(bytecode, typesversion);
writeStringTable(bytecode);
@ -650,7 +679,7 @@ void BytecodeBuilder::finalize()
writeVarInt(bytecode, mainFunction);
}
void BytecodeBuilder::writeFunction(std::string& ss, uint32_t id, uint8_t flags) const
void BytecodeBuilder::writeFunction(std::string& ss, uint32_t id, uint8_t flags)
{
LUAU_ASSERT(id < functions.size());
const Function& func = functions[id];
@ -663,8 +692,43 @@ void BytecodeBuilder::writeFunction(std::string& ss, uint32_t id, uint8_t flags)
writeByte(ss, flags);
writeVarInt(ss, uint32_t(func.typeinfo.size()));
ss.append(func.typeinfo);
if (FFlag::LuauCompileTypeInfo)
{
if (!func.typeinfo.empty() || !typedUpvals.empty() || !typedLocals.empty())
{
// collect type info into a temporary string to know the overall size of type data
tempTypeInfo.clear();
writeVarInt(tempTypeInfo, uint32_t(func.typeinfo.size()));
writeVarInt(tempTypeInfo, uint32_t(typedUpvals.size()));
writeVarInt(tempTypeInfo, uint32_t(typedLocals.size()));
tempTypeInfo.append(func.typeinfo);
for (const TypedUpval& l : typedUpvals)
writeByte(tempTypeInfo, l.type);
for (const TypedLocal& l : typedLocals)
{
writeByte(tempTypeInfo, l.type);
writeByte(tempTypeInfo, l.reg);
writeVarInt(tempTypeInfo, l.startpc);
LUAU_ASSERT(l.endpc >= l.startpc);
writeVarInt(tempTypeInfo, l.endpc - l.startpc);
}
writeVarInt(ss, uint32_t(tempTypeInfo.size()));
ss.append(tempTypeInfo);
}
else
{
writeVarInt(ss, 0);
}
}
else
{
writeVarInt(ss, uint32_t(func.typeinfo.size()));
ss.append(func.typeinfo);
}
// instructions
writeVarInt(ss, uint32_t(insns.size()));
@ -1134,7 +1198,7 @@ uint8_t BytecodeBuilder::getVersion()
uint8_t BytecodeBuilder::getTypeEncodingVersion()
{
return LBC_TYPE_VERSION;
return FFlag::LuauCompileTypeInfo ? LBC_TYPE_VERSION : LBC_TYPE_VERSION_DEPRECATED;
}
#ifdef LUAU_ASSERTENABLED
@ -2162,6 +2226,39 @@ void BytecodeBuilder::dumpInstruction(const uint32_t* code, std::string& result,
}
}
static const char* getBaseTypeString(uint8_t type)
{
uint8_t tag = type & ~LBC_TYPE_OPTIONAL_BIT;
switch (tag)
{
case LBC_TYPE_NIL:
return "nil";
case LBC_TYPE_BOOLEAN:
return "boolean";
case LBC_TYPE_NUMBER:
return "number";
case LBC_TYPE_STRING:
return "string";
case LBC_TYPE_TABLE:
return "table";
case LBC_TYPE_FUNCTION:
return "function";
case LBC_TYPE_THREAD:
return "thread";
case LBC_TYPE_USERDATA:
return "userdata";
case LBC_TYPE_VECTOR:
return "vector";
case LBC_TYPE_BUFFER:
return "buffer";
case LBC_TYPE_ANY:
return "any";
}
LUAU_ASSERT(!"Unhandled type in getBaseTypeString");
return nullptr;
}
std::string BytecodeBuilder::dumpCurrentFunction(std::vector<int>& dumpinstoffs) const
{
if ((dumpFlags & Dump_Code) == 0)
@ -2198,6 +2295,45 @@ std::string BytecodeBuilder::dumpCurrentFunction(std::vector<int>& dumpinstoffs)
}
}
if (FFlag::LuauCompileTypeInfo)
{
if (dumpFlags & Dump_Types)
{
const std::string& typeinfo = functions.back().typeinfo;
// Arguments start from third byte in function typeinfo string
for (uint8_t i = 2; i < typeinfo.size(); ++i)
{
uint8_t et = typeinfo[i];
const char* base = getBaseTypeString(et);
const char* optional = (et & LBC_TYPE_OPTIONAL_BIT) ? "?" : "";
formatAppend(result, "R%d: %s%s [argument]\n", i - 2, base, optional);
}
for (size_t i = 0; i < typedUpvals.size(); ++i)
{
const TypedUpval& l = typedUpvals[i];
const char* base = getBaseTypeString(l.type);
const char* optional = (l.type & LBC_TYPE_OPTIONAL_BIT) ? "?" : "";
formatAppend(result, "U%d: %s%s\n", int(i), base, optional);
}
for (size_t i = 0; i < typedLocals.size(); ++i)
{
const TypedLocal& l = typedLocals[i];
const char* base = getBaseTypeString(l.type);
const char* optional = (l.type & LBC_TYPE_OPTIONAL_BIT) ? "?" : "";
formatAppend(result, "R%d: %s%s from %d to %d\n", l.reg, base, optional, l.startpc, l.endpc);
}
}
}
std::vector<int> labels(insns.size(), -1);
// annotate valid jump targets with 0
@ -2364,39 +2500,6 @@ std::string BytecodeBuilder::dumpSourceRemarks() const
return result;
}
static const char* getBaseTypeString(uint8_t type)
{
uint8_t tag = type & ~LBC_TYPE_OPTIONAL_BIT;
switch (tag)
{
case LBC_TYPE_NIL:
return "nil";
case LBC_TYPE_BOOLEAN:
return "boolean";
case LBC_TYPE_NUMBER:
return "number";
case LBC_TYPE_STRING:
return "string";
case LBC_TYPE_TABLE:
return "table";
case LBC_TYPE_FUNCTION:
return "function";
case LBC_TYPE_THREAD:
return "thread";
case LBC_TYPE_USERDATA:
return "userdata";
case LBC_TYPE_VECTOR:
return "vector";
case LBC_TYPE_BUFFER:
return "buffer";
case LBC_TYPE_ANY:
return "any";
}
LUAU_ASSERT(!"Unhandled type in getBaseTypeString");
return nullptr;
}
std::string BytecodeBuilder::dumpTypeInfo() const
{
std::string result;

View File

@ -27,6 +27,8 @@ LUAU_FASTINTVARIABLE(LuauCompileInlineThresholdMaxBoost, 300)
LUAU_FASTINTVARIABLE(LuauCompileInlineDepth, 5)
LUAU_FASTFLAGVARIABLE(LuauCompileRepeatUntilSkippedLocals, false)
LUAU_FASTFLAG(LuauCompileTypeInfo)
LUAU_FASTFLAGVARIABLE(LuauTypeInfoLookupImprovement, false)
namespace Luau
{
@ -40,6 +42,8 @@ static const uint32_t kMaxInstructionCount = 1'000'000'000;
static const uint8_t kInvalidReg = 255;
static const uint32_t kDefaultAllocPc = ~0u;
CompileError::CompileError(const Location& location, const std::string& message)
: location(location)
, message(message)
@ -102,7 +106,8 @@ struct Compiler
, locstants(nullptr)
, tableShapes(nullptr)
, builtins(nullptr)
, typeMap(nullptr)
, functionTypes(nullptr)
, localTypes(nullptr)
{
// preallocate some buffers that are very likely to grow anyway; this works around std::vector's inefficient growth policy for small arrays
localStack.reserve(16);
@ -204,9 +209,12 @@ struct Compiler
setDebugLine(func);
// note: we move types out of typeMap which is safe because compileFunction is only called once per function
if (std::string* funcType = typeMap.find(func))
bytecode.setFunctionTypeInfo(std::move(*funcType));
if (!FFlag::LuauCompileTypeInfo)
{
// note: we move types out of typeMap which is safe because compileFunction is only called once per function
if (std::string* funcType = functionTypes.find(func))
bytecode.setFunctionTypeInfo(std::move(*funcType));
}
if (func->vararg)
bytecode.emitABC(LOP_PREPVARARGS, uint8_t(self + func->args.size), 0, 0);
@ -214,10 +222,13 @@ struct Compiler
uint8_t args = allocReg(func, self + unsigned(func->args.size));
if (func->self)
pushLocal(func->self, args);
pushLocal(func->self, args, kDefaultAllocPc);
for (size_t i = 0; i < func->args.size; ++i)
pushLocal(func->args.data[i], uint8_t(args + self + i));
pushLocal(func->args.data[i], uint8_t(args + self + i), kDefaultAllocPc);
if (FFlag::LuauCompileTypeInfo)
argCount = localStack.size();
AstStatBlock* stat = func->body;
@ -249,6 +260,19 @@ struct Compiler
bytecode.pushDebugUpval(sref(l->name));
}
if (FFlag::LuauCompileTypeInfo && options.typeInfoLevel >= 1)
{
for (AstLocal* l : upvals)
{
LuauBytecodeType ty = LBC_TYPE_ANY;
if (LuauBytecodeType* recordedTy = localTypes.find(l))
ty = *recordedTy;
bytecode.pushUpvalTypeInfo(ty);
}
}
if (options.optimizationLevel >= 1)
bytecode.foldJumps();
@ -259,6 +283,13 @@ struct Compiler
if (bytecode.getInstructionCount() > kMaxInstructionCount)
CompileError::raise(func->location, "Exceeded function instruction limit; split the function into parts to compile");
if (FFlag::LuauCompileTypeInfo)
{
// note: we move types out of typeMap which is safe because compileFunction is only called once per function
if (std::string* funcType = functionTypes.find(func))
bytecode.setFunctionTypeInfo(std::move(*funcType));
}
// top-level code only executes once so it can be marked as cold if it has no loops; code with loops might be profitable to compile natively
if (func->functionDepth == 0 && !hasLoops)
protoflags |= LPF_NATIVE_COLD;
@ -287,6 +318,10 @@ struct Compiler
upvals.clear(); // note: instead of std::move above, we copy & clear to preserve capacity for future pushes
stackSize = 0;
if (FFlag::LuauCompileTypeInfo)
argCount = 0;
hasLoops = false;
return fid;
@ -585,6 +620,7 @@ struct Compiler
// if the last argument can return multiple values, we need to compute all of them into the remaining arguments
unsigned int tail = unsigned(func->args.size - expr->args.size) + 1;
uint8_t reg = allocReg(arg, tail);
uint32_t allocpc = FFlag::LuauCompileTypeInfo ? bytecode.getDebugPC() : kDefaultAllocPc;
if (AstExprCall* expr = arg->as<AstExprCall>())
compileExprCall(expr, reg, tail, /* targetTop= */ true);
@ -594,7 +630,12 @@ struct Compiler
LUAU_ASSERT(!"Unexpected expression type");
for (size_t j = i; j < func->args.size; ++j)
args.push_back({func->args.data[j], uint8_t(reg + (j - i))});
{
if (FFlag::LuauCompileTypeInfo)
args.push_back({func->args.data[j], uint8_t(reg + (j - i)), {Constant::Type_Unknown}, allocpc});
else
args.push_back({func->args.data[j], uint8_t(reg + (j - i))});
}
// all remaining function arguments have been allocated and assigned to
break;
@ -603,13 +644,17 @@ struct Compiler
{
// if the argument is mutated, we need to allocate a fresh register even if it's a constant
uint8_t reg = allocReg(arg, 1);
uint32_t allocpc = FFlag::LuauCompileTypeInfo ? bytecode.getDebugPC() : kDefaultAllocPc;
if (arg)
compileExprTemp(arg, reg);
else
bytecode.emitABC(LOP_LOADNIL, reg, 0, 0);
args.push_back({var, reg});
if (FFlag::LuauCompileTypeInfo)
args.push_back({var, reg, {Constant::Type_Unknown}, allocpc});
else
args.push_back({var, reg});
}
else if (arg == nullptr)
{
@ -629,14 +674,22 @@ struct Compiler
// if the argument is a local that isn't mutated, we will simply reuse the existing register
if (int reg = le ? getExprLocalReg(le) : -1; reg >= 0 && (!lv || !lv->written))
{
args.push_back({var, uint8_t(reg)});
if (FFlag::LuauTypeInfoLookupImprovement)
args.push_back({var, uint8_t(reg), {Constant::Type_Unknown}, kDefaultAllocPc});
else
args.push_back({var, uint8_t(reg)});
}
else
{
uint8_t temp = allocReg(arg, 1);
uint32_t allocpc = FFlag::LuauCompileTypeInfo ? bytecode.getDebugPC() : kDefaultAllocPc;
compileExprTemp(arg, temp);
args.push_back({var, temp});
if (FFlag::LuauCompileTypeInfo)
args.push_back({var, temp, {Constant::Type_Unknown}, allocpc});
else
args.push_back({var, temp});
}
}
}
@ -651,7 +704,10 @@ struct Compiler
{
if (arg.value.type == Constant::Type_Unknown)
{
pushLocal(arg.local, arg.reg);
if (FFlag::LuauCompileTypeInfo)
pushLocal(arg.local, arg.reg, arg.allocpc);
else
pushLocal(arg.local, arg.reg, kDefaultAllocPc);
}
else
{
@ -2851,7 +2907,7 @@ struct Compiler
if (int reg = getExprLocalReg(re); reg >= 0 && (!lv || !lv->written) && (!rv || !rv->written))
{
pushLocal(stat->vars.data[0], uint8_t(reg));
pushLocal(stat->vars.data[0], uint8_t(reg), kDefaultAllocPc);
return;
}
}
@ -2859,11 +2915,12 @@ struct Compiler
// note: allocReg in this case allocates into parent block register - note that we don't have RegScope here
uint8_t vars = allocReg(stat, unsigned(stat->vars.size));
uint32_t allocpc = FFlag::LuauCompileTypeInfo ? bytecode.getDebugPC() : kDefaultAllocPc;
compileExprListTemp(stat->values, vars, uint8_t(stat->vars.size), /* targetTop= */ true);
for (size_t i = 0; i < stat->vars.size; ++i)
pushLocal(stat->vars.data[i], uint8_t(vars + i));
pushLocal(stat->vars.data[i], uint8_t(vars + i), allocpc);
}
bool tryCompileUnrolledFor(AstStatFor* stat, int thresholdBase, int thresholdMaxBoost)
@ -2990,6 +3047,7 @@ struct Compiler
// this makes sure the code inside the loop can't interfere with the iteration process (other than modifying the table we're iterating
// through)
uint8_t varreg = regs + 2;
uint32_t varregallocpc = FFlag::LuauCompileTypeInfo ? bytecode.getDebugPC() : kDefaultAllocPc;
if (Variable* il = variables.find(stat->var); il && il->written)
varreg = allocReg(stat, 1);
@ -3011,7 +3069,7 @@ struct Compiler
if (varreg != regs + 2)
bytecode.emitABC(LOP_MOVE, varreg, regs + 2, 0);
pushLocal(stat->var, varreg);
pushLocal(stat->var, varreg, varregallocpc);
compileStat(stat->body);
@ -3056,6 +3114,7 @@ struct Compiler
// note that we reserve at least 2 variables; this allows our fast path to assume that we need 2 variables instead of 1 or 2
uint8_t vars = allocReg(stat, std::max(unsigned(stat->vars.size), 2u));
LUAU_ASSERT(vars == regs + 3);
uint32_t varsallocpc = FFlag::LuauCompileTypeInfo ? bytecode.getDebugPC() : kDefaultAllocPc;
LuauOpcode skipOp = LOP_FORGPREP;
@ -3091,7 +3150,7 @@ struct Compiler
size_t loopLabel = bytecode.emitLabel();
for (size_t i = 0; i < stat->vars.size; ++i)
pushLocal(stat->vars.data[i], uint8_t(vars + i));
pushLocal(stat->vars.data[i], uint8_t(vars + i), varsallocpc);
compileStat(stat->body);
@ -3515,7 +3574,7 @@ struct Compiler
{
uint8_t var = allocReg(stat, 1);
pushLocal(stat->name, var);
pushLocal(stat->name, var, kDefaultAllocPc);
compileExprFunction(stat->func, var);
Local& l = locals[stat->name];
@ -3569,7 +3628,7 @@ struct Compiler
getUpval(local);
}
void pushLocal(AstLocal* local, uint8_t reg)
void pushLocal(AstLocal* local, uint8_t reg, uint32_t allocpc)
{
if (localStack.size() >= kMaxLocalCount)
CompileError::raise(
@ -3584,6 +3643,9 @@ struct Compiler
l.reg = reg;
l.allocated = true;
l.debugpc = bytecode.getDebugPC();
if (FFlag::LuauCompileTypeInfo)
l.allocpc = allocpc == kDefaultAllocPc ? l.debugpc : allocpc;
}
bool areLocalsCaptured(size_t start)
@ -3645,6 +3707,17 @@ struct Compiler
bytecode.pushDebugLocal(sref(localStack[i]->name), l->reg, l->debugpc, debugpc);
}
if (FFlag::LuauCompileTypeInfo && options.typeInfoLevel >= 1 && i >= argCount)
{
uint32_t debugpc = bytecode.getDebugPC();
LuauBytecodeType ty = LBC_TYPE_ANY;
if (LuauBytecodeType* recordedTy = localTypes.find(localStack[i]))
ty = *recordedTy;
bytecode.pushLocalTypeInfo(ty, l->reg, l->allocpc, debugpc);
}
}
localStack.resize(start);
@ -3909,6 +3982,7 @@ struct Compiler
bool allocated = false;
bool captured = false;
uint32_t debugpc = 0;
uint32_t allocpc = 0;
};
struct LoopJump
@ -3937,6 +4011,7 @@ struct Compiler
uint8_t reg;
Constant value;
uint32_t allocpc;
};
struct InlineFrame
@ -3969,7 +4044,8 @@ struct Compiler
DenseHashMap<AstLocal*, Constant> locstants;
DenseHashMap<AstExprTable*, TableShape> tableShapes;
DenseHashMap<AstExprCall*, int> builtins;
DenseHashMap<AstExprFunction*, std::string> typeMap;
DenseHashMap<AstExprFunction*, std::string> functionTypes;
DenseHashMap<AstLocal*, LuauBytecodeType> localTypes;
const DenseHashMap<AstExprCall*, int>* builtinsFold = nullptr;
bool builtinsFoldMathK = false;
@ -3977,6 +4053,7 @@ struct Compiler
// compileFunction state, gets reset for every function
unsigned int regTop = 0;
unsigned int stackSize = 0;
size_t argCount = 0;
bool hasLoops = false;
bool getfenvUsed = false;
@ -4010,6 +4087,9 @@ void compileOrThrow(BytecodeBuilder& bytecode, const ParseResult& parseResult, c
{
mainFlags |= LPF_NATIVE_MODULE;
options.optimizationLevel = 2; // note: this might be removed in the future in favor of --!optimize
if (FFlag::LuauCompileTypeInfo)
options.typeInfoLevel = 1;
}
}
@ -4058,8 +4138,16 @@ void compileOrThrow(BytecodeBuilder& bytecode, const ParseResult& parseResult, c
root->visit(&functionVisitor);
// computes type information for all functions based on type annotations
if (functionVisitor.hasTypes)
buildTypeMap(compiler.typeMap, root, options.vectorType);
if (FFlag::LuauCompileTypeInfo)
{
if (options.typeInfoLevel >= 1)
buildTypeMap(compiler.functionTypes, compiler.localTypes, root, options.vectorType);
}
else
{
if (functionVisitor.hasTypes)
buildTypeMap(compiler.functionTypes, compiler.localTypes, root, options.vectorType);
}
for (AstExprFunction* expr : functions)
compiler.compileFunction(expr, 0);

View File

@ -3,6 +3,8 @@
#include "Luau/BytecodeBuilder.h"
LUAU_FASTFLAG(LuauCompileTypeInfo)
namespace Luau
{
@ -144,14 +146,17 @@ static std::string getFunctionType(const AstExprFunction* func, const DenseHashM
struct TypeMapVisitor : AstVisitor
{
DenseHashMap<AstExprFunction*, std::string>& typeMap;
DenseHashMap<AstExprFunction*, std::string>& functionTypes;
DenseHashMap<AstLocal*, LuauBytecodeType>& localTypes;
const char* vectorType;
DenseHashMap<AstName, AstStatTypeAlias*> typeAliases;
std::vector<std::pair<AstName, AstStatTypeAlias*>> typeAliasStack;
TypeMapVisitor(DenseHashMap<AstExprFunction*, std::string>& typeMap, const char* vectorType)
: typeMap(typeMap)
TypeMapVisitor(
DenseHashMap<AstExprFunction*, std::string>& functionTypes, DenseHashMap<AstLocal*, LuauBytecodeType>& localTypes, const char* vectorType)
: functionTypes(functionTypes)
, localTypes(localTypes)
, vectorType(vectorType)
, typeAliases(AstName())
{
@ -216,15 +221,34 @@ struct TypeMapVisitor : AstVisitor
std::string type = getFunctionType(node, typeAliases, vectorType);
if (!type.empty())
typeMap[node] = std::move(type);
functionTypes[node] = std::move(type);
return true;
}
bool visit(AstExprLocal* node) override
{
if (FFlag::LuauCompileTypeInfo)
{
AstLocal* local = node->local;
if (AstType* annotation = local->annotation)
{
LuauBytecodeType ty = getType(annotation, {}, typeAliases, /* resolveAliases= */ true, vectorType);
if (ty != LBC_TYPE_ANY)
localTypes[local] = ty;
}
}
return true;
}
};
void buildTypeMap(DenseHashMap<AstExprFunction*, std::string>& typeMap, AstNode* root, const char* vectorType)
void buildTypeMap(DenseHashMap<AstExprFunction*, std::string>& functionTypes, DenseHashMap<AstLocal*, LuauBytecodeType>& localTypes, AstNode* root,
const char* vectorType)
{
TypeMapVisitor visitor(typeMap, vectorType);
TypeMapVisitor visitor(functionTypes, localTypes, vectorType);
root->visit(&visitor);
}

View File

@ -2,6 +2,7 @@
#pragma once
#include "Luau/Ast.h"
#include "Luau/Bytecode.h"
#include "Luau/DenseHash.h"
#include <string>
@ -9,6 +10,7 @@
namespace Luau
{
void buildTypeMap(DenseHashMap<AstExprFunction*, std::string>& typeMap, AstNode* root, const char* vectorType);
void buildTypeMap(DenseHashMap<AstExprFunction*, std::string>& functionTypes, DenseHashMap<AstLocal*, LuauBytecodeType>& localTypes, AstNode* root,
const char* vectorType);
} // namespace Luau

View File

@ -6,6 +6,8 @@
#include "lmem.h"
#include "lgc.h"
LUAU_FASTFLAGVARIABLE(LuauLoadTypeInfo, false)
Proto* luaF_newproto(lua_State* L)
{
Proto* f = luaM_newgco(L, Proto, sizeof(Proto), L->activememcat);
@ -51,6 +53,9 @@ Proto* luaF_newproto(lua_State* L)
f->linedefined = 0;
f->bytecodeid = 0;
if (FFlag::LuauLoadTypeInfo)
f->sizetypeinfo = 0;
return f;
}
@ -173,8 +178,16 @@ void luaF_freeproto(lua_State* L, Proto* f, lua_Page* page)
if (f->execdata)
L->global->ecb.destroy(L, f);
if (f->typeinfo)
luaM_freearray(L, f->typeinfo, f->numparams + 2, uint8_t, f->memcat);
if (FFlag::LuauLoadTypeInfo)
{
if (f->typeinfo)
luaM_freearray(L, f->typeinfo, f->sizetypeinfo, uint8_t, f->memcat);
}
else
{
if (f->typeinfo)
luaM_freearray(L, f->typeinfo, f->numparams + 2, uint8_t, f->memcat);
}
luaM_freegco(L, f, sizeof(Proto), f->memcat, page);
}

View File

@ -14,6 +14,8 @@
#include <string.h>
LUAU_FASTFLAG(LuauLoadTypeInfo)
/*
* Luau uses an incremental non-generational non-moving mark&sweep garbage collector.
*
@ -504,8 +506,17 @@ static size_t propagatemark(global_State* g)
Proto* p = gco2p(o);
g->gray = p->gclist;
traverseproto(g, p);
return sizeof(Proto) + sizeof(Instruction) * p->sizecode + sizeof(Proto*) * p->sizep + sizeof(TValue) * p->sizek + p->sizelineinfo +
sizeof(LocVar) * p->sizelocvars + sizeof(TString*) * p->sizeupvalues;
if (FFlag::LuauLoadTypeInfo)
{
return sizeof(Proto) + sizeof(Instruction) * p->sizecode + sizeof(Proto*) * p->sizep + sizeof(TValue) * p->sizek + p->sizelineinfo +
sizeof(LocVar) * p->sizelocvars + sizeof(TString*) * p->sizeupvalues + p->sizetypeinfo;
}
else
{
return sizeof(Proto) + sizeof(Instruction) * p->sizecode + sizeof(Proto*) * p->sizep + sizeof(TValue) * p->sizek + p->sizelineinfo +
sizeof(LocVar) * p->sizelocvars + sizeof(TString*) * p->sizeupvalues;
}
}
default:
LUAU_ASSERT(0);

View File

@ -335,6 +335,7 @@ typedef struct Proto
int linegaplog2;
int linedefined;
int bytecodeid;
int sizetypeinfo;
} Proto;
// clang-format on

View File

@ -13,6 +13,8 @@
#include <string.h>
LUAU_FASTFLAG(LuauLoadTypeInfo)
// TODO: RAII deallocation doesn't work for longjmp builds if a memory error happens
template<typename T>
struct TempBuffer
@ -258,21 +260,78 @@ int luau_load(lua_State* L, const char* chunkname, const char* data, size_t size
{
p->flags = read<uint8_t>(data, size, offset);
uint32_t typesize = readVarInt(data, size, offset);
if (typesize && typesversion == LBC_TYPE_VERSION)
if (FFlag::LuauLoadTypeInfo)
{
uint8_t* types = (uint8_t*)data + offset;
if (typesversion == 1)
{
uint32_t typesize = readVarInt(data, size, offset);
LUAU_ASSERT(typesize == unsigned(2 + p->numparams));
LUAU_ASSERT(types[0] == LBC_TYPE_FUNCTION);
LUAU_ASSERT(types[1] == p->numparams);
if (typesize)
{
uint8_t* types = (uint8_t*)data + offset;
p->typeinfo = luaM_newarray(L, typesize, uint8_t, p->memcat);
memcpy(p->typeinfo, types, typesize);
LUAU_ASSERT(typesize == unsigned(2 + p->numparams));
LUAU_ASSERT(types[0] == LBC_TYPE_FUNCTION);
LUAU_ASSERT(types[1] == p->numparams);
// transform v1 into v2 format
int headersize = typesize > 127 ? 4 : 3;
p->typeinfo = luaM_newarray(L, headersize + typesize, uint8_t, p->memcat);
p->sizetypeinfo = headersize + typesize;
if (headersize == 4)
{
p->typeinfo[0] = (typesize & 127) | (1 << 7);
p->typeinfo[1] = typesize >> 7;
p->typeinfo[2] = 0;
p->typeinfo[3] = 0;
}
else
{
p->typeinfo[0] = uint8_t(typesize);
p->typeinfo[1] = 0;
p->typeinfo[2] = 0;
}
memcpy(p->typeinfo + headersize, types, typesize);
}
offset += typesize;
}
else if (typesversion == 2)
{
uint32_t typesize = readVarInt(data, size, offset);
if (typesize)
{
uint8_t* types = (uint8_t*)data + offset;
p->typeinfo = luaM_newarray(L, typesize, uint8_t, p->memcat);
p->sizetypeinfo = typesize;
memcpy(p->typeinfo, types, typesize);
offset += typesize;
}
}
}
else
{
uint32_t typesize = readVarInt(data, size, offset);
offset += typesize;
if (typesize && typesversion == LBC_TYPE_VERSION_DEPRECATED)
{
uint8_t* types = (uint8_t*)data + offset;
LUAU_ASSERT(typesize == unsigned(2 + p->numparams));
LUAU_ASSERT(types[0] == LBC_TYPE_FUNCTION);
LUAU_ASSERT(types[1] == p->numparams);
p->typeinfo = luaM_newarray(L, typesize, uint8_t, p->memcat);
memcpy(p->typeinfo, types, typesize);
}
offset += typesize;
}
}
const int sizecode = readVarInt(data, size, offset);
@ -433,6 +492,8 @@ int luau_load(lua_State* L, const char* chunkname, const char* data, size_t size
}
const int sizeupvalues = readVarInt(data, size, offset);
LUAU_ASSERT(sizeupvalues == p->nups);
p->upvalues = luaM_newarray(L, sizeupvalues, TString*, p->memcat);
p->sizeupvalues = sizeupvalues;

View File

@ -71,6 +71,7 @@ static std::string compileTypeTable(const char* source)
Luau::CompileOptions opts;
opts.vectorType = "Vector3";
opts.typeInfoLevel = 1;
Luau::compileOrThrow(bcb, source, opts);
return bcb.dumpTypeInfo();

View File

@ -33,9 +33,6 @@ void luaC_validate(lua_State* L);
LUAU_FASTFLAG(DebugLuauAbortingChecks)
LUAU_FASTINT(CodegenHeuristicsInstructionLimit)
LUAU_FASTFLAG(LuauCompileRepeatUntilSkippedLocals)
LUAU_FASTFLAG(LuauCodegenInferNumTag)
LUAU_FASTFLAG(LuauCodegenDetailedCompilationResult)
LUAU_FASTFLAG(LuauCodegenCheckTruthyFormB)
LUAU_DYNAMIC_FASTFLAG(LuauFastCrossTableMove)
static lua_CompileOptions defaultOptions()
@ -43,6 +40,7 @@ static lua_CompileOptions defaultOptions()
lua_CompileOptions copts = {};
copts.optimizationLevel = optimizationLevel;
copts.debugLevel = 1;
copts.typeInfoLevel = 1;
copts.vectorCtor = "vector";
copts.vectorType = "vector";
@ -240,12 +238,7 @@ static StateRef runConformance(const char* name, void (*setup)(lua_State* L) = n
free(bytecode);
if (result == 0 && codegen && !skipCodegen && luau_codegen_supported())
{
if (FFlag::LuauCodegenDetailedCompilationResult)
Luau::CodeGen::compile(L, -1, Luau::CodeGen::CodeGen_ColdFunctions);
else
Luau::CodeGen::compile_DEPRECATED(L, -1, Luau::CodeGen::CodeGen_ColdFunctions);
}
Luau::CodeGen::compile(L, -1, Luau::CodeGen::CodeGen_ColdFunctions);
int status = (result == 0) ? lua_resume(L, nullptr, 0) : LUA_ERRSYNTAX;
@ -338,6 +331,7 @@ static std::vector<Luau::CodeGen::FunctionBytecodeSummary> analyzeFile(const cha
Luau::CompileOptions options;
options.optimizationLevel = optimizationLevel;
options.debugLevel = 1;
options.typeInfoLevel = 1;
compileOrThrow(bcb, source, options);
@ -2070,9 +2064,6 @@ TEST_CASE("SafeEnv")
TEST_CASE("Native")
{
ScopedFastFlag luauCodegenRemoveDeadStores{FFlag::LuauCodegenInferNumTag, true};
ScopedFastFlag luauCodegenCheckTruthyFormB{FFlag::LuauCodegenCheckTruthyFormB, true};
// This tests requires code to run natively, otherwise all 'is_native' checks will fail
if (!codegen || !luau_codegen_supported())
return;
@ -2130,8 +2121,6 @@ TEST_CASE("NativeTypeAnnotations")
TEST_CASE("HugeFunction")
{
ScopedFastFlag luauCodegenDetailedCompilationResult{FFlag::LuauCodegenDetailedCompilationResult, true};
std::string source = makeHugeFunctionSource();
StateRef globalState(luaL_newstate(), lua_close);
@ -2235,7 +2224,6 @@ TEST_CASE("IrInstructionLimit")
return;
ScopedFastInt codegenHeuristicsInstructionLimit{FInt::CodegenHeuristicsInstructionLimit, 50'000};
ScopedFastFlag luauCodegenDetailedCompilationResult{FFlag::LuauCodegenDetailedCompilationResult, true};
std::string source;

View File

@ -678,4 +678,30 @@ TEST_CASE_FIXTURE(DataFlowGraphFixture, "dfg_function_definition_in_a_do_block")
CHECK(x2 == x3);
}
TEST_CASE_FIXTURE(DataFlowGraphFixture, "dfg_captured_local_is_assigned_a_function")
{
dfg(R"(
local f
local function g()
f()
end
function f()
end
)");
DefId f1 = graph->getDef(query<AstStatLocal>(module)->vars.data[0]);
DefId f2 = getDef<AstExprLocal, 1>();
DefId f3 = getDef<AstExprLocal, 2>();
CHECK(f1 != f2);
CHECK(f2 != f3);
const Phi* f2phi = get<Phi>(f2);
REQUIRE(f2phi);
CHECK(f2phi->operands.size() == 1);
CHECK(f2phi->operands.at(0) == f3);
}
TEST_SUITE_END();

View File

@ -14,7 +14,6 @@
LUAU_FASTFLAG(LuauCodegenRemoveDeadStores5)
LUAU_FASTFLAG(DebugLuauAbortingChecks)
LUAU_FASTFLAG(LuauCodegenInferNumTag)
LUAU_FASTFLAG(LuauCodegenLoadPropCheckRegLinkInTv)
using namespace Luau::CodeGen;
@ -2633,8 +2632,6 @@ bb_0:
TEST_CASE_FIXTURE(IrBuilderFixture, "InferNumberTagFromLimitedContext")
{
ScopedFastFlag luauCodegenRemoveDeadStores{FFlag::LuauCodegenInferNumTag, true};
IrOp entry = build.block(IrBlockKind::Internal);
build.beginBlock(entry);

View File

@ -13,10 +13,13 @@
#include <memory>
LUAU_FASTFLAG(LuauCodegenRemoveDeadStores5)
LUAU_FASTFLAG(LuauCodegenLoadTVTag)
LUAU_FASTFLAG(LuauCodegenDirectUserdataFlow)
LUAU_FASTFLAG(LuauCompileTypeInfo)
LUAU_FASTFLAG(LuauLoadTypeInfo)
LUAU_FASTFLAG(LuauCodegenTypeInfo)
LUAU_FASTFLAG(LuauTypeInfoLookupImprovement)
static std::string getCodegenAssembly(const char* source)
static std::string getCodegenAssembly(const char* source, bool includeIrTypes = false)
{
Luau::CodeGen::AssemblyOptions options;
@ -27,6 +30,7 @@ static std::string getCodegenAssembly(const char* source)
options.includeAssembly = false;
options.includeIr = true;
options.includeOutlinedCode = false;
options.includeIrTypes = includeIrTypes;
options.includeIrPrefix = Luau::CodeGen::IncludeIrPrefix::No;
options.includeUseInfo = Luau::CodeGen::IncludeUseInfo::No;
@ -44,6 +48,7 @@ static std::string getCodegenAssembly(const char* source)
copts.optimizationLevel = 2;
copts.debugLevel = 1;
copts.typeInfoLevel = 1;
copts.vectorCtor = "vector";
copts.vectorType = "vector";
@ -396,7 +401,6 @@ bb_bytecode_0:
TEST_CASE("VectorConstantTag")
{
ScopedFastFlag luauCodegenRemoveDeadStores{FFlag::LuauCodegenRemoveDeadStores5, true};
ScopedFastFlag luauCodegenLoadTVTag{FFlag::LuauCodegenLoadTVTag, true};
CHECK_EQ("\n" + getCodegenAssembly(R"(
local function vecrcp(a: vector)
@ -545,4 +549,315 @@ bb_4:
)");
}
TEST_CASE("ExplicitUpvalueAndLocalTypes")
{
ScopedFastFlag sffs[]{{FFlag::LuauLoadTypeInfo, true}, {FFlag::LuauCompileTypeInfo, true}, {FFlag::LuauCodegenTypeInfo, true},
{FFlag::LuauCodegenRemoveDeadStores5, true}};
CHECK_EQ("\n" + getCodegenAssembly(R"(
local y: vector = ...
local function getsum(t)
local x: vector = t
return x.X + x.Y + y.X + y.Y
end
)",
/* includeIrTypes */ true),
R"(
; function getsum($arg0) line 4
; U0: vector
; R0: vector from 0 to 14
bb_bytecode_0:
CHECK_TAG R0, tvector, exit(0)
%2 = LOAD_FLOAT R0, 0i
STORE_DOUBLE R4, %2
STORE_TAG R4, tnumber
%7 = LOAD_FLOAT R0, 4i
%16 = ADD_NUM %2, %7
STORE_DOUBLE R3, %16
STORE_TAG R3, tnumber
GET_UPVALUE R5, U0
CHECK_TAG R5, tvector, exit(6)
%22 = LOAD_FLOAT R5, 0i
%31 = ADD_NUM %16, %22
STORE_DOUBLE R2, %31
STORE_TAG R2, tnumber
GET_UPVALUE R4, U0
CHECK_TAG R4, tvector, exit(10)
%37 = LOAD_FLOAT R4, 4i
%46 = ADD_NUM %31, %37
STORE_DOUBLE R1, %46
STORE_TAG R1, tnumber
INTERRUPT 13u
RETURN R1, 1i
)");
}
TEST_CASE("FastcallTypeInferThroughLocal")
{
ScopedFastFlag sffs[]{{FFlag::LuauLoadTypeInfo, true}, {FFlag::LuauCompileTypeInfo, true}, {FFlag::LuauCodegenTypeInfo, true},
{FFlag::LuauCodegenRemoveDeadStores5, true}};
CHECK_EQ("\n" + getCodegenAssembly(R"(
local function getsum(x, c)
local v = vector(x, 2, 3)
if c then
return v.X + v.Y
else
return v.Z
end
end
)",
/* includeIrTypes */ true),
R"(
; function getsum($arg0, $arg1) line 2
; R2: vector from 0 to 17
bb_bytecode_0:
%0 = LOAD_TVALUE R0
STORE_TVALUE R3, %0
STORE_DOUBLE R4, 2
STORE_TAG R4, tnumber
STORE_DOUBLE R5, 3
STORE_TAG R5, tnumber
CHECK_SAFE_ENV exit(4)
CHECK_TAG R3, tnumber, exit(4)
%13 = LOAD_DOUBLE R3
STORE_VECTOR R2, %13, 2, 3
STORE_TAG R2, tvector
JUMP_IF_FALSY R1, bb_bytecode_1, bb_3
bb_3:
CHECK_TAG R2, tvector, exit(8)
%21 = LOAD_FLOAT R2, 0i
%26 = LOAD_FLOAT R2, 4i
%35 = ADD_NUM %21, %26
STORE_DOUBLE R3, %35
STORE_TAG R3, tnumber
INTERRUPT 13u
RETURN R3, 1i
bb_bytecode_1:
CHECK_TAG R2, tvector, exit(14)
%42 = LOAD_FLOAT R2, 8i
STORE_DOUBLE R3, %42
STORE_TAG R3, tnumber
INTERRUPT 16u
RETURN R3, 1i
)");
}
TEST_CASE("FastcallTypeInferThroughUpvalue")
{
ScopedFastFlag sffs[]{{FFlag::LuauLoadTypeInfo, true}, {FFlag::LuauCompileTypeInfo, true}, {FFlag::LuauCodegenTypeInfo, true},
{FFlag::LuauCodegenRemoveDeadStores5, true}};
CHECK_EQ("\n" + getCodegenAssembly(R"(
local v = ...
local function getsum(x, c)
v = vector(x, 2, 3)
if c then
return v.X + v.Y
else
return v.Z
end
end
)",
/* includeIrTypes */ true),
R"(
; function getsum($arg0, $arg1) line 4
; U0: vector
bb_bytecode_0:
%0 = LOAD_TVALUE R0
STORE_TVALUE R3, %0
STORE_DOUBLE R4, 2
STORE_TAG R4, tnumber
STORE_DOUBLE R5, 3
STORE_TAG R5, tnumber
CHECK_SAFE_ENV exit(4)
CHECK_TAG R3, tnumber, exit(4)
%13 = LOAD_DOUBLE R3
STORE_VECTOR R2, %13, 2, 3
STORE_TAG R2, tvector
SET_UPVALUE U0, R2, tvector
JUMP_IF_FALSY R1, bb_bytecode_1, bb_3
bb_3:
GET_UPVALUE R4, U0
CHECK_TAG R4, tvector, exit(10)
%23 = LOAD_FLOAT R4, 0i
STORE_DOUBLE R3, %23
STORE_TAG R3, tnumber
GET_UPVALUE R5, U0
CHECK_TAG R5, tvector, exit(13)
%29 = LOAD_FLOAT R5, 4i
%38 = ADD_NUM %23, %29
STORE_DOUBLE R2, %38
STORE_TAG R2, tnumber
INTERRUPT 16u
RETURN R2, 1i
bb_bytecode_1:
GET_UPVALUE R3, U0
CHECK_TAG R3, tvector, exit(18)
%46 = LOAD_FLOAT R3, 8i
STORE_DOUBLE R2, %46
STORE_TAG R2, tnumber
INTERRUPT 20u
RETURN R2, 1i
)");
}
TEST_CASE("LoadAndMoveTypePropagation")
{
ScopedFastFlag sffs[]{{FFlag::LuauLoadTypeInfo, true}, {FFlag::LuauCompileTypeInfo, true}, {FFlag::LuauCodegenTypeInfo, true},
{FFlag::LuauCodegenRemoveDeadStores5, true}, {FFlag::LuauTypeInfoLookupImprovement, true}};
CHECK_EQ("\n" + getCodegenAssembly(R"(
local function getsum(n)
local seqsum = 0
for i = 1,n do
if i < 10 then
seqsum += i
else
seqsum *= i
end
end
return seqsum
end
)",
/* includeIrTypes */ true),
R"(
; function getsum($arg0) line 2
; R1: number from 0 to 13
; R4: number from 1 to 11
bb_bytecode_0:
STORE_DOUBLE R1, 0
STORE_TAG R1, tnumber
STORE_DOUBLE R4, 1
STORE_TAG R4, tnumber
%4 = LOAD_TVALUE R0
STORE_TVALUE R2, %4
STORE_DOUBLE R3, 1
STORE_TAG R3, tnumber
CHECK_TAG R2, tnumber, exit(4)
%12 = LOAD_DOUBLE R2
JUMP_CMP_NUM 1, %12, not_le, bb_bytecode_4, bb_bytecode_1
bb_bytecode_1:
INTERRUPT 5u
STORE_DOUBLE R5, 10
STORE_TAG R5, tnumber
CHECK_TAG R4, tnumber, bb_fallback_6
JUMP_CMP_NUM R4, 10, not_lt, bb_bytecode_2, bb_5
bb_5:
CHECK_TAG R1, tnumber, exit(8)
CHECK_TAG R4, tnumber, exit(8)
%32 = LOAD_DOUBLE R1
%34 = ADD_NUM %32, R4
STORE_DOUBLE R1, %34
JUMP bb_bytecode_3
bb_bytecode_2:
CHECK_TAG R1, tnumber, exit(10)
CHECK_TAG R4, tnumber, exit(10)
%41 = LOAD_DOUBLE R1
%43 = MUL_NUM %41, R4
STORE_DOUBLE R1, %43
JUMP bb_bytecode_3
bb_bytecode_3:
%46 = LOAD_DOUBLE R2
%47 = LOAD_DOUBLE R4
%48 = ADD_NUM %47, 1
STORE_DOUBLE R4, %48
JUMP_CMP_NUM %48, %46, le, bb_bytecode_1, bb_bytecode_4
bb_bytecode_4:
INTERRUPT 12u
RETURN R1, 1i
)");
}
TEST_CASE("ArgumentTypeRefinement")
{
ScopedFastFlag sffs[]{{FFlag::LuauLoadTypeInfo, true}, {FFlag::LuauCompileTypeInfo, true}, {FFlag::LuauCodegenTypeInfo, true},
{FFlag::LuauCodegenRemoveDeadStores5, true}, {FFlag::LuauTypeInfoLookupImprovement, true}};
CHECK_EQ("\n" + getCodegenAssembly(R"(
local function getsum(x, y)
x = vector(1, y, 3)
return x.Y + x.Z
end
)",
/* includeIrTypes */ true),
R"(
; function getsum($arg0, $arg1) line 2
; R0: vector [argument]
bb_bytecode_0:
STORE_DOUBLE R3, 1
STORE_TAG R3, tnumber
%2 = LOAD_TVALUE R1
STORE_TVALUE R4, %2
STORE_DOUBLE R5, 3
STORE_TAG R5, tnumber
CHECK_SAFE_ENV exit(4)
CHECK_TAG R4, tnumber, exit(4)
%14 = LOAD_DOUBLE R4
STORE_VECTOR R2, 1, %14, 3
STORE_TAG R2, tvector
%18 = LOAD_TVALUE R2
STORE_TVALUE R0, %18
%22 = LOAD_FLOAT R0, 4i
%27 = LOAD_FLOAT R0, 8i
%36 = ADD_NUM %22, %27
STORE_DOUBLE R2, %36
STORE_TAG R2, tnumber
INTERRUPT 13u
RETURN R2, 1i
)");
}
TEST_CASE("InlineFunctionType")
{
ScopedFastFlag sffs[]{{FFlag::LuauLoadTypeInfo, true}, {FFlag::LuauCompileTypeInfo, true}, {FFlag::LuauCodegenTypeInfo, true},
{FFlag::LuauCodegenRemoveDeadStores5, true}, {FFlag::LuauTypeInfoLookupImprovement, true}};
CHECK_EQ("\n" + getCodegenAssembly(R"(
local function inl(v: vector, s: number)
return v.Y * s
end
local function getsum(x)
return inl(x, 2) + inl(x, 5)
end
)",
/* includeIrTypes */ true),
R"(
; function inl($arg0, $arg1) line 2
; R0: vector [argument]
; R1: number [argument]
bb_0:
CHECK_TAG R0, tvector, exit(entry)
CHECK_TAG R1, tnumber, exit(entry)
JUMP bb_2
bb_2:
JUMP bb_bytecode_1
bb_bytecode_1:
%8 = LOAD_FLOAT R0, 4i
%17 = MUL_NUM %8, R1
STORE_DOUBLE R2, %17
STORE_TAG R2, tnumber
INTERRUPT 3u
RETURN R2, 1i
; function getsum($arg0) line 6
; R0: vector from 0 to 3
; R0: vector from 3 to 6
bb_bytecode_0:
CHECK_TAG R0, tvector, exit(0)
%2 = LOAD_FLOAT R0, 4i
%8 = MUL_NUM %2, 2
%13 = LOAD_FLOAT R0, 4i
%19 = MUL_NUM %13, 5
%28 = ADD_NUM %8, %19
STORE_DOUBLE R1, %28
STORE_TAG R1, tnumber
INTERRUPT 7u
RETURN R1, 1i
)");
}
TEST_SUITE_END();

View File

@ -63,6 +63,7 @@ struct NonStrictTypeCheckerFixture : Fixture
NonStrictTypeCheckerFixture()
{
registerHiddenTypes(&frontend);
registerTestTypes();
}
CheckResult checkNonStrict(const std::string& code)
@ -76,6 +77,17 @@ struct NonStrictTypeCheckerFixture : Fixture
return check(Mode::Nonstrict, code);
}
CheckResult checkNonStrictModule(const std::string& moduleName)
{
ScopedFastFlag flags[] = {
{FFlag::LuauCheckedFunctionSyntax, true},
{FFlag::DebugLuauDeferredConstraintResolution, true},
};
LoadDefinitionFileResult res = loadDefinition(definitions);
LUAU_ASSERT(res.success);
return frontend.check(moduleName);
}
std::string definitions = R"BUILTIN_SRC(
declare function @checked abs(n: number): number
declare function @checked lower(s: string): string
@ -106,6 +118,8 @@ type DateTypeArg = {
declare os : {
time: @checked (time: DateTypeArg?) -> number
}
declare function @checked require(target : any) : any
)BUILTIN_SRC";
};
@ -527,4 +541,23 @@ os.time({year = 0, month = 0, day = 0, min = 0, isdst = nil})
Luau::InternalCompilerError);
}
TEST_CASE_FIXTURE(NonStrictTypeCheckerFixture, "non_strict_shouldnt_warn_on_require_module")
{
fileResolver.source["Modules/A"] = R"(
--!strict
type t = {x : number}
local e : t = {x = 3}
return e
)";
fileResolver.sourceTypes["Modules/A"] = SourceCode::Module;
fileResolver.source["Modules/B"] = R"(
--!nonstrict
local E = require(script.Parent.A)
)";
CheckResult result = checkNonStrictModule("Modules/B");
LUAU_REQUIRE_NO_ERRORS(result);
}
TEST_SUITE_END();

View File

@ -16,7 +16,6 @@
#endif
LUAU_FASTFLAG(LuauCodegenContext)
LUAU_FASTFLAG(LuauCodegenDetailedCompilationResult)
using namespace Luau::CodeGen;
@ -34,7 +33,6 @@ TEST_CASE("NativeModuleRefRefcounting")
return;
ScopedFastFlag luauCodegenContext{FFlag::LuauCodegenContext, true};
ScopedFastFlag luauCodegenDetailedCompilationResult{FFlag::LuauCodegenDetailedCompilationResult, true};
CodeAllocator codeAllocator{kBlockSize, kMaxTotalSize};
SharedCodeAllocator allocator{&codeAllocator};
@ -253,7 +251,6 @@ TEST_CASE("NativeProtoRefcounting")
return;
ScopedFastFlag luauCodegenContext{FFlag::LuauCodegenContext, true};
ScopedFastFlag luauCodegenDetailedCompilationResult{FFlag::LuauCodegenDetailedCompilationResult, true};
CodeAllocator codeAllocator{kBlockSize, kMaxTotalSize};
SharedCodeAllocator allocator{&codeAllocator};
@ -307,7 +304,6 @@ TEST_CASE("NativeProtoState")
return;
ScopedFastFlag luauCodegenContext{FFlag::LuauCodegenContext, true};
ScopedFastFlag luauCodegenDetailedCompilationResult{FFlag::LuauCodegenDetailedCompilationResult, true};
CodeAllocator codeAllocator{kBlockSize, kMaxTotalSize};
SharedCodeAllocator allocator{&codeAllocator};
@ -369,7 +365,6 @@ TEST_CASE("AnonymousModuleLifetime")
return;
ScopedFastFlag luauCodegenContext{FFlag::LuauCodegenContext, true};
ScopedFastFlag luauCodegenDetailedCompilationResult{FFlag::LuauCodegenDetailedCompilationResult, true};
CodeAllocator codeAllocator{kBlockSize, kMaxTotalSize};
SharedCodeAllocator allocator{&codeAllocator};
@ -419,7 +414,6 @@ TEST_CASE("SharedAllocation")
return;
ScopedFastFlag luauCodegenContext{FFlag::LuauCodegenContext, true};
ScopedFastFlag luauCodegenDetailedCompilationResult{FFlag::LuauCodegenDetailedCompilationResult, true};
UniqueSharedCodeGenContext sharedCodeGenContext = createSharedCodeGenContext();

View File

@ -14,7 +14,6 @@ LUAU_FASTFLAG(LuauRecursiveTypeParameterRestriction);
LUAU_FASTFLAG(DebugLuauDeferredConstraintResolution);
LUAU_FASTFLAG(LuauCheckedFunctionSyntax);
LUAU_FASTFLAG(DebugLuauSharedSelf);
LUAU_FASTFLAG(LuauStringifyCyclesRootedAtPacks);
TEST_SUITE_BEGIN("ToString");
@ -239,7 +238,6 @@ TEST_CASE_FIXTURE(Fixture, "functions_are_always_parenthesized_in_unions_or_inte
TEST_CASE_FIXTURE(Fixture, "simple_intersections_printed_on_one_line")
{
ScopedFastFlag sff{FFlag::LuauToStringSimpleCompositeTypesSingleLine, true};
CheckResult result = check(R"(
local a: string & number
)");
@ -252,7 +250,6 @@ TEST_CASE_FIXTURE(Fixture, "simple_intersections_printed_on_one_line")
TEST_CASE_FIXTURE(Fixture, "complex_intersections_printed_on_multiple_lines")
{
ScopedFastFlag sff{FFlag::LuauToStringSimpleCompositeTypesSingleLine, true};
CheckResult result = check(R"(
local a: string & number & boolean
)");
@ -271,7 +268,6 @@ TEST_CASE_FIXTURE(Fixture, "complex_intersections_printed_on_multiple_lines")
TEST_CASE_FIXTURE(Fixture, "overloaded_functions_always_printed_on_multiple_lines")
{
ScopedFastFlag sff{FFlag::LuauToStringSimpleCompositeTypesSingleLine, true};
CheckResult result = check(R"(
local a: ((string) -> string) & ((number) -> number)
)");
@ -288,7 +284,6 @@ TEST_CASE_FIXTURE(Fixture, "overloaded_functions_always_printed_on_multiple_line
TEST_CASE_FIXTURE(Fixture, "simple_unions_printed_on_one_line")
{
ScopedFastFlag sff{FFlag::LuauToStringSimpleCompositeTypesSingleLine, true};
CheckResult result = check(R"(
local a: number | boolean
)");
@ -301,7 +296,6 @@ TEST_CASE_FIXTURE(Fixture, "simple_unions_printed_on_one_line")
TEST_CASE_FIXTURE(Fixture, "complex_unions_printed_on_multiple_lines")
{
ScopedFastFlag sff{FFlag::LuauToStringSimpleCompositeTypesSingleLine, true};
CheckResult result = check(R"(
local a: string | number | boolean
)");
@ -1006,8 +1000,6 @@ TEST_CASE_FIXTURE(Fixture, "read_only_properties")
TEST_CASE_FIXTURE(Fixture, "cycle_rooted_in_a_pack")
{
ScopedFastFlag sff{FFlag::LuauStringifyCyclesRootedAtPacks, true};
TypeArena arena;
TypePackId thePack = arena.addTypePack({builtinTypes->numberType, builtinTypes->numberType});

View File

@ -2369,7 +2369,7 @@ end
auto err = get<ExplicitFunctionAnnotationRecommended>(result.errors.back());
LUAU_ASSERT(err);
CHECK("number" == toString(err->recommendedReturn));
CHECK(err->recommendedArgs.size() == 2);
REQUIRE(err->recommendedArgs.size() == 2);
CHECK("number" == toString(err->recommendedArgs[0].second));
CHECK("number" == toString(err->recommendedArgs[1].second));
}
@ -2597,4 +2597,80 @@ We are unable to determine the appropriate result type for such a call.)";
CHECK("Cannot call non-function (() -> () -> ()) | (() -> ())" == toString(result.errors[0]));
}
TEST_CASE_FIXTURE(Fixture, "fuzzer_missing_follow_in_ast_stat_fun")
{
(void)check(R"(
local _ = function<t0...>()
end ~= _
while (_) do
_,_,_,_,_,_,_,_,_,_._,_ = nil
function _(...):<t0...>()->()
end
function _<t0...>(...):any
_ ..= ...
end
_,_,_,_,_,_,_,_,_,_,_ = nil
end
)");
}
TEST_CASE_FIXTURE(Fixture, "unifier_should_not_bind_free_types")
{
CheckResult result = check(R"(
function foo(player)
local success,result = player:thing()
if(success) then
return "Successfully posted message.";
elseif(not result) then
return false;
else
return result;
end
end
)");
if (FFlag::DebugLuauDeferredConstraintResolution)
{
// The new solver should ideally be able to do better here, but this is no worse than the old solver.
LUAU_REQUIRE_ERROR_COUNT(2, result);
auto tm1 = get<TypePackMismatch>(result.errors[0]);
REQUIRE(tm1);
CHECK(toString(tm1->wantedTp) == "string");
CHECK(toString(tm1->givenTp) == "boolean");
auto tm2 = get<TypePackMismatch>(result.errors[1]);
REQUIRE(tm2);
CHECK(toString(tm2->wantedTp) == "string");
CHECK(toString(tm2->givenTp) == "~(false?)");
}
else
{
LUAU_REQUIRE_ERROR_COUNT(1, result);
const TypeMismatch* tm = get<TypeMismatch>(result.errors[0]);
REQUIRE(tm);
CHECK(toString(tm->wantedType) == "string");
CHECK(toString(tm->givenType) == "boolean");
}
}
TEST_CASE_FIXTURE(Fixture, "captured_local_is_assigned_a_function")
{
CheckResult result = check(R"(
local f
local function g()
f()
end
function f()
end
)");
LUAU_REQUIRE_NO_ERRORS(result);
}
TEST_SUITE_END();

View File

@ -1076,4 +1076,42 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "iter_mm_results_are_lvalue")
LUAU_REQUIRE_NO_ERRORS(result);
}
TEST_CASE_FIXTURE(BuiltinsFixture, "forin_metatable_no_iter_mm")
{
ScopedFastFlag sff{FFlag::DebugLuauDeferredConstraintResolution, true};
CheckResult result = check(R"(
local t = setmetatable({1, 2, 3}, {})
for i, v in t do
print(i, v)
end
)");
LUAU_REQUIRE_NO_ERRORS(result);
CHECK_EQ("number", toString(requireTypeAtPosition({4, 18})));
CHECK_EQ("number", toString(requireTypeAtPosition({4, 21})));
}
TEST_CASE_FIXTURE(BuiltinsFixture, "forin_metatable_iter_mm")
{
ScopedFastFlag sff{FFlag::DebugLuauDeferredConstraintResolution, true};
CheckResult result = check(R"(
type Iterable<T...> = typeof(setmetatable({}, {} :: {
__iter: (Iterable<T...>) -> () -> T...
}))
for i, v in {} :: Iterable<...number> do
print(i, v)
end
)");
LUAU_REQUIRE_NO_ERRORS(result);
CHECK_EQ("number", toString(requireTypeAtPosition({6, 18})));
CHECK_EQ("number", toString(requireTypeAtPosition({6, 21})));
}
TEST_SUITE_END();

View File

@ -4196,6 +4196,9 @@ TEST_CASE_FIXTURE(Fixture, "read_ond_write_only_indexers_are_unsupported")
TEST_CASE_FIXTURE(Fixture, "table_writes_introduce_write_properties")
{
if (!FFlag::DebugLuauDeferredConstraintResolution)
return;
ScopedFastFlag sff[] = {{FFlag::LuauReadWritePropertySyntax, true}, {FFlag::DebugLuauDeferredConstraintResolution, true}};
CheckResult result = check(R"(
@ -4389,6 +4392,22 @@ return {}
LUAU_REQUIRE_NO_ERRORS(result);
}
TEST_CASE_FIXTURE(Fixture, "setprop_on_a_mutating_local_in_both_loops_and_functions")
{
CheckResult result = check(R"(
local _ = 5
while (_) do
_._ = nil
function _()
_ = nil
end
end
)");
LUAU_REQUIRE_ERRORS(result);
}
TEST_CASE_FIXTURE(Fixture, "setindexer_multiple_tables_intersection")
{
ScopedFastFlag sff{FFlag::DebugLuauDeferredConstraintResolution, true};

View File

@ -1518,4 +1518,41 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "be_sure_to_use_active_txnlog_when_evaluating
CHECK(5 == e.location.begin.line);
}
/*
* We had an issue where this kind of typeof() call could produce the untestable type ~{}
*/
TEST_CASE_FIXTURE(Fixture, "typeof_cannot_refine_builtin_alias")
{
GlobalTypes& globals = frontend.globals;
TypeArena& arena = globals.globalTypes;
unfreeze(arena);
globals.globalScope->exportedTypeBindings["GlobalTable"] = TypeFun{{}, arena.addType(TableType{TableState::Sealed, TypeLevel{}})};
freeze(arena);
(void) check(R"(
function foo(x)
if typeof(x) == 'GlobalTable' then
end
end
)");
}
/*
* We had an issue where we tripped the canMutate() check when binding one
* blocked type to another.
*/
TEST_CASE_FIXTURE(Fixture, "delay_setIndexer_constraint_if_the_indexers_type_is_blocked")
{
(void) check(R"(
local SG = GetService(true)
local lines: { [string]: typeof(SG.ScreenGui) } = {}
lines[deadline] = nil -- This line
)");
// As long as type inference doesn't trip an assert or crash, we're good!
}
TEST_SUITE_END();

View File

@ -76,13 +76,13 @@ TEST_CASE_FIXTURE(Unifier2Fixture, "T <: U")
CHECK(u2.unify(left, right));
CHECK("'a" == toString(left));
CHECK("'a" == toString(right));
CHECK("t1 where t1 = ('a <: (t1 <: 'b))" == toString(left));
CHECK("t1 where t1 = (('a <: t1) <: 'b)" == toString(right));
CHECK("never" == toString(freeLeft->lowerBound));
CHECK("unknown" == toString(freeLeft->upperBound));
CHECK("t1 where t1 = (('a <: t1) <: 'b)" == toString(freeLeft->upperBound));
CHECK("never" == toString(freeRight->lowerBound));
CHECK("t1 where t1 = ('a <: (t1 <: 'b))" == toString(freeRight->lowerBound));
CHECK("unknown" == toString(freeRight->upperBound));
}

View File

@ -66,7 +66,6 @@ GenericsTests.higher_rank_polymorphism_should_not_accept_instantiated_arguments
GenericsTests.infer_generic_function_function_argument
GenericsTests.infer_generic_function_function_argument_2
GenericsTests.infer_generic_function_function_argument_3
GenericsTests.infer_generic_function_function_argument_overloaded
GenericsTests.instantiated_function_argument_names
GenericsTests.no_stack_overflow_from_quantifying
GenericsTests.properties_can_be_instantiated_polytypes
@ -99,6 +98,7 @@ IntersectionTypes.table_write_sealed_indirect
IntersectionTypes.union_saturate_overloaded_functions
Linter.TableOperationsIndexer
ModuleTests.clone_self_property
Negations.cofinite_strings_can_be_compared_for_equality
NonstrictModeTests.inconsistent_module_return_types_are_ok
NonstrictModeTests.infer_nullary_function
NonstrictModeTests.infer_the_maximum_number_of_values_the_function_could_return
@ -178,6 +178,7 @@ TableTests.generalize_table_argument
TableTests.generic_table_instantiation_potential_regression
TableTests.indexer_on_sealed_table_must_unify_with_free_table
TableTests.indexers_get_quantified_too
TableTests.infer_array
TableTests.infer_indexer_from_array_like_table
TableTests.infer_indexer_from_its_variable_type_and_unifiable
TableTests.inferred_return_type_of_free_table
@ -222,6 +223,7 @@ TableTests.table_subtyping_with_extra_props_dont_report_multiple_errors
TableTests.table_subtyping_with_missing_props_dont_report_multiple_errors2
TableTests.table_unification_4
TableTests.table_unifies_into_map
TableTests.table_writes_introduce_write_properties
TableTests.type_mismatch_on_massive_table_is_cut_short
TableTests.used_colon_instead_of_dot
TableTests.used_dot_instead_of_colon
@ -265,7 +267,6 @@ TypeInfer.cli_50041_committing_txnlog_in_apollo_client_error
TypeInfer.dont_ice_when_failing_the_occurs_check
TypeInfer.dont_report_type_errors_within_an_AstExprError
TypeInfer.dont_report_type_errors_within_an_AstStatError
TypeInfer.follow_on_new_types_in_substitution
TypeInfer.globals
TypeInfer.globals2
TypeInfer.globals_are_banned_in_strict_mode
@ -311,6 +312,7 @@ TypeInferFunctions.function_does_not_return_enough_values
TypeInferFunctions.function_exprs_are_generalized_at_signature_scope_not_enclosing
TypeInferFunctions.function_is_supertype_of_concrete_functions
TypeInferFunctions.function_statement_sealed_table_assignment_through_indexer
TypeInferFunctions.fuzzer_missing_follow_in_ast_stat_fun
TypeInferFunctions.generic_packs_are_not_variadic
TypeInferFunctions.higher_order_function_2
TypeInferFunctions.higher_order_function_3
@ -341,7 +343,6 @@ TypeInferFunctions.too_many_arguments_error_location
TypeInferFunctions.too_many_return_values_in_parentheses
TypeInferFunctions.too_many_return_values_no_function
TypeInferLoops.cli_68448_iterators_need_not_accept_nil
TypeInferLoops.dcr_iteration_fragmented_keys
TypeInferLoops.dcr_iteration_on_never_gives_never
TypeInferLoops.dcr_xpath_candidates
TypeInferLoops.for_in_loop
@ -392,8 +393,6 @@ TypeInferOperators.UnknownGlobalCompoundAssign
TypeInferPrimitives.CheckMethodsOfNumber
TypeInferPrimitives.string_index
TypeInferUnknownNever.assign_to_local_which_is_never
TypeInferUnknownNever.compare_never
TypeInferUnknownNever.dont_unify_operands_if_one_of_the_operand_is_never_in_any_ordering_operators
TypeInferUnknownNever.index_on_union_of_tables_for_properties_that_is_never
TypeInferUnknownNever.index_on_union_of_tables_for_properties_that_is_sorta_never
TypeInferUnknownNever.length_of_never

View File

@ -228,21 +228,7 @@ class DenseHashMapSyntheticChildrenProvider:
skipped += 1
continue
slot_key = slot_key_valobj.GetSummary()
if slot_key is None:
slot_key = slot_key_valobj.GetValue()
if slot_key is None:
slot_key = slot_key_valobj.GetValueAsSigned()
if slot_key is None:
slot_key = slot_key_valobj.GetValueAsUnsigned()
if slot_key is None:
slot_key = str(index)
slot_value_valobj = slot_pair.GetChildMemberWithName("second")
return self.valobj.CreateValueFromData(f"[{slot_key}]", slot_value_valobj.GetData(), slot_value_valobj.GetType())
return self.valobj.CreateValueFromData(f"[{index}]", slot_pair.GetData(), slot_pair.GetType())
except Exception as e:
print("get_child_at_index error", e, index)