Sync to upstream/release/581 (#958)

* Definition files can now ascribe indexers to class types.
(https://github.com/Roblox/luau/pull/949)
* Remove --compile support from the REPL. You can just use luau-compile
instead.
* When an exception is thrown during parallel typechecking (usually an
ICE), we now gracefully stop typechecking and drain active workers
before rethrowing the exception.

New solver

* Include more source location information when we hit an internal
compiler error
* Improve the logic that simplifies intersections of tables

JIT

* Save testable type annotations to bytecode
* Improve block placement for linearized blocks
* Add support for lea reg, [rip+offset] for labels
* Unify X64 and A64 codegen for RETURN
* Outline interrupt handlers for X64
* Remove global rArgN in favor of build.abi
* Change A64 INTERRUPT lowering to match X64

---------

Co-authored-by: Arseny Kapoulkine <arseny.kapoulkine@gmail.com>
Co-authored-by: Vyacheslav Egorov <vegorov@roblox.com>
This commit is contained in:
Andy Friesen 2023-06-16 10:35:18 -07:00 committed by GitHub
parent bc0722471f
commit d458d240cd
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
42 changed files with 969 additions and 613 deletions

View File

@ -174,10 +174,10 @@ struct ConstraintSolver
bool blockOnPendingTypes(TypePackId target, NotNull<const Constraint> constraint);
void unblock(NotNull<const Constraint> progressed);
void unblock(TypeId progressed);
void unblock(TypePackId progressed);
void unblock(const std::vector<TypeId>& types);
void unblock(const std::vector<TypePackId>& packs);
void unblock(TypeId progressed, Location location);
void unblock(TypePackId progressed, Location location);
void unblock(const std::vector<TypeId>& types, Location location);
void unblock(const std::vector<TypePackId>& packs, Location location);
/**
* @returns true if the TypeId is in a blocked state.

View File

@ -539,8 +539,8 @@ bool ConstraintSolver::tryDispatch(const GeneralizationConstraint& c, NotNull<co
asMutable(c.generalizedType)->ty.emplace<BoundType>(builtinTypes->errorRecoveryType());
}
unblock(c.generalizedType);
unblock(c.sourceType);
unblock(c.generalizedType, constraint->location);
unblock(c.sourceType, constraint->location);
return true;
}
@ -564,7 +564,7 @@ bool ConstraintSolver::tryDispatch(const InstantiationConstraint& c, NotNull<con
reportError(UnificationTooComplex{}, constraint->location);
asMutable(c.subType)->ty.emplace<BoundType>(errorRecoveryType());
unblock(c.subType);
unblock(c.subType, constraint->location);
return true;
}
@ -574,7 +574,7 @@ bool ConstraintSolver::tryDispatch(const InstantiationConstraint& c, NotNull<con
InstantiationQueuer queuer{constraint->scope, constraint->location, this};
queuer.traverse(c.subType);
unblock(c.subType);
unblock(c.subType, constraint->location);
return true;
}
@ -597,7 +597,7 @@ bool ConstraintSolver::tryDispatch(const UnaryConstraint& c, NotNull<const Const
{
asMutable(c.resultType)->ty.emplace<BoundType>(builtinTypes->booleanType);
unblock(c.resultType);
unblock(c.resultType, constraint->location);
return true;
}
case AstExprUnary::Len:
@ -605,7 +605,7 @@ bool ConstraintSolver::tryDispatch(const UnaryConstraint& c, NotNull<const Const
// __len must return a number.
asMutable(c.resultType)->ty.emplace<BoundType>(builtinTypes->numberType);
unblock(c.resultType);
unblock(c.resultType, constraint->location);
return true;
}
case AstExprUnary::Minus:
@ -635,7 +635,7 @@ bool ConstraintSolver::tryDispatch(const UnaryConstraint& c, NotNull<const Const
asMutable(c.resultType)->ty.emplace<BoundType>(builtinTypes->errorRecoveryType());
}
unblock(c.resultType);
unblock(c.resultType, constraint->location);
return true;
}
}
@ -684,7 +684,7 @@ bool ConstraintSolver::tryDispatch(const BinaryConstraint& c, NotNull<const Cons
if (isBlocked(leftType) || (hasTypeInIntersection<FreeType>(leftType) && !isLogical))
{
asMutable(resultType)->ty.emplace<BoundType>(errorRecoveryType());
unblock(resultType);
unblock(resultType, constraint->location);
return true;
}
@ -697,7 +697,7 @@ bool ConstraintSolver::tryDispatch(const BinaryConstraint& c, NotNull<const Cons
{
// TODO: Boolean singleton false? The result is _always_ boolean false.
asMutable(resultType)->ty.emplace<BoundType>(builtinTypes->booleanType);
unblock(resultType);
unblock(resultType, constraint->location);
return true;
}
@ -760,7 +760,7 @@ bool ConstraintSolver::tryDispatch(const BinaryConstraint& c, NotNull<const Cons
}
asMutable(resultType)->ty.emplace<BoundType>(mmResult);
unblock(resultType);
unblock(resultType, constraint->location);
(*c.astOriginalCallTypes)[c.astFragment] = *mm;
(*c.astOverloadResolvedTypes)[c.astFragment] = *instantiatedMm;
@ -790,14 +790,14 @@ bool ConstraintSolver::tryDispatch(const BinaryConstraint& c, NotNull<const Cons
{
unify(leftType, rightType, constraint->scope);
asMutable(resultType)->ty.emplace<BoundType>(anyPresent ? builtinTypes->anyType : leftType);
unblock(resultType);
unblock(resultType, constraint->location);
return true;
}
else if (get<NeverType>(leftType) || get<NeverType>(rightType))
{
unify(leftType, rightType, constraint->scope);
asMutable(resultType)->ty.emplace<BoundType>(builtinTypes->neverType);
unblock(resultType);
unblock(resultType, constraint->location);
return true;
}
@ -814,14 +814,14 @@ bool ConstraintSolver::tryDispatch(const BinaryConstraint& c, NotNull<const Cons
{
unify(leftType, rightType, constraint->scope);
asMutable(resultType)->ty.emplace<BoundType>(anyPresent ? builtinTypes->anyType : leftType);
unblock(resultType);
unblock(resultType, constraint->location);
return true;
}
else if (get<NeverType>(leftType) || get<NeverType>(rightType))
{
unify(leftType, rightType, constraint->scope);
asMutable(resultType)->ty.emplace<BoundType>(builtinTypes->neverType);
unblock(resultType);
unblock(resultType, constraint->location);
return true;
}
@ -840,14 +840,14 @@ bool ConstraintSolver::tryDispatch(const BinaryConstraint& c, NotNull<const Cons
if (lt && rt && (lt->isExactlyNumber() || get<AnyType>(lt->tops)) && rt->isExactlyNumber())
{
asMutable(resultType)->ty.emplace<BoundType>(builtinTypes->booleanType);
unblock(resultType);
unblock(resultType, constraint->location);
return true;
}
if (lt && rt && (lt->isSubtypeOfString() || get<AnyType>(lt->tops)) && rt->isSubtypeOfString())
{
asMutable(resultType)->ty.emplace<BoundType>(builtinTypes->booleanType);
unblock(resultType);
unblock(resultType, constraint->location);
return true;
}
@ -855,7 +855,7 @@ bool ConstraintSolver::tryDispatch(const BinaryConstraint& c, NotNull<const Cons
if (get<NeverType>(leftType) || get<NeverType>(rightType))
{
asMutable(resultType)->ty.emplace<BoundType>(builtinTypes->booleanType);
unblock(resultType);
unblock(resultType, constraint->location);
return true;
}
@ -867,7 +867,7 @@ bool ConstraintSolver::tryDispatch(const BinaryConstraint& c, NotNull<const Cons
case AstExprBinary::Op::CompareEq:
case AstExprBinary::Op::CompareNe:
asMutable(resultType)->ty.emplace<BoundType>(builtinTypes->booleanType);
unblock(resultType);
unblock(resultType, constraint->location);
return true;
// And evalutes to a boolean if the LHS is falsey, and the RHS type if LHS is
// truthy.
@ -876,7 +876,7 @@ bool ConstraintSolver::tryDispatch(const BinaryConstraint& c, NotNull<const Cons
TypeId leftFilteredTy = simplifyIntersection(builtinTypes, arena, leftType, builtinTypes->falsyType).result;
asMutable(resultType)->ty.emplace<BoundType>(simplifyUnion(builtinTypes, arena, rightType, leftFilteredTy).result);
unblock(resultType);
unblock(resultType, constraint->location);
return true;
}
// Or evaluates to the LHS type if the LHS is truthy, and the RHS type if
@ -886,7 +886,7 @@ bool ConstraintSolver::tryDispatch(const BinaryConstraint& c, NotNull<const Cons
TypeId leftFilteredTy = simplifyIntersection(builtinTypes, arena, leftType, builtinTypes->truthyType).result;
asMutable(resultType)->ty.emplace<BoundType>(simplifyUnion(builtinTypes, arena, rightType, leftFilteredTy).result);
unblock(resultType);
unblock(resultType, constraint->location);
return true;
}
default:
@ -898,7 +898,7 @@ bool ConstraintSolver::tryDispatch(const BinaryConstraint& c, NotNull<const Cons
unify(leftType, errorRecoveryType(), constraint->scope);
unify(rightType, errorRecoveryType(), constraint->scope);
asMutable(resultType)->ty.emplace<BoundType>(errorRecoveryType());
unblock(resultType);
unblock(resultType, constraint->location);
return true;
}
@ -1065,14 +1065,14 @@ bool ConstraintSolver::tryDispatch(const TypeAliasExpansionConstraint& c, NotNul
const PendingExpansionType* petv = get<PendingExpansionType>(follow(c.target));
if (!petv)
{
unblock(c.target);
unblock(c.target, constraint->location);
return true;
}
auto bindResult = [this, &c](TypeId result) {
auto bindResult = [this, &c, constraint](TypeId result) {
LUAU_ASSERT(get<PendingExpansionType>(c.target));
asMutable(c.target)->ty.emplace<BoundType>(result);
unblock(c.target);
unblock(c.target, constraint->location);
};
std::optional<TypeFun> tf = (petv->prefix) ? constraint->scope->lookupImportedType(petv->prefix->value, petv->name.value)
@ -1400,9 +1400,9 @@ bool ConstraintSolver::tryDispatch(const FunctionCallConstraint& c, NotNull<cons
const auto [changedTypes, changedPacks] = bestOverloadLog->getChanges();
bestOverloadLog->commit();
unblock(changedTypes);
unblock(changedPacks);
unblock(c.result);
unblock(changedTypes, constraint->location);
unblock(changedPacks, constraint->location);
unblock(c.result, constraint->location);
InstantiationQueuer queuer{constraint->scope, constraint->location, this};
queuer.traverse(fn);
@ -1421,7 +1421,7 @@ bool ConstraintSolver::tryDispatch(const PrimitiveTypeConstraint& c, NotNull<con
TypeId bindTo = maybeSingleton(expectedType) ? c.singletonType : c.multitonType;
asMutable(c.resultType)->ty.emplace<BoundType>(bindTo);
unblock(c.resultType);
unblock(c.resultType, constraint->location);
return true;
}
@ -1440,7 +1440,7 @@ bool ConstraintSolver::tryDispatch(const HasPropConstraint& c, NotNull<const Con
TableType& ttv = asMutable(subjectType)->ty.emplace<TableType>(TableState::Free, TypeLevel{}, constraint->scope);
ttv.props[c.prop] = Property{c.resultType};
asMutable(c.resultType)->ty.emplace<FreeType>(constraint->scope);
unblock(c.resultType);
unblock(c.resultType, constraint->location);
return true;
}
@ -1454,7 +1454,7 @@ bool ConstraintSolver::tryDispatch(const HasPropConstraint& c, NotNull<const Con
}
asMutable(c.resultType)->ty.emplace<BoundType>(result.value_or(builtinTypes->anyType));
unblock(c.resultType);
unblock(c.resultType, constraint->location);
return true;
}
@ -1568,7 +1568,7 @@ bool ConstraintSolver::tryDispatch(const SetPropConstraint& c, NotNull<const Con
if (!isBlocked(c.propType))
unify(c.propType, *existingPropType, constraint->scope);
bind(c.resultType, c.subjectType);
unblock(c.resultType);
unblock(c.resultType, constraint->location);
return true;
}
@ -1593,8 +1593,8 @@ bool ConstraintSolver::tryDispatch(const SetPropConstraint& c, NotNull<const Con
bind(subjectType, ty);
if (follow(c.resultType) != follow(ty))
bind(c.resultType, ty);
unblock(subjectType);
unblock(c.resultType);
unblock(subjectType, constraint->location);
unblock(c.resultType, constraint->location);
return true;
}
else if (auto ttv = getMutable<TableType>(subjectType))
@ -1605,7 +1605,7 @@ bool ConstraintSolver::tryDispatch(const SetPropConstraint& c, NotNull<const Con
ttv->props[c.path[0]] = Property{c.propType};
bind(c.resultType, c.subjectType);
unblock(c.resultType);
unblock(c.resultType, constraint->location);
return true;
}
else if (ttv->state == TableState::Unsealed)
@ -1614,14 +1614,14 @@ bool ConstraintSolver::tryDispatch(const SetPropConstraint& c, NotNull<const Con
updateTheTableType(builtinTypes, NotNull{arena}, subjectType, c.path, c.propType);
bind(c.resultType, c.subjectType);
unblock(subjectType);
unblock(c.resultType);
unblock(subjectType, constraint->location);
unblock(c.resultType, constraint->location);
return true;
}
else
{
bind(c.resultType, subjectType);
unblock(c.resultType);
unblock(c.resultType, constraint->location);
return true;
}
}
@ -1630,7 +1630,7 @@ bool ConstraintSolver::tryDispatch(const SetPropConstraint& c, NotNull<const Con
// Other kinds of types don't change shape when properties are assigned
// to them. (if they allow properties at all!)
bind(c.resultType, subjectType);
unblock(c.resultType);
unblock(c.resultType, constraint->location);
return true;
}
}
@ -1649,8 +1649,8 @@ bool ConstraintSolver::tryDispatch(const SetIndexerConstraint& c, NotNull<const
asMutable(c.resultType)->ty.emplace<BoundType>(subjectType);
asMutable(c.propType)->ty.emplace<FreeType>(scope);
unblock(c.propType);
unblock(c.resultType);
unblock(c.propType, constraint->location);
unblock(c.resultType, constraint->location);
return true;
}
@ -1662,8 +1662,8 @@ bool ConstraintSolver::tryDispatch(const SetIndexerConstraint& c, NotNull<const
unify(c.indexType, tt->indexer->indexType, constraint->scope);
asMutable(c.propType)->ty.emplace<BoundType>(tt->indexer->indexResultType);
asMutable(c.resultType)->ty.emplace<BoundType>(subjectType);
unblock(c.propType);
unblock(c.resultType);
unblock(c.propType, constraint->location);
unblock(c.resultType, constraint->location);
return true;
}
else if (tt->state == TableState::Free || tt->state == TableState::Unsealed)
@ -1675,8 +1675,8 @@ bool ConstraintSolver::tryDispatch(const SetIndexerConstraint& c, NotNull<const
mtt->indexer = TableIndexer{promotedIndexTy, c.propType};
asMutable(c.propType)->ty.emplace<FreeType>(tt->scope);
asMutable(c.resultType)->ty.emplace<BoundType>(subjectType);
unblock(c.propType);
unblock(c.resultType);
unblock(c.propType, constraint->location);
unblock(c.resultType, constraint->location);
return true;
}
// Do not augment sealed or generic tables that lack indexers
@ -1684,8 +1684,8 @@ bool ConstraintSolver::tryDispatch(const SetIndexerConstraint& c, NotNull<const
asMutable(c.propType)->ty.emplace<BoundType>(builtinTypes->errorRecoveryType());
asMutable(c.resultType)->ty.emplace<BoundType>(builtinTypes->errorRecoveryType());
unblock(c.propType);
unblock(c.resultType);
unblock(c.propType, constraint->location);
unblock(c.resultType, constraint->location);
return true;
}
@ -1704,7 +1704,7 @@ bool ConstraintSolver::tryDispatch(const SingletonOrTopTypeConstraint& c, NotNul
else
*asMutable(c.resultType) = BoundType{builtinTypes->anyType};
unblock(c.resultType);
unblock(c.resultType, constraint->location);
return true;
}
@ -1720,7 +1720,7 @@ bool ConstraintSolver::tryDispatch(const UnpackConstraint& c, NotNull<const Cons
if (isBlocked(resultPack))
{
asMutable(resultPack)->ty.emplace<BoundTypePack>(sourcePack);
unblock(resultPack);
unblock(resultPack, constraint->location);
return true;
}
@ -1745,7 +1745,7 @@ bool ConstraintSolver::tryDispatch(const UnpackConstraint& c, NotNull<const Cons
}
else
asMutable(*destIter)->ty.emplace<BoundType>(srcTy);
unblock(*destIter);
unblock(*destIter, constraint->location);
}
else
unify(*destIter, srcTy, constraint->scope);
@ -1763,7 +1763,7 @@ bool ConstraintSolver::tryDispatch(const UnpackConstraint& c, NotNull<const Cons
if (isBlocked(*destIter))
{
asMutable(*destIter)->ty.emplace<BoundType>(builtinTypes->errorRecoveryType());
unblock(*destIter);
unblock(*destIter, constraint->location);
}
++destIter;
@ -1852,7 +1852,7 @@ bool ConstraintSolver::tryDispatch(const RefineConstraint& c, NotNull<const Cons
if (c.mode == RefineConstraint::Intersection && isNegatedAny(c.discriminant))
{
asMutable(c.resultType)->ty.emplace<BoundType>(c.type);
unblock(c.resultType);
unblock(c.resultType, constraint->location);
return true;
}
@ -1880,7 +1880,7 @@ bool ConstraintSolver::tryDispatch(const RefineConstraint& c, NotNull<const Cons
else
asMutable(c.resultType)->ty.emplace<BoundType>(c.discriminant);
unblock(c.resultType);
unblock(c.resultType, constraint->location);
return true;
}
@ -1892,7 +1892,7 @@ bool ConstraintSolver::tryDispatch(const RefineConstraint& c, NotNull<const Cons
asMutable(c.resultType)->ty.emplace<BoundType>(result);
unblock(c.resultType);
unblock(c.resultType, constraint->location);
return true;
}
@ -1904,10 +1904,10 @@ bool ConstraintSolver::tryDispatch(const ReduceConstraint& c, NotNull<const Cons
reduceFamilies(ty, constraint->location, NotNull{arena}, builtinTypes, constraint->scope, normalizer, nullptr, force);
for (TypeId r : result.reducedTypes)
unblock(r);
unblock(r, constraint->location);
for (TypePackId r : result.reducedPacks)
unblock(r);
unblock(r, constraint->location);
if (force)
return true;
@ -1928,10 +1928,10 @@ bool ConstraintSolver::tryDispatch(const ReducePackConstraint& c, NotNull<const
reduceFamilies(tp, constraint->location, NotNull{arena}, builtinTypes, constraint->scope, normalizer, nullptr, force);
for (TypeId r : result.reducedTypes)
unblock(r);
unblock(r, constraint->location);
for (TypePackId r : result.reducedPacks)
unblock(r);
unblock(r, constraint->location);
if (force)
return true;
@ -2374,8 +2374,8 @@ bool ConstraintSolver::tryUnify(NotNull<const Constraint> constraint, TID subTy,
u.log.commit();
unblock(changedTypes);
unblock(changedPacks);
unblock(changedTypes, constraint->location);
unblock(changedPacks, constraint->location);
return true;
}
@ -2509,7 +2509,7 @@ void ConstraintSolver::unblock(NotNull<const Constraint> progressed)
return unblock_(progressed.get());
}
void ConstraintSolver::unblock(TypeId ty)
void ConstraintSolver::unblock(TypeId ty, Location location)
{
DenseHashSet<TypeId> seen{nullptr};
@ -2517,7 +2517,7 @@ void ConstraintSolver::unblock(TypeId ty)
while (true)
{
if (seen.find(progressed))
iceReporter.ice("ConstraintSolver::unblock encountered a self-bound type!");
iceReporter.ice("ConstraintSolver::unblock encountered a self-bound type!", location);
seen.insert(progressed);
if (logger)
@ -2532,7 +2532,7 @@ void ConstraintSolver::unblock(TypeId ty)
}
}
void ConstraintSolver::unblock(TypePackId progressed)
void ConstraintSolver::unblock(TypePackId progressed, Location)
{
if (logger)
logger->popBlock(progressed);
@ -2540,16 +2540,16 @@ void ConstraintSolver::unblock(TypePackId progressed)
return unblock_(progressed);
}
void ConstraintSolver::unblock(const std::vector<TypeId>& types)
void ConstraintSolver::unblock(const std::vector<TypeId>& types, Location location)
{
for (TypeId t : types)
unblock(t);
unblock(t, location);
}
void ConstraintSolver::unblock(const std::vector<TypePackId>& packs)
void ConstraintSolver::unblock(const std::vector<TypePackId>& packs, Location location)
{
for (TypePackId t : packs)
unblock(t);
unblock(t, location);
}
bool ConstraintSolver::isBlocked(TypeId ty)
@ -2586,8 +2586,8 @@ ErrorVec ConstraintSolver::unify(TypeId subType, TypeId superType, NotNull<Scope
u.log.commit();
unblock(changedTypes);
unblock(changedPacks);
unblock(changedTypes, Location{});
unblock(changedPacks, Location{});
return std::move(u.errors);
}
@ -2604,8 +2604,8 @@ ErrorVec ConstraintSolver::unify(TypePackId subPack, TypePackId superPack, NotNu
u.log.commit();
unblock(changedTypes);
unblock(changedPacks);
unblock(changedTypes, Location{});
unblock(changedPacks, Location{});
return std::move(u.errors);
}

View File

@ -35,6 +35,7 @@ LUAU_FASTINTVARIABLE(LuauAutocompleteCheckTimeoutMs, 100)
LUAU_FASTFLAGVARIABLE(DebugLuauDeferredConstraintResolution, false)
LUAU_FASTFLAGVARIABLE(DebugLuauLogSolverToJson, false)
LUAU_FASTFLAGVARIABLE(DebugLuauReadWriteProperties, false)
LUAU_FASTFLAGVARIABLE(LuauFixBuildQueueExceptionUnwrap, false)
namespace Luau
{
@ -596,6 +597,7 @@ std::vector<ModuleName> Frontend::checkQueuedModules(std::optional<FrontendOptio
sendCycleItemTask();
std::vector<size_t> nextItems;
std::optional<size_t> itemWithException;
while (remaining != 0)
{
@ -603,17 +605,25 @@ std::vector<ModuleName> Frontend::checkQueuedModules(std::optional<FrontendOptio
std::unique_lock guard(mtx);
// If nothing is ready yet, wait
if (readyQueueItems.empty())
{
cv.wait(guard, [&readyQueueItems] {
return !readyQueueItems.empty();
});
}
// Handle checked items
for (size_t i : readyQueueItems)
{
const BuildQueueItem& item = buildQueueItems[i];
if (FFlag::LuauFixBuildQueueExceptionUnwrap)
{
// If exception was thrown, stop adding new items and wait for processing items to complete
if (item.exception)
itemWithException = i;
if (itemWithException)
break;
}
recordItemResult(item);
// Notify items that were waiting for this dependency
@ -648,8 +658,17 @@ std::vector<ModuleName> Frontend::checkQueuedModules(std::optional<FrontendOptio
// If we aren't done, but don't have anything processing, we hit a cycle
if (remaining != 0 && processing == 0)
{
// We might have stopped because of a pending exception
if (FFlag::LuauFixBuildQueueExceptionUnwrap && itemWithException)
{
recordItemResult(buildQueueItems[*itemWithException]);
break;
}
sendCycleItemTask();
}
}
std::vector<ModuleName> checkedModules;
checkedModules.reserve(buildQueueItems.size());
@ -1104,6 +1123,8 @@ ModulePtr check(const SourceModule& sourceModule, const std::vector<RequireCycle
result->name = sourceModule.name;
result->humanReadableName = sourceModule.humanReadableName;
iceHandler->moduleName = sourceModule.name;
std::unique_ptr<DcrLogger> logger;
if (recordJsonLog)
{
@ -1189,10 +1210,20 @@ ModulePtr Frontend::check(const SourceModule& sourceModule, Mode mode, std::vect
prepareModuleScope(name, scope, forAutocomplete);
};
try
{
return Luau::check(sourceModule, requireCycles, builtinTypes, NotNull{&iceHandler},
NotNull{forAutocomplete ? &moduleResolverForAutocomplete : &moduleResolver}, NotNull{fileResolver},
environmentScope ? *environmentScope : globals.globalScope, prepareModuleScopeWrap, options, recordJsonLog);
}
catch (const InternalCompilerError& err)
{
InternalCompilerError augmented = err.location.has_value()
? InternalCompilerError{err.message, sourceModule.humanReadableName, *err.location}
: InternalCompilerError{err.message, sourceModule.humanReadableName};
throw augmented;
}
}
else
{
TypeChecker typeChecker(forAutocomplete ? globalsForAutocomplete.globalScope : globals.globalScope,

View File

@ -2117,15 +2117,15 @@ std::optional<TypeId> Normalizer::intersectionOfTables(TypeId here, TypeId there
TypeId hmtable = nullptr;
if (const MetatableType* hmtv = get<MetatableType>(here))
{
htable = hmtv->table;
hmtable = hmtv->metatable;
htable = follow(hmtv->table);
hmtable = follow(hmtv->metatable);
}
TypeId ttable = there;
TypeId tmtable = nullptr;
if (const MetatableType* tmtv = get<MetatableType>(there))
{
ttable = tmtv->table;
tmtable = tmtv->metatable;
ttable = follow(tmtv->table);
tmtable = follow(tmtv->metatable);
}
const TableType* httv = get<TableType>(htable);

View File

@ -6,6 +6,7 @@
#include "Luau/ToString.h"
#include "Luau/TypeArena.h"
#include "Luau/Normalize.h" // TypeIds
#include <algorithm>
LUAU_FASTINT(LuauTypeReductionRecursionLimit)
@ -236,6 +237,17 @@ Relation relateTables(TypeId left, TypeId right)
NotNull<const TableType> leftTable{get<TableType>(left)};
NotNull<const TableType> rightTable{get<TableType>(right)};
LUAU_ASSERT(1 == rightTable->props.size());
// Disjoint props have nothing in common
// t1 with props p1's cannot appear in t2 and t2 with props p2's cannot appear in t1
bool foundPropFromLeftInRight = std::any_of(begin(leftTable->props), end(leftTable->props), [&](auto prop) {
return rightTable->props.find(prop.first) != end(rightTable->props);
});
bool foundPropFromRightInLeft = std::any_of(begin(rightTable->props), end(rightTable->props), [&](auto prop) {
return leftTable->props.find(prop.first) != end(leftTable->props);
});
if (!(foundPropFromLeftInRight || foundPropFromRightInLeft) && leftTable->props.size() >= 1 && rightTable->props.size() >= 1)
return Relation::Disjoint;
const auto [propName, rightProp] = *begin(rightTable->props);

View File

@ -111,11 +111,14 @@ static TypeId shallowClone(TypeId ty, TypeArena& dest, const TxnLog* log, bool a
else if constexpr (std::is_same_v<T, GenericType>)
return dest.addType(a);
else if constexpr (std::is_same_v<T, BlockedType>)
return ty;
return dest.addType(a);
else if constexpr (std::is_same_v<T, PrimitiveType>)
return ty;
else if constexpr (std::is_same_v<T, PendingExpansionType>)
return ty;
{
PendingExpansionType clone = PendingExpansionType{a.prefix, a.name, a.typeArguments, a.packArguments};
return dest.addType(std::move(clone));
}
else if constexpr (std::is_same_v<T, AnyType>)
return ty;
else if constexpr (std::is_same_v<T, ErrorType>)

View File

@ -6,7 +6,6 @@
#include "Luau/CodeGen.h"
#include "Luau/Compiler.h"
#include "Luau/BytecodeBuilder.h"
#include "Luau/Parser.h"
#include "Luau/TimeTrace.h"
@ -40,27 +39,6 @@
LUAU_FASTFLAG(DebugLuauTimeTracing)
enum class CliMode
{
Unknown,
Repl,
Compile,
RunSourceFiles
};
enum class CompileFormat
{
Text,
Binary,
Remarks,
Codegen, // Prints annotated native code including IR and assembly
CodegenAsm, // Prints annotated native code assembly
CodegenIr, // Prints annotated native code IR
CodegenVerbose, // Prints annotated native code including IR, assembly and outlined code
CodegenNull,
Null
};
constexpr int MaxTraversalLimit = 50;
static bool codegen = false;
@ -668,178 +646,11 @@ static bool runFile(const char* name, lua_State* GL, bool repl)
return status == 0;
}
static void report(const char* name, const Luau::Location& location, const char* type, const char* message)
{
fprintf(stderr, "%s(%d,%d): %s: %s\n", name, location.begin.line + 1, location.begin.column + 1, type, message);
}
static void reportError(const char* name, const Luau::ParseError& error)
{
report(name, error.getLocation(), "SyntaxError", error.what());
}
static void reportError(const char* name, const Luau::CompileError& error)
{
report(name, error.getLocation(), "CompileError", error.what());
}
static std::string getCodegenAssembly(const char* name, const std::string& bytecode, Luau::CodeGen::AssemblyOptions options)
{
std::unique_ptr<lua_State, void (*)(lua_State*)> globalState(luaL_newstate(), lua_close);
lua_State* L = globalState.get();
if (luau_load(L, name, bytecode.data(), bytecode.size(), 0) == 0)
return Luau::CodeGen::getAssembly(L, -1, options);
fprintf(stderr, "Error loading bytecode %s\n", name);
return "";
}
static void annotateInstruction(void* context, std::string& text, int fid, int instpos)
{
Luau::BytecodeBuilder& bcb = *(Luau::BytecodeBuilder*)context;
bcb.annotateInstruction(text, fid, instpos);
}
struct CompileStats
{
size_t lines;
size_t bytecode;
size_t codegen;
double readTime;
double miscTime;
double parseTime;
double compileTime;
double codegenTime;
};
static double recordDeltaTime(double& timer)
{
double now = Luau::TimeTrace::getClock();
double delta = now - timer;
timer = now;
return delta;
}
static bool compileFile(const char* name, CompileFormat format, CompileStats& stats)
{
double currts = Luau::TimeTrace::getClock();
std::optional<std::string> source = readFile(name);
if (!source)
{
fprintf(stderr, "Error opening %s\n", name);
return false;
}
stats.readTime += recordDeltaTime(currts);
// NOTE: Normally, you should use Luau::compile or luau_compile (see lua_require as an example)
// This function is much more complicated because it supports many output human-readable formats through internal interfaces
try
{
Luau::BytecodeBuilder bcb;
Luau::CodeGen::AssemblyOptions options;
options.outputBinary = format == CompileFormat::CodegenNull;
if (!options.outputBinary)
{
options.includeAssembly = format != CompileFormat::CodegenIr;
options.includeIr = format != CompileFormat::CodegenAsm;
options.includeOutlinedCode = format == CompileFormat::CodegenVerbose;
}
options.annotator = annotateInstruction;
options.annotatorContext = &bcb;
if (format == CompileFormat::Text)
{
bcb.setDumpFlags(Luau::BytecodeBuilder::Dump_Code | Luau::BytecodeBuilder::Dump_Source | Luau::BytecodeBuilder::Dump_Locals |
Luau::BytecodeBuilder::Dump_Remarks);
bcb.setDumpSource(*source);
}
else if (format == CompileFormat::Remarks)
{
bcb.setDumpFlags(Luau::BytecodeBuilder::Dump_Source | Luau::BytecodeBuilder::Dump_Remarks);
bcb.setDumpSource(*source);
}
else if (format == CompileFormat::Codegen || format == CompileFormat::CodegenAsm || format == CompileFormat::CodegenIr ||
format == CompileFormat::CodegenVerbose)
{
bcb.setDumpFlags(Luau::BytecodeBuilder::Dump_Code | Luau::BytecodeBuilder::Dump_Source | Luau::BytecodeBuilder::Dump_Locals |
Luau::BytecodeBuilder::Dump_Remarks);
bcb.setDumpSource(*source);
}
stats.miscTime += recordDeltaTime(currts);
Luau::Allocator allocator;
Luau::AstNameTable names(allocator);
Luau::ParseResult result = Luau::Parser::parse(source->c_str(), source->size(), names, allocator);
if (!result.errors.empty())
throw Luau::ParseErrors(result.errors);
stats.lines += result.lines;
stats.parseTime += recordDeltaTime(currts);
Luau::compileOrThrow(bcb, result, names, copts());
stats.bytecode += bcb.getBytecode().size();
stats.compileTime += recordDeltaTime(currts);
switch (format)
{
case CompileFormat::Text:
printf("%s", bcb.dumpEverything().c_str());
break;
case CompileFormat::Remarks:
printf("%s", bcb.dumpSourceRemarks().c_str());
break;
case CompileFormat::Binary:
fwrite(bcb.getBytecode().data(), 1, bcb.getBytecode().size(), stdout);
break;
case CompileFormat::Codegen:
case CompileFormat::CodegenAsm:
case CompileFormat::CodegenIr:
case CompileFormat::CodegenVerbose:
printf("%s", getCodegenAssembly(name, bcb.getBytecode(), options).c_str());
break;
case CompileFormat::CodegenNull:
stats.codegen += getCodegenAssembly(name, bcb.getBytecode(), options).size();
stats.codegenTime += recordDeltaTime(currts);
break;
case CompileFormat::Null:
break;
}
return true;
}
catch (Luau::ParseErrors& e)
{
for (auto& error : e.getErrors())
reportError(name, error);
return false;
}
catch (Luau::CompileError& e)
{
reportError(name, e);
return false;
}
}
static void displayHelp(const char* argv0)
{
printf("Usage: %s [--mode] [options] [file list]\n", argv0);
printf("Usage: %s [options] [file list]\n", argv0);
printf("\n");
printf("When mode and file list are omitted, an interactive REPL is started instead.\n");
printf("\n");
printf("Available modes:\n");
printf(" omitted: compile and run input files one by one\n");
printf(" --compile[=format]: compile input files and output resulting bytecode/assembly (binary, text, remarks, codegen)\n");
printf("When file list is omitted, an interactive REPL is started instead.\n");
printf("\n");
printf("Available options:\n");
printf(" --coverage: collect code coverage while running the code and output results to coverage.out\n");
@ -864,67 +675,12 @@ int replMain(int argc, char** argv)
setLuauFlagsDefault();
CliMode mode = CliMode::Unknown;
CompileFormat compileFormat{};
int profile = 0;
bool coverage = false;
bool interactive = false;
bool codegenPerf = false;
// Set the mode if the user has explicitly specified one.
int argStart = 1;
if (argc >= 2 && strncmp(argv[1], "--compile", strlen("--compile")) == 0)
{
argStart++;
mode = CliMode::Compile;
if (strcmp(argv[1], "--compile") == 0)
{
compileFormat = CompileFormat::Text;
}
else if (strcmp(argv[1], "--compile=binary") == 0)
{
compileFormat = CompileFormat::Binary;
}
else if (strcmp(argv[1], "--compile=text") == 0)
{
compileFormat = CompileFormat::Text;
}
else if (strcmp(argv[1], "--compile=remarks") == 0)
{
compileFormat = CompileFormat::Remarks;
}
else if (strcmp(argv[1], "--compile=codegen") == 0)
{
compileFormat = CompileFormat::Codegen;
}
else if (strcmp(argv[1], "--compile=codegenasm") == 0)
{
compileFormat = CompileFormat::CodegenAsm;
}
else if (strcmp(argv[1], "--compile=codegenir") == 0)
{
compileFormat = CompileFormat::CodegenIr;
}
else if (strcmp(argv[1], "--compile=codegenverbose") == 0)
{
compileFormat = CompileFormat::CodegenVerbose;
}
else if (strcmp(argv[1], "--compile=codegennull") == 0)
{
compileFormat = CompileFormat::CodegenNull;
}
else if (strcmp(argv[1], "--compile=null") == 0)
{
compileFormat = CompileFormat::Null;
}
else
{
fprintf(stderr, "Error: Unrecognized value for '--compile' specified.\n");
return 1;
}
}
for (int i = argStart; i < argc; i++)
for (int i = 1; i < argc; i++)
{
if (strcmp(argv[i], "-h") == 0 || strcmp(argv[i], "--help") == 0)
{
@ -1026,50 +782,20 @@ int replMain(int argc, char** argv)
#endif
}
const std::vector<std::string> files = getSourceFiles(argc, argv);
if (mode == CliMode::Unknown)
{
mode = files.empty() ? CliMode::Repl : CliMode::RunSourceFiles;
}
if (mode != CliMode::Compile && codegen && !Luau::CodeGen::isSupported())
if (codegen && !Luau::CodeGen::isSupported())
{
fprintf(stderr, "Cannot enable --codegen, native code generation is not supported in current configuration\n");
return 1;
}
switch (mode)
{
case CliMode::Compile:
{
#ifdef _WIN32
if (compileFormat == CompileFormat::Binary)
_setmode(_fileno(stdout), _O_BINARY);
#endif
const std::vector<std::string> files = getSourceFiles(argc, argv);
CompileStats stats = {};
int failed = 0;
for (const std::string& path : files)
failed += !compileFile(path.c_str(), compileFormat, stats);
if (compileFormat == CompileFormat::Null)
printf("Compiled %d KLOC into %d KB bytecode (read %.2fs, parse %.2fs, compile %.2fs)\n", int(stats.lines / 1000),
int(stats.bytecode / 1024), stats.readTime, stats.parseTime, stats.compileTime);
else if (compileFormat == CompileFormat::CodegenNull)
printf("Compiled %d KLOC into %d KB bytecode => %d KB native code (%.2fx) (read %.2fs, parse %.2fs, compile %.2fs, codegen %.2fs)\n",
int(stats.lines / 1000), int(stats.bytecode / 1024), int(stats.codegen / 1024),
stats.bytecode == 0 ? 0.0 : double(stats.codegen) / double(stats.bytecode), stats.readTime, stats.parseTime, stats.compileTime,
stats.codegenTime);
return failed ? 1 : 0;
}
case CliMode::Repl:
if (files.empty())
{
runRepl();
return 0;
}
case CliMode::RunSourceFiles:
else
{
std::unique_ptr<lua_State, void (*)(lua_State*)> globalState(luaL_newstate(), lua_close);
lua_State* L = globalState.get();
@ -1101,9 +827,4 @@ int replMain(int argc, char** argv)
return failed ? 1 : 0;
}
case CliMode::Unknown:
default:
LUAU_ASSERT(!"Unhandled cli mode.");
return 1;
}
}

View File

@ -98,6 +98,8 @@ public:
void call(Label& label);
void call(OperandX64 op);
void lea(RegisterX64 lhs, Label& label);
void int3();
void ud2();
@ -243,6 +245,7 @@ private:
LUAU_NOINLINE void log(const char* opcode, OperandX64 op1, OperandX64 op2, OperandX64 op3, OperandX64 op4);
LUAU_NOINLINE void log(Label label);
LUAU_NOINLINE void log(const char* opcode, Label label);
LUAU_NOINLINE void log(const char* opcode, RegisterX64 reg, Label label);
void log(OperandX64 op);
const char* getSizeName(SizeX64 size) const;

View File

@ -801,6 +801,8 @@ struct IrBlock
uint32_t start = ~0u;
uint32_t finish = ~0u;
uint32_t sortkey = ~0u;
Label label;
};

View File

@ -38,6 +38,8 @@ std::string toString(const IrFunction& function, bool includeUseInfo);
std::string dump(const IrFunction& function);
std::string toDot(const IrFunction& function, bool includeInst);
std::string toDotCfg(const IrFunction& function);
std::string toDotDjGraph(const IrFunction& function);
std::string dumpDot(const IrFunction& function, bool includeInst);

View File

@ -463,6 +463,20 @@ void AssemblyBuilderX64::call(OperandX64 op)
commit();
}
void AssemblyBuilderX64::lea(RegisterX64 lhs, Label& label)
{
LUAU_ASSERT(lhs.size == SizeX64::qword);
placeBinaryRegAndRegMem(lhs, OperandX64(SizeX64::qword, noreg, 1, rip, 0), 0x8d, 0x8d);
codePos -= 4;
placeLabel(label);
commit();
if (logText)
log("lea", lhs, label);
}
void AssemblyBuilderX64::int3()
{
if (logText)
@ -1415,7 +1429,7 @@ void AssemblyBuilderX64::commit()
{
LUAU_ASSERT(codePos <= codeEnd);
if (codeEnd - codePos < kMaxInstructionLength)
if (unsigned(codeEnd - codePos) < kMaxInstructionLength)
extend();
}
@ -1501,6 +1515,14 @@ void AssemblyBuilderX64::log(const char* opcode, Label label)
logAppend(" %-12s.L%d\n", opcode, label.id);
}
void AssemblyBuilderX64::log(const char* opcode, RegisterX64 reg, Label label)
{
logAppend(" %-12s", opcode);
log(reg);
text.append(",");
logAppend(".L%d\n", label.id);
}
void AssemblyBuilderX64::log(OperandX64 op)
{
switch (op.cat)

View File

@ -56,8 +56,10 @@ static void makePagesExecutable(uint8_t* mem, size_t size)
static void flushInstructionCache(uint8_t* mem, size_t size)
{
#if WINAPI_FAMILY_PARTITION(WINAPI_PARTITION_APP | WINAPI_PARTITION_SYSTEM)
if (FlushInstructionCache(GetCurrentProcess(), mem, size) == 0)
LUAU_ASSERT(!"Failed to flush instruction cache");
#endif
}
#else
static uint8_t* allocatePages(size_t size)

View File

@ -125,7 +125,7 @@ static bool lowerImpl(AssemblyBuilder& build, IrLowering& lowering, IrFunction&
return (a.kind == IrBlockKind::Fallback) < (b.kind == IrBlockKind::Fallback);
// Try to order by instruction order
return a.start < b.start;
return a.sortkey < b.sortkey;
});
// For each IR instruction that begins a bytecode instruction, which bytecode instruction is it?
@ -234,6 +234,8 @@ static bool lowerImpl(AssemblyBuilder& build, IrLowering& lowering, IrFunction&
build.setLabel(abandoned.label);
}
lowering.finishFunction();
return false;
}
}
@ -244,7 +246,15 @@ static bool lowerImpl(AssemblyBuilder& build, IrLowering& lowering, IrFunction&
build.logAppend("#\n");
}
if (outputEnabled && !options.includeOutlinedCode && seenFallback)
if (!seenFallback)
{
textSize = build.text.length();
codeSize = build.getCodeSize();
}
lowering.finishFunction();
if (outputEnabled && !options.includeOutlinedCode && textSize < build.text.size())
{
build.text.resize(textSize);
@ -594,6 +604,12 @@ std::string getAssembly(lua_State* L, int idx, AssemblyOptions options)
X64::assembleHelpers(build, helpers);
#endif
if (!options.includeOutlinedCode && options.includeAssembly)
{
build.text.clear();
build.logAppend("; skipping %u bytes of outlined helpers\n", unsigned(build.getCodeSize() * sizeof(build.code[0])));
}
for (Proto* p : protos)
if (p)
if (std::optional<NativeProto> np = assembleFunction(build, data, helpers, p, options))

View File

@ -288,27 +288,27 @@ void assembleHelpers(AssemblyBuilderA64& build, ModuleHelpers& helpers)
{
if (build.logText)
build.logAppend("; exitContinueVm\n");
helpers.exitContinueVm = build.setLabel();
build.setLabel(helpers.exitContinueVm);
emitExit(build, /* continueInVm */ true);
if (build.logText)
build.logAppend("; exitNoContinueVm\n");
helpers.exitNoContinueVm = build.setLabel();
build.setLabel(helpers.exitNoContinueVm);
emitExit(build, /* continueInVm */ false);
if (build.logText)
build.logAppend("; reentry\n");
helpers.reentry = build.setLabel();
build.setLabel(helpers.reentry);
emitReentry(build, helpers);
if (build.logText)
build.logAppend("; interrupt\n");
helpers.interrupt = build.setLabel();
build.setLabel(helpers.interrupt);
emitInterrupt(build);
if (build.logText)
build.logAppend("; return\n");
helpers.return_ = build.setLabel();
build.setLabel(helpers.return_);
emitReturn(build, helpers);
}

View File

@ -56,6 +56,11 @@ static EntryLocations buildEntryFunction(AssemblyBuilderX64& build, UnwindBuilde
locations.start = build.setLabel();
unwind.startFunction();
RegisterX64 rArg1 = (build.abi == ABIX64::Windows) ? rcx : rdi;
RegisterX64 rArg2 = (build.abi == ABIX64::Windows) ? rdx : rsi;
RegisterX64 rArg3 = (build.abi == ABIX64::Windows) ? r8 : rdx;
RegisterX64 rArg4 = (build.abi == ABIX64::Windows) ? r9 : rcx;
// Save common non-volatile registers
if (build.abi == ABIX64::SystemV)
{
@ -177,22 +182,27 @@ void assembleHelpers(X64::AssemblyBuilderX64& build, ModuleHelpers& helpers)
{
if (build.logText)
build.logAppend("; exitContinueVm\n");
helpers.exitContinueVm = build.setLabel();
build.setLabel(helpers.exitContinueVm);
emitExit(build, /* continueInVm */ true);
if (build.logText)
build.logAppend("; exitNoContinueVm\n");
helpers.exitNoContinueVm = build.setLabel();
build.setLabel(helpers.exitNoContinueVm);
emitExit(build, /* continueInVm */ false);
if (build.logText)
build.logAppend("; continueCallInVm\n");
helpers.continueCallInVm = build.setLabel();
build.setLabel(helpers.continueCallInVm);
emitContinueCallInVm(build);
if (build.logText)
build.logAppend("; interrupt\n");
build.setLabel(helpers.interrupt);
emitInterrupt(build);
if (build.logText)
build.logAppend("; return\n");
helpers.return_ = build.setLabel();
build.setLabel(helpers.return_);
emitReturn(build, helpers);
}

View File

@ -25,13 +25,13 @@ struct ModuleHelpers
Label exitContinueVm;
Label exitNoContinueVm;
Label return_;
Label interrupt;
// X64
Label continueCallInVm;
// A64
Label reentry; // x0: closure
Label interrupt; // x0: pc offset, x1: return address, x2: interrupt
};
} // namespace CodeGen

View File

@ -278,39 +278,34 @@ void emitUpdateBase(AssemblyBuilderX64& build)
build.mov(rBase, qword[rState + offsetof(lua_State, base)]);
}
static void emitSetSavedPc(IrRegAllocX64& regs, AssemblyBuilderX64& build, int pcpos)
void emitInterrupt(AssemblyBuilderX64& build)
{
ScopedRegX64 tmp1{regs, SizeX64::qword};
ScopedRegX64 tmp2{regs, SizeX64::qword};
// rax = pcpos + 1
// rbx = return address in native code
build.mov(tmp1.reg, sCode);
build.add(tmp1.reg, pcpos * sizeof(Instruction));
build.mov(tmp2.reg, qword[rState + offsetof(lua_State, ci)]);
build.mov(qword[tmp2.reg + offsetof(CallInfo, savedpc)], tmp1.reg);
}
// note: rbx is non-volatile so it will be saved across interrupt call automatically
RegisterX64 rArg1 = (build.abi == ABIX64::Windows) ? rcx : rdi;
RegisterX64 rArg2 = (build.abi == ABIX64::Windows) ? rdx : rsi;
void emitInterrupt(IrRegAllocX64& regs, AssemblyBuilderX64& build, int pcpos)
{
Label skip;
ScopedRegX64 tmp{regs, SizeX64::qword};
// Update L->ci->savedpc; required in case interrupt errors
build.mov(rcx, sCode);
build.lea(rcx, addr[rcx + rax * sizeof(Instruction)]);
build.mov(rax, qword[rState + offsetof(lua_State, ci)]);
build.mov(qword[rax + offsetof(CallInfo, savedpc)], rcx);
// Skip if there is no interrupt set
build.mov(tmp.reg, qword[rState + offsetof(lua_State, global)]);
build.mov(tmp.reg, qword[tmp.reg + offsetof(global_State, cb.interrupt)]);
build.test(tmp.reg, tmp.reg);
// Load interrupt handler; it may be nullptr in case the update raced with the check before we got here
build.mov(rax, qword[rState + offsetof(lua_State, global)]);
build.mov(rax, qword[rax + offsetof(global_State, cb.interrupt)]);
build.test(rax, rax);
build.jcc(ConditionX64::Zero, skip);
emitSetSavedPc(regs, build, pcpos + 1);
// Call interrupt
// TODO: This code should move to the end of the function, or even be outlined so that it can be shared by multiple interruptible instructions
IrCallWrapperX64 callWrap(regs, build);
callWrap.addArgument(SizeX64::qword, rState);
callWrap.addArgument(SizeX64::dword, -1);
callWrap.call(tmp.release());
emitUpdateBase(build); // interrupt may have reallocated stack
build.mov(rArg1, rState);
build.mov(dwordReg(rArg2), -1);
build.call(rax);
// Check if we need to exit
build.mov(al, byte[rState + offsetof(lua_State, status)]);
@ -322,6 +317,10 @@ void emitInterrupt(IrRegAllocX64& regs, AssemblyBuilderX64& build, int pcpos)
emitExit(build, /* continueInVm */ false);
build.setLabel(skip);
emitUpdateBase(build); // interrupt may have reallocated stack
build.jmp(rbx);
}
void emitFallback(IrRegAllocX64& regs, AssemblyBuilderX64& build, int offset, int pcpos)
@ -354,14 +353,15 @@ void emitContinueCallInVm(AssemblyBuilderX64& build)
void emitReturn(AssemblyBuilderX64& build, ModuleHelpers& helpers)
{
// input: ci in r8, res in rdi, number of written values in ecx
RegisterX64 ci = r8;
// input: res in rdi, number of written values in ecx
RegisterX64 res = rdi;
RegisterX64 written = ecx;
RegisterX64 ci = r8;
RegisterX64 cip = r9;
RegisterX64 nresults = esi;
build.mov(ci, qword[rState + offsetof(lua_State, ci)]);
build.lea(cip, addr[ci - sizeof(CallInfo)]);
// nresults = ci->nresults

View File

@ -53,31 +53,6 @@ constexpr OperandX64 sCode = qword[rsp + kStackSize + 8]; // Instruction* cod
constexpr OperandX64 sTemporarySlot = addr[rsp + kStackSize + 16];
constexpr OperandX64 sSpillArea = addr[rsp + kStackSize + 24];
// TODO: These should be replaced with a portable call function that checks the ABI at runtime and reorders moves accordingly to avoid conflicts
#if defined(_WIN32)
constexpr RegisterX64 rArg1 = rcx;
constexpr RegisterX64 rArg2 = rdx;
constexpr RegisterX64 rArg3 = r8;
constexpr RegisterX64 rArg4 = r9;
constexpr RegisterX64 rArg5 = noreg;
constexpr RegisterX64 rArg6 = noreg;
constexpr OperandX64 sArg5 = qword[rsp + 32];
constexpr OperandX64 sArg6 = qword[rsp + 40];
#else
constexpr RegisterX64 rArg1 = rdi;
constexpr RegisterX64 rArg2 = rsi;
constexpr RegisterX64 rArg3 = rdx;
constexpr RegisterX64 rArg4 = rcx;
constexpr RegisterX64 rArg5 = r8;
constexpr RegisterX64 rArg6 = r9;
constexpr OperandX64 sArg5 = noreg;
constexpr OperandX64 sArg6 = noreg;
#endif
inline OperandX64 luauReg(int ri)
{
return xmmword[rBase + ri * sizeof(TValue)];
@ -202,7 +177,7 @@ void callStepGc(IrRegAllocX64& regs, AssemblyBuilderX64& build);
void emitExit(AssemblyBuilderX64& build, bool continueInVm);
void emitUpdateBase(AssemblyBuilderX64& build);
void emitInterrupt(IrRegAllocX64& regs, AssemblyBuilderX64& build, int pcpos);
void emitInterrupt(AssemblyBuilderX64& build);
void emitFallback(IrRegAllocX64& regs, AssemblyBuilderX64& build, int offset, int pcpos);
void emitContinueCallInVm(AssemblyBuilderX64& build);

View File

@ -18,6 +18,12 @@ namespace X64
void emitInstCall(AssemblyBuilderX64& build, ModuleHelpers& helpers, int ra, int nparams, int nresults)
{
// TODO: This should use IrCallWrapperX64
RegisterX64 rArg1 = (build.abi == ABIX64::Windows) ? rcx : rdi;
RegisterX64 rArg2 = (build.abi == ABIX64::Windows) ? rdx : rsi;
RegisterX64 rArg3 = (build.abi == ABIX64::Windows) ? r8 : rdx;
RegisterX64 rArg4 = (build.abi == ABIX64::Windows) ? r9 : rcx;
build.mov(rArg1, rState);
build.lea(rArg2, luauRegAddress(ra));
@ -163,20 +169,34 @@ void emitInstCall(AssemblyBuilderX64& build, ModuleHelpers& helpers, int ra, int
}
}
void emitInstReturn(AssemblyBuilderX64& build, ModuleHelpers& helpers, int ra, int actualResults)
void emitInstReturn(AssemblyBuilderX64& build, ModuleHelpers& helpers, int ra, int actualResults, bool functionVariadic)
{
RegisterX64 ci = r8;
RegisterX64 res = rdi;
RegisterX64 written = ecx;
build.mov(ci, qword[rState + offsetof(lua_State, ci)]);
build.mov(res, qword[ci + offsetof(CallInfo, func)]);
if (functionVariadic)
{
build.mov(res, qword[rState + offsetof(lua_State, ci)]);
build.mov(res, qword[res + offsetof(CallInfo, func)]);
}
else if (actualResults != 1)
build.lea(res, addr[rBase - sizeof(TValue)]); // invariant: ci->func + 1 == ci->base for non-variadic frames
if (actualResults == 0)
{
build.xor_(written, written);
build.jmp(helpers.return_);
}
else if (actualResults == 1 && !functionVariadic)
{
// fast path: minimizes res adjustments
// note that we skipped res computation for this specific case above
build.vmovups(xmm0, luauReg(ra));
build.vmovups(xmmword[rBase - sizeof(TValue)], xmm0);
build.mov(res, rBase);
build.mov(written, 1);
build.jmp(helpers.return_);
}
else if (actualResults >= 1 && actualResults <= 3)
{
for (int r = 0; r < actualResults; ++r)
@ -206,8 +226,11 @@ void emitInstReturn(AssemblyBuilderX64& build, ModuleHelpers& helpers, int ra, i
Label repeatValueLoop, exitValueLoop;
if (actualResults == LUA_MULTRET)
{
build.cmp(vali, valend);
build.jcc(ConditionX64::NotBelow, exitValueLoop);
}
build.setLabel(repeatValueLoop);
build.vmovups(xmm0, xmmword[vali]);
@ -225,6 +248,11 @@ void emitInstReturn(AssemblyBuilderX64& build, ModuleHelpers& helpers, int ra, i
void emitInstSetList(IrRegAllocX64& regs, AssemblyBuilderX64& build, int ra, int rb, int count, uint32_t index)
{
// TODO: This should use IrCallWrapperX64
RegisterX64 rArg1 = (build.abi == ABIX64::Windows) ? rcx : rdi;
RegisterX64 rArg2 = (build.abi == ABIX64::Windows) ? rdx : rsi;
RegisterX64 rArg3 = (build.abi == ABIX64::Windows) ? r8 : rdx;
OperandX64 last = index + count - 1;
// Using non-volatile 'rbx' for dynamic 'count' value (for LUA_MULTRET) to skip later recomputation
@ -327,6 +355,12 @@ void emitInstForGLoop(AssemblyBuilderX64& build, int ra, int aux, Label& loopRep
// ipairs-style traversal is handled in IR
LUAU_ASSERT(aux >= 0);
// TODO: This should use IrCallWrapperX64
RegisterX64 rArg1 = (build.abi == ABIX64::Windows) ? rcx : rdi;
RegisterX64 rArg2 = (build.abi == ABIX64::Windows) ? rdx : rsi;
RegisterX64 rArg3 = (build.abi == ABIX64::Windows) ? r8 : rdx;
RegisterX64 rArg4 = (build.abi == ABIX64::Windows) ? r9 : rcx;
// This is a fast-path for builtin table iteration, tag check for 'ra' has to be performed before emitting this instruction
// Registers are chosen in this way to simplify fallback code for the node part

View File

@ -18,7 +18,7 @@ class AssemblyBuilderX64;
struct IrRegAllocX64;
void emitInstCall(AssemblyBuilderX64& build, ModuleHelpers& helpers, int ra, int nparams, int nresults);
void emitInstReturn(AssemblyBuilderX64& build, ModuleHelpers& helpers, int ra, int actualResults);
void emitInstReturn(AssemblyBuilderX64& build, ModuleHelpers& helpers, int ra, int actualResults, bool functionVariadic);
void emitInstSetList(IrRegAllocX64& regs, AssemblyBuilderX64& build, int ra, int rb, int count, uint32_t index);
void emitInstForGLoop(AssemblyBuilderX64& build, int ra, int aux, Label& loopRepeat);

View File

@ -429,6 +429,7 @@ void IrBuilder::beginBlock(IrOp block)
LUAU_ASSERT(target.start == ~0u || target.start == uint32_t(function.instructions.size()));
target.start = uint32_t(function.instructions.size());
target.sortkey = target.start;
inTerminatedBlock = false;
}

View File

@ -656,12 +656,8 @@ std::string dump(const IrFunction& function)
return result;
}
std::string toDot(const IrFunction& function, bool includeInst)
static void appendLabelRegset(IrToStringContext& ctx, const std::vector<RegisterSet>& regSets, size_t blockIdx, const char* name)
{
std::string result;
IrToStringContext ctx{result, function.blocks, function.constants, function.cfg};
auto appendLabelRegset = [&ctx](const std::vector<RegisterSet>& regSets, size_t blockIdx, const char* name) {
if (blockIdx < regSets.size())
{
const RegisterSet& rs = regSets[blockIdx];
@ -673,11 +669,10 @@ std::string toDot(const IrFunction& function, bool includeInst)
append(ctx.result, "}");
}
}
};
append(ctx.result, "digraph CFG {\n");
append(ctx.result, "node[shape=record]\n");
}
static void appendBlocks(IrToStringContext& ctx, const IrFunction& function, bool includeInst, bool includeIn, bool includeOut, bool includeDef)
{
for (size_t i = 0; i < function.blocks.size(); i++)
{
const IrBlock& block = function.blocks[i];
@ -692,7 +687,8 @@ std::string toDot(const IrFunction& function, bool includeInst)
append(ctx.result, "label=\"{");
toString(ctx, block, uint32_t(i));
appendLabelRegset(ctx.cfg.in, i, "in");
if (includeIn)
appendLabelRegset(ctx, ctx.cfg.in, i, "in");
if (includeInst && block.start != ~0u)
{
@ -709,11 +705,25 @@ std::string toDot(const IrFunction& function, bool includeInst)
}
}
appendLabelRegset(ctx.cfg.def, i, "def");
appendLabelRegset(ctx.cfg.out, i, "out");
if (includeDef)
appendLabelRegset(ctx, ctx.cfg.def, i, "def");
if (includeOut)
appendLabelRegset(ctx, ctx.cfg.out, i, "out");
append(ctx.result, "}\"];\n");
}
}
std::string toDot(const IrFunction& function, bool includeInst)
{
std::string result;
IrToStringContext ctx{result, function.blocks, function.constants, function.cfg};
append(ctx.result, "digraph CFG {\n");
append(ctx.result, "node[shape=record]\n");
appendBlocks(ctx, function, includeInst, /* includeIn */ true, /* includeOut */ true, /* includeDef */ true);
for (size_t i = 0; i < function.blocks.size(); i++)
{
@ -750,6 +760,107 @@ std::string toDot(const IrFunction& function, bool includeInst)
return result;
}
std::string toDotCfg(const IrFunction& function)
{
std::string result;
IrToStringContext ctx{result, function.blocks, function.constants, function.cfg};
append(ctx.result, "digraph CFG {\n");
append(ctx.result, "node[shape=record]\n");
appendBlocks(ctx, function, /* includeInst */ false, /* includeIn */ false, /* includeOut */ false, /* includeDef */ true);
for (size_t i = 0; i < function.blocks.size() && i < ctx.cfg.successorsOffsets.size(); i++)
{
BlockIteratorWrapper succ = successors(ctx.cfg, unsigned(i));
for (uint32_t target : succ)
append(ctx.result, "b%u -> b%u;\n", unsigned(i), target);
}
append(ctx.result, "}\n");
return result;
}
std::string toDotDjGraph(const IrFunction& function)
{
std::string result;
IrToStringContext ctx{result, function.blocks, function.constants, function.cfg};
append(ctx.result, "digraph CFG {\n");
for (size_t i = 0; i < ctx.blocks.size(); i++)
{
const IrBlock& block = ctx.blocks[i];
append(ctx.result, "b%u [", unsigned(i));
if (block.kind == IrBlockKind::Fallback)
append(ctx.result, "style=filled;fillcolor=salmon;");
else if (block.kind == IrBlockKind::Bytecode)
append(ctx.result, "style=filled;fillcolor=palegreen;");
append(ctx.result, "label=\"");
toString(ctx, block, uint32_t(i));
append(ctx.result, "\"];\n");
}
// Layer by depth in tree
uint32_t depth = 0;
bool found = true;
while (found)
{
found = false;
append(ctx.result, "{rank = same;");
for (size_t i = 0; i < ctx.cfg.domOrdering.size(); i++)
{
if (ctx.cfg.domOrdering[i].depth == depth)
{
append(ctx.result, "b%u;", unsigned(i));
found = true;
}
}
append(ctx.result, "}\n");
depth++;
}
for (size_t i = 0; i < ctx.cfg.domChildrenOffsets.size(); i++)
{
BlockIteratorWrapper dom = domChildren(ctx.cfg, unsigned(i));
for (uint32_t target : dom)
append(ctx.result, "b%u -> b%u;\n", unsigned(i), target);
// Join edges are all successor edges that do not strongly dominate
BlockIteratorWrapper succ = successors(ctx.cfg, unsigned(i));
for (uint32_t successor : succ)
{
bool found = false;
for (uint32_t target : dom)
{
if (target == successor)
{
found = true;
break;
}
}
if (!found)
append(ctx.result, "b%u -> b%u [style=dotted];\n", unsigned(i), successor);
}
}
append(ctx.result, "}\n");
return result;
}
std::string dumpDot(const IrFunction& function, bool includeInst)
{
std::string result = toDot(function, includeInst);

View File

@ -1165,25 +1165,17 @@ void IrLoweringA64::lowerInst(IrInst& inst, uint32_t index, IrBlock& next)
}
case IrCmd::INTERRUPT:
{
RegisterA64 temp = regs.allocTemp(KindA64::x);
regs.spill(build, index);
Label skip, next;
build.ldr(temp, mem(rState, offsetof(lua_State, global)));
build.ldr(temp, mem(temp, offsetof(global_State, cb.interrupt)));
build.cbz(temp, skip);
Label self;
size_t spills = regs.spill(build, index);
build.ldr(x0, mem(rState, offsetof(lua_State, global)));
build.ldr(x0, mem(x0, offsetof(global_State, cb.interrupt)));
build.cbnz(x0, self);
// Jump to outlined interrupt handler, it will give back control to x1
build.mov(x0, (uintOp(inst.a) + 1) * sizeof(Instruction));
build.adr(x1, next);
build.b(helpers.interrupt);
Label next = build.setLabel();
build.setLabel(next);
regs.restore(build, spills); // need to restore before skip so that registers are in a consistent state
build.setLabel(skip);
interruptHandlers.push_back({self, uintOp(inst.a), next});
break;
}
case IrCmd::CHECK_GC:
@ -1733,6 +1725,20 @@ void IrLoweringA64::finishBlock()
regs.assertNoSpills();
}
void IrLoweringA64::finishFunction()
{
if (build.logText)
build.logAppend("; interrupt handlers\n");
for (InterruptHandler& handler : interruptHandlers)
{
build.setLabel(handler.self);
build.mov(x0, (handler.pcpos + 1) * sizeof(Instruction));
build.adr(x1, handler.next);
build.b(helpers.interrupt);
}
}
bool IrLoweringA64::hasError() const
{
return error;

View File

@ -27,6 +27,7 @@ struct IrLoweringA64
void lowerInst(IrInst& inst, uint32_t index, IrBlock& next);
void finishBlock();
void finishFunction();
bool hasError() const;
@ -53,6 +54,13 @@ struct IrLoweringA64
IrBlock& blockOp(IrOp op) const;
Label& labelOp(IrOp op) const;
struct InterruptHandler
{
Label self;
unsigned int pcpos;
Label next;
};
AssemblyBuilderA64& build;
ModuleHelpers& helpers;
NativeState& data;
@ -63,6 +71,8 @@ struct IrLoweringA64
IrValueLocationTracking valueTracker;
std::vector<InterruptHandler> interruptHandlers;
bool error = false;
};

View File

@ -958,8 +958,27 @@ void IrLoweringX64::lowerInst(IrInst& inst, uint32_t index, IrBlock& next)
break;
}
case IrCmd::INTERRUPT:
emitInterrupt(regs, build, uintOp(inst.a));
{
unsigned pcpos = uintOp(inst.a);
// We unconditionally spill values here because that allows us to ignore register state when we synthesize interrupt handler
// This can be changed in the future if we can somehow record interrupt handler code separately
// Since interrupts are loop edges or call/ret, we don't have a significant opportunity for register reuse here anyway
regs.preserveAndFreeInstValues();
ScopedRegX64 tmp{regs, SizeX64::qword};
Label self;
build.mov(tmp.reg, qword[rState + offsetof(lua_State, global)]);
build.cmp(qword[tmp.reg + offsetof(global_State, cb.interrupt)], 0);
build.jcc(ConditionX64::NotEqual, self);
Label next = build.setLabel();
interruptHandlers.push_back({self, pcpos, next});
break;
}
case IrCmd::CHECK_GC:
callStepGc(regs, build);
break;
@ -991,7 +1010,6 @@ void IrLoweringX64::lowerInst(IrInst& inst, uint32_t index, IrBlock& next)
}
case IrCmd::SET_SAVEDPC:
{
// This is like emitSetSavedPc, but using register allocation instead of relying on rax/rdx
ScopedRegX64 tmp1{regs, SizeX64::qword};
ScopedRegX64 tmp2{regs, SizeX64::qword};
@ -1048,7 +1066,7 @@ void IrLoweringX64::lowerInst(IrInst& inst, uint32_t index, IrBlock& next)
case IrCmd::RETURN:
regs.assertAllFree();
regs.assertNoSpills();
emitInstReturn(build, helpers, vmRegOp(inst.a), intOp(inst.b));
emitInstReturn(build, helpers, vmRegOp(inst.a), intOp(inst.b), function.variadic);
break;
case IrCmd::FORGLOOP:
regs.assertAllFree();
@ -1350,6 +1368,20 @@ void IrLoweringX64::finishBlock()
regs.assertNoSpills();
}
void IrLoweringX64::finishFunction()
{
if (build.logText)
build.logAppend("; interrupt handlers\n");
for (InterruptHandler& handler : interruptHandlers)
{
build.setLabel(handler.self);
build.mov(rax, handler.pcpos + 1);
build.lea(rbx, handler.next);
build.jmp(helpers.interrupt);
}
}
bool IrLoweringX64::hasError() const
{
// If register allocator had to use more stack slots than we have available, this function can't run natively

View File

@ -29,6 +29,7 @@ struct IrLoweringX64
void lowerInst(IrInst& inst, uint32_t index, IrBlock& next);
void finishBlock();
void finishFunction();
bool hasError() const;
@ -53,6 +54,13 @@ struct IrLoweringX64
IrBlock& blockOp(IrOp op) const;
Label& labelOp(IrOp op) const;
struct InterruptHandler
{
Label self;
unsigned int pcpos;
Label next;
};
AssemblyBuilderX64& build;
ModuleHelpers& helpers;
NativeState& data;
@ -62,6 +70,8 @@ struct IrLoweringX64
IrRegAllocX64 regs;
IrValueLocationTracking valueTracker;
std::vector<InterruptHandler> interruptHandlers;
};
} // namespace X64

View File

@ -1059,16 +1059,21 @@ static void tryCreateLinearBlock(IrBuilder& build, std::vector<uint8_t>& visited
// TODO: using values from the first block can cause 'live out' of the linear block predecessor to not have all required registers
constPropInBlock(build, startingBlock, state);
// Veryfy that target hasn't changed
// Verify that target hasn't changed
LUAU_ASSERT(function.instructions[startingBlock.finish].a.index == targetBlockIdx);
// Note: using startingBlock after this line is unsafe as the reference may be reallocated by build.block() below
uint32_t startingInsn = startingBlock.start;
// Create new linearized block into which we are going to redirect starting block jump
IrOp newBlock = build.block(IrBlockKind::Linearized);
visited.push_back(false);
// TODO: placement of linear blocks in final lowering is sub-optimal, it should follow our predecessor
build.beginBlock(newBlock);
// By default, blocks are ordered according to start instruction; we alter sort order to make sure linearized block is placed right after the starting block
function.blocks[newBlock.index].sortkey = startingInsn + 1;
replace(function, termInst.a, newBlock);
// Clone the collected path into our fresh block

View File

@ -413,8 +413,10 @@ enum LuauBytecodeTag
{
// Bytecode version; runtime supports [MIN, MAX], compiler emits TARGET by default but may emit a higher version when flags are enabled
LBC_VERSION_MIN = 3,
LBC_VERSION_MAX = 3,
LBC_VERSION_MAX = 4,
LBC_VERSION_TARGET = 3,
// Type encoding version
LBC_TYPE_VERSION = 1,
// Types of constant table entries
LBC_CONSTANT_NIL = 0,
LBC_CONSTANT_BOOLEAN,
@ -425,6 +427,25 @@ enum LuauBytecodeTag
LBC_CONSTANT_CLOSURE,
};
// Type table tags
enum LuauBytecodeEncodedType
{
LBC_TYPE_NIL = 0,
LBC_TYPE_BOOLEAN,
LBC_TYPE_NUMBER,
LBC_TYPE_STRING,
LBC_TYPE_TABLE,
LBC_TYPE_FUNCTION,
LBC_TYPE_THREAD,
LBC_TYPE_USERDATA,
LBC_TYPE_VECTOR,
LBC_TYPE_ANY = 15,
LBC_TYPE_OPTIONAL_BIT = 1 << 7,
LBC_TYPE_INVALID = 256,
};
// Builtin function ids, used in LOP_FASTCALL
enum LuauBuiltinFunction
{

View File

@ -74,6 +74,8 @@ public:
void foldJumps();
void expandJumps();
void setFunctionTypeInfo(std::string value);
void setDebugFunctionName(StringRef name);
void setDebugFunctionLineDefined(int line);
void setDebugLine(int line);
@ -118,6 +120,7 @@ public:
std::string dumpFunction(uint32_t id) const;
std::string dumpEverything() const;
std::string dumpSourceRemarks() const;
std::string dumpTypeInfo() const;
void annotateInstruction(std::string& result, uint32_t fid, uint32_t instpos) const;
@ -132,6 +135,7 @@ public:
static std::string getError(const std::string& message);
static uint8_t getVersion();
static uint8_t getTypeEncodingVersion();
private:
struct Constant
@ -186,6 +190,7 @@ private:
std::string dump;
std::string dumpname;
std::vector<int> dumpinstoffs;
std::string typeinfo;
};
struct DebugLocal

View File

@ -6,6 +6,8 @@
#include <algorithm>
#include <string.h>
LUAU_FASTFLAGVARIABLE(BytecodeVersion4, false)
namespace Luau
{
@ -513,6 +515,11 @@ bool BytecodeBuilder::patchSkipC(size_t jumpLabel, size_t targetLabel)
return true;
}
void BytecodeBuilder::setFunctionTypeInfo(std::string value)
{
functions[currentFunction].typeinfo = std::move(value);
}
void BytecodeBuilder::setDebugFunctionName(StringRef name)
{
unsigned int index = addStringTableEntry(name);
@ -606,6 +613,13 @@ void BytecodeBuilder::finalize()
bytecode = char(version);
if (FFlag::BytecodeVersion4)
{
uint8_t typesversion = getTypeEncodingVersion();
LUAU_ASSERT(typesversion == 1);
writeByte(bytecode, typesversion);
}
writeStringTable(bytecode);
writeVarInt(bytecode, uint32_t(functions.size()));
@ -628,6 +642,14 @@ void BytecodeBuilder::writeFunction(std::string& ss, uint32_t id) const
writeByte(ss, func.numupvalues);
writeByte(ss, func.isvararg);
if (FFlag::BytecodeVersion4)
{
writeByte(ss, 0); // Reserved for cgflags
writeVarInt(ss, uint32_t(func.typeinfo.size()));
ss.append(func.typeinfo);
}
// instructions
writeVarInt(ss, uint32_t(insns.size()));
@ -1092,9 +1114,18 @@ std::string BytecodeBuilder::getError(const std::string& message)
uint8_t BytecodeBuilder::getVersion()
{
// This function usually returns LBC_VERSION_TARGET but may sometimes return a higher number (within LBC_VERSION_MIN/MAX) under fast flags
if (FFlag::BytecodeVersion4)
return 4;
return LBC_VERSION_TARGET;
}
uint8_t BytecodeBuilder::getTypeEncodingVersion()
{
return LBC_TYPE_VERSION;
}
#ifdef LUAU_ASSERTENABLED
void BytecodeBuilder::validate() const
{
@ -2269,6 +2300,75 @@ 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 "{ }";
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_ANY:
return "any";
}
LUAU_ASSERT(!"Unhandled type in getBaseTypeString");
return nullptr;
}
std::string BytecodeBuilder::dumpTypeInfo() const
{
std::string result;
for (size_t i = 0; i < functions.size(); ++i)
{
const std::string& typeinfo = functions[i].typeinfo;
if (typeinfo.empty())
continue;
uint8_t encodedType = typeinfo[0];
LUAU_ASSERT(encodedType == LBC_TYPE_FUNCTION);
formatAppend(result, "%zu: function(", i);
LUAU_ASSERT(typeinfo.size() >= 2);
uint8_t numparams = typeinfo[1];
LUAU_ASSERT(size_t(1 + numparams - 1) < typeinfo.size());
for (uint8_t i = 0; i < numparams; ++i)
{
uint8_t et = typeinfo[2 + i];
const char* optional = (et & LBC_TYPE_OPTIONAL_BIT) ? "?" : "";
formatAppend(result, "%s%s", getBaseTypeString(et), optional);
if (i + 1 != numparams)
formatAppend(result, ", ");
}
formatAppend(result, ")\n");
}
return result;
}
void BytecodeBuilder::annotateInstruction(std::string& result, uint32_t fid, uint32_t instpos) const
{
if ((dumpFlags & Dump_Code) == 0)

View File

@ -10,6 +10,7 @@
#include "ConstantFolding.h"
#include "CostModel.h"
#include "TableShape.h"
#include "Types.h"
#include "ValueTracking.h"
#include <algorithm>
@ -25,7 +26,8 @@ LUAU_FASTINTVARIABLE(LuauCompileInlineThreshold, 25)
LUAU_FASTINTVARIABLE(LuauCompileInlineThresholdMaxBoost, 300)
LUAU_FASTINTVARIABLE(LuauCompileInlineDepth, 5)
LUAU_FASTFLAGVARIABLE(LuauCompileInlineDefer, false)
LUAU_FASTFLAGVARIABLE(CompileFunctionType, false)
LUAU_FASTFLAG(BytecodeVersion4)
namespace Luau
{
@ -202,6 +204,13 @@ struct Compiler
setDebugLine(func);
if (FFlag::BytecodeVersion4 && FFlag::CompileFunctionType)
{
std::string funcType = getFunctionType(func);
if (!funcType.empty())
bytecode.setFunctionTypeInfo(std::move(funcType));
}
if (func->vararg)
bytecode.emitABC(LOP_PREPVARARGS, uint8_t(self + func->args.size), 0, 0);
@ -560,15 +569,7 @@ struct Compiler
size_t oldLocals = localStack.size();
std::vector<InlineArg> args;
if (FFlag::LuauCompileInlineDefer)
{
args.reserve(func->args.size);
}
else
{
// note that we push the frame early; this is needed to block recursive inline attempts
inlineFrames.push_back({func, oldLocals, target, targetCount});
}
// evaluate all arguments; note that we don't emit code for constant arguments (relying on constant folding)
// note that compiler state (variable registers/values) does not change here - we defer that to a separate loop below to handle nested calls
@ -590,16 +591,8 @@ struct Compiler
else
LUAU_ASSERT(!"Unexpected expression type");
if (FFlag::LuauCompileInlineDefer)
{
for (size_t j = i; j < func->args.size; ++j)
args.push_back({func->args.data[j], uint8_t(reg + (j - i))});
}
else
{
for (size_t j = i; j < func->args.size; ++j)
pushLocal(func->args.data[j], uint8_t(reg + (j - i)));
}
// all remaining function arguments have been allocated and assigned to
break;
@ -614,26 +607,17 @@ struct Compiler
else
bytecode.emitABC(LOP_LOADNIL, reg, 0, 0);
if (FFlag::LuauCompileInlineDefer)
args.push_back({var, reg});
else
pushLocal(var, reg);
}
else if (arg == nullptr)
{
// since the argument is not mutated, we can simply fold the value into the expressions that need it
if (FFlag::LuauCompileInlineDefer)
args.push_back({var, kInvalidReg, {Constant::Type_Nil}});
else
locstants[var] = {Constant::Type_Nil};
}
else if (const Constant* cv = constants.find(arg); cv && cv->type != Constant::Type_Unknown)
{
// since the argument is not mutated, we can simply fold the value into the expressions that need it
if (FFlag::LuauCompileInlineDefer)
args.push_back({var, kInvalidReg, *cv});
else
locstants[var] = *cv;
}
else
{
@ -643,20 +627,14 @@ 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))
{
if (FFlag::LuauCompileInlineDefer)
args.push_back({var, uint8_t(reg)});
else
pushLocal(var, uint8_t(reg));
}
else
{
uint8_t temp = allocReg(arg, 1);
compileExprTemp(arg, temp);
if (FFlag::LuauCompileInlineDefer)
args.push_back({var, temp});
else
pushLocal(var, temp);
}
}
}
@ -668,8 +646,6 @@ struct Compiler
compileExprAuto(expr->args.data[i], rsi);
}
if (FFlag::LuauCompileInlineDefer)
{
// apply all evaluated arguments to the compiler state
// note: locals use current startpc for debug info, although some of them have been computed earlier; this is similar to compileStatLocal
for (InlineArg& arg : args)
@ -680,7 +656,6 @@ struct Compiler
// the inline frame will be used to compile return statements as well as to reject recursive inlining attempts
inlineFrames.push_back({func, oldLocals, target, targetCount});
}
// fold constant values updated above into expressions in the function body
foldConstants(constants, variables, locstants, builtinsFold, func->body);

106
Compiler/src/Types.cpp Normal file
View File

@ -0,0 +1,106 @@
// This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details
#include "Luau/BytecodeBuilder.h"
#include "Types.h"
namespace Luau
{
static LuauBytecodeEncodedType getType(AstType* ty)
{
if (AstTypeReference* ref = ty->as<AstTypeReference>())
{
if (ref->name == "nil")
return LBC_TYPE_NIL;
else if (ref->name == "boolean")
return LBC_TYPE_BOOLEAN;
else if (ref->name == "number")
return LBC_TYPE_NUMBER;
else if (ref->name == "string")
return LBC_TYPE_STRING;
else if (ref->name == "thread")
return LBC_TYPE_THREAD;
else if (ref->name == "any" || ref->name == "unknown")
return LBC_TYPE_ANY;
}
else if (AstTypeTable* table = ty->as<AstTypeTable>())
{
return LBC_TYPE_TABLE;
}
else if (AstTypeFunction* func = ty->as<AstTypeFunction>())
{
return LBC_TYPE_FUNCTION;
}
else if (AstTypeUnion* un = ty->as<AstTypeUnion>())
{
bool optional = false;
LuauBytecodeEncodedType type = LBC_TYPE_INVALID;
for (AstType* ty : un->types)
{
LuauBytecodeEncodedType et = getType(ty);
if (et == LBC_TYPE_NIL)
{
optional = true;
continue;
}
if (type == LBC_TYPE_INVALID)
{
type = et;
continue;
}
if (type != et)
return LBC_TYPE_ANY;
}
if (type == LBC_TYPE_INVALID)
return LBC_TYPE_ANY;
return LuauBytecodeEncodedType(type | (optional && (type != LBC_TYPE_ANY) ? LBC_TYPE_OPTIONAL_BIT : 0));
}
else if (AstTypeIntersection* inter = ty->as<AstTypeIntersection>())
{
return LBC_TYPE_ANY;
}
return LBC_TYPE_ANY;
}
std::string getFunctionType(const AstExprFunction* func)
{
if (func->vararg || func->generics.size || func->genericPacks.size)
return {};
bool self = func->self != 0;
std::string typeInfo;
typeInfo.reserve(func->args.size + self + 2);
typeInfo.push_back(LBC_TYPE_FUNCTION);
typeInfo.push_back(uint8_t(self + func->args.size));
if (self)
typeInfo.push_back(LBC_TYPE_TABLE);
bool haveNonAnyParam = false;
for (AstLocal* arg : func->args)
{
LuauBytecodeEncodedType ty = arg->annotation ? getType(arg->annotation) : LBC_TYPE_ANY;
if (ty != LBC_TYPE_ANY)
haveNonAnyParam = true;
typeInfo.push_back(ty);
}
// If all parameters simplify to any, we can just omit type info for this function
if (!haveNonAnyParam)
return {};
return typeInfo;
}
} // namespace Luau

9
Compiler/src/Types.h Normal file
View File

@ -0,0 +1,9 @@
// This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details
#pragma once
#include "Luau/Ast.h"
namespace Luau
{
std::string getFunctionType(const AstExprFunction* func);
} // namespace Luau

View File

@ -43,6 +43,7 @@ target_sources(Luau.Compiler PRIVATE
Compiler/src/ConstantFolding.cpp
Compiler/src/CostModel.cpp
Compiler/src/TableShape.cpp
Compiler/src/Types.cpp
Compiler/src/ValueTracking.cpp
Compiler/src/lcode.cpp
Compiler/src/Builtins.h
@ -50,6 +51,7 @@ target_sources(Luau.Compiler PRIVATE
Compiler/src/ConstantFolding.h
Compiler/src/CostModel.h
Compiler/src/TableShape.h
Compiler/src/Types.h
Compiler/src/ValueTracking.h
)

View File

@ -24,7 +24,6 @@ LUAI_FUNC void luaV_gettable(lua_State* L, const TValue* t, TValue* key, StkId v
LUAI_FUNC void luaV_settable(lua_State* L, const TValue* t, TValue* key, StkId val);
LUAI_FUNC void luaV_concat(lua_State* L, int total, int last);
LUAI_FUNC void luaV_getimport(lua_State* L, Table* env, TValue* k, StkId res, uint32_t id, bool propagatenil);
LUAI_FUNC void luaV_getimport_dep(lua_State* L, Table* env, TValue* k, uint32_t id, bool propagatenil);
LUAI_FUNC void luaV_prepareFORN(lua_State* L, StkId plimit, StkId pstep, StkId pinit);
LUAI_FUNC void luaV_callTM(lua_State* L, int nparams, int res);
LUAI_FUNC void luaV_tryfuncTM(lua_State* L, StkId func);

View File

@ -16,8 +16,6 @@
#include <string.h>
LUAU_FASTFLAG(LuauGetImportDirect)
// Disable c99-designator to avoid the warning in CGOTO dispatch table
#ifdef __clang__
#if __has_warning("-Wc99-designator")
@ -432,21 +430,9 @@ reentry:
{
uint32_t aux = *pc++;
if (FFlag::LuauGetImportDirect)
{
VM_PROTECT(luaV_getimport(L, cl->env, k, ra, aux, /* propagatenil= */ false));
VM_NEXT();
}
else
{
VM_PROTECT(luaV_getimport_dep(L, cl->env, k, aux, /* propagatenil= */ false));
ra = VM_REG(LUAU_INSN_A(insn)); // previous call may change the stack
setobj2s(L, ra, L->top - 1);
L->top--;
VM_NEXT();
}
}
}
VM_CASE(LOP_GETTABLEKS)

View File

@ -13,8 +13,6 @@
#include <string.h>
LUAU_FASTFLAGVARIABLE(LuauGetImportDirect, false)
// TODO: RAII deallocation doesn't work for longjmp builds if a memory error happens
template<typename T>
struct TempBuffer
@ -77,34 +75,6 @@ void luaV_getimport(lua_State* L, Table* env, TValue* k, StkId res, uint32_t id,
luaV_gettable(L, res, &k[id2], res);
}
void luaV_getimport_dep(lua_State* L, Table* env, TValue* k, uint32_t id, bool propagatenil)
{
LUAU_ASSERT(!FFlag::LuauGetImportDirect);
int count = id >> 30;
int id0 = count > 0 ? int(id >> 20) & 1023 : -1;
int id1 = count > 1 ? int(id >> 10) & 1023 : -1;
int id2 = count > 2 ? int(id) & 1023 : -1;
// allocate a stack slot so that we can do table lookups
luaD_checkstack(L, 1);
setnilvalue(L->top);
L->top++;
// global lookup into L->top-1
TValue g;
sethvalue(L, &g, env);
luaV_gettable(L, &g, &k[id0], L->top - 1);
// table lookup for id1
if (id1 >= 0 && (!propagatenil || !ttisnil(L->top - 1)))
luaV_gettable(L, L->top - 1, &k[id1], L->top - 1);
// table lookup for id2
if (id2 >= 0 && (!propagatenil || !ttisnil(L->top - 1)))
luaV_gettable(L, L->top - 1, &k[id2], L->top - 1);
}
template<typename T>
static T read(const char* data, size_t size, size_t& offset)
{
@ -153,8 +123,6 @@ static void resolveImportSafe(lua_State* L, Table* env, TValue* k, uint32_t id)
// note: we call getimport with nil propagation which means that accesses to table chains like A.B.C will resolve in nil
// this is technically not necessary but it reduces the number of exceptions when loading scripts that rely on getfenv/setfenv for global
// injection
if (FFlag::LuauGetImportDirect)
{
// allocate a stack slot so that we can do table lookups
luaD_checkstack(L, 1);
setnilvalue(L->top);
@ -162,9 +130,6 @@ static void resolveImportSafe(lua_State* L, Table* env, TValue* k, uint32_t id)
luaV_getimport(L, L->gt, self->k, L->top - 1, self->id, /* propagatenil= */ true);
}
else
luaV_getimport_dep(L, L->gt, self->k, self->id, /* propagatenil= */ true);
}
};
ResolveImport ri = {k, id};
@ -194,6 +159,8 @@ int luau_load(lua_State* L, const char* chunkname, const char* data, size_t size
uint8_t version = read<uint8_t>(data, size, offset);
// 0 means the rest of the bytecode is the error message
if (version == 0)
{
@ -221,6 +188,13 @@ int luau_load(lua_State* L, const char* chunkname, const char* data, size_t size
TString* source = luaS_new(L, chunkname);
if (version >= 4)
{
uint8_t typesversion = read<uint8_t>(data, size, offset);
LUAU_ASSERT(typesversion == 1);
}
// string table
unsigned int stringCount = readVarInt(data, size, offset);
TempBuffer<TString*> strings(L, stringCount);
@ -248,6 +222,25 @@ int luau_load(lua_State* L, const char* chunkname, const char* data, size_t size
p->nups = read<uint8_t>(data, size, offset);
p->is_vararg = read<uint8_t>(data, size, offset);
if (version >= 4)
{
uint8_t cgflags = read<uint8_t>(data, size, offset);
LUAU_ASSERT(cgflags == 0);
uint32_t typesize = readVarInt(data, size, offset);
if (typesize)
{
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);
offset += typesize;
}
}
p->sizecode = readVarInt(data, size, offset);
p->code = luaM_newarray(L, p->sizecode, Instruction, p->memcat);
for (int j = 0; j < p->sizecode; ++j)

View File

@ -73,7 +73,7 @@ def arrayRangeOffset(count, offset):
return result
def getCallgrindOutput(lines):
def getCallgrindOutput(stdout, lines):
result = []
name = None
@ -86,12 +86,36 @@ def getCallgrindOutput(lines):
result += "|><|" + name + "|><|" + str(insn / CALLGRIND_INSN_PER_SEC * 1000.0) + "||_||"
name = None
# If no results were found above, this may indicate the native executable running
# the benchmark doesn't have support for callgrind builtin. In that case just
# report the "totals" from the output file.
if len(result) == 0:
elements = stdout.decode('utf8').split("|><|")
if len(elements) >= 2:
name = elements[1]
for l in lines:
if l.startswith("totals: "):
insn = int(l[8:])
# Note: we only run each bench once under callgrind so we only report a single time per run; callgrind instruction count variance is ~0.01% so it might as well be zero
result += "|><|" + name + "|><|" + str(insn / CALLGRIND_INSN_PER_SEC * 1000.0) + "||_||"
return "".join(result)
def conditionallyShowCommand(cmd):
if arguments.show_commands:
print(f'{colored(Color.BLUE, "EXECUTING")}: {cmd}')
def checkValgrindExecutable():
"""Return true if valgrind can be successfully spawned"""
try:
subprocess.check_call("valgrind --version", shell=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
except:
print(f"{colored(Color.YELLOW, 'WARNING')}: Unable to spawn 'valgrind'. Please ensure valgrind is installed when using '--callgrind'.")
return False
return True
def getVmOutput(cmd):
if os.name == "nt":
try:
@ -103,17 +127,24 @@ def getVmOutput(cmd):
except:
return ""
elif arguments.callgrind:
if not checkValgrindExecutable():
return ""
output_path = os.path.join(scriptdir, "callgrind.out")
try:
os.unlink(output_path) # Remove stale output
except:
pass
fullCmd = "valgrind --tool=callgrind --callgrind-out-file=callgrind.out --combine-dumps=yes --dump-line=no " + cmd
conditionallyShowCommand(fullCmd)
subprocess.check_call(fullCmd, shell=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, cwd=scriptdir)
path = os.path.join(scriptdir, "callgrind.out")
with open(path, "r") as file:
try:
output = subprocess.check_output(fullCmd, shell=True, stderr=subprocess.DEVNULL, cwd=scriptdir)
except subprocess.CalledProcessError as e:
print(f"{colored(Color.YELLOW, 'WARNING')}: Valgrind returned error code {e.returncode}")
output = e.output
with open(output_path, "r") as file:
lines = file.readlines()
os.unlink(path)
return getCallgrindOutput(lines)
except:
return ""
os.unlink(output_path)
return getCallgrindOutput(output, lines)
else:
conditionallyShowCommand(cmd)
with subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True, cwd=scriptdir) as p:
@ -352,7 +383,7 @@ def analyzeResult(subdir, main, comparisons):
if influxReporter != None:
influxReporter.report_result(subdir, main.name, main.filename, "SUCCESS", main.min, main.avg, main.max, main.sampleConfidenceInterval, main.shortVm, main.vm)
print(colored(Color.YELLOW, 'SUCCESS') + ': {:<40}'.format(main.name) + ": " + '{:8.3f}'.format(main.avg) + "ms +/- " +
print(colored(Color.GREEN, 'SUCCESS') + ': {:<40}'.format(main.name) + ": " + '{:8.3f}'.format(main.avg) + "ms +/- " +
'{:6.3f}'.format(main.sampleConfidenceInterval / main.avg * 100) + "% on " + main.shortVm)
plotLabels.append(main.name)
@ -449,7 +480,7 @@ def analyzeResult(subdir, main, comparisons):
'P(T<=t)': '---' if pValue < 0 else '{:.0f}%'.format(pValue * 100)
})
print(colored(Color.YELLOW, 'SUCCESS') + ': {:<40}'.format(main.name) + ": " + '{:8.3f}'.format(compare.avg) + "ms +/- " +
print(colored(Color.GREEN, 'SUCCESS') + ': {:<40}'.format(main.name) + ": " + '{:8.3f}'.format(compare.avg) + "ms +/- " +
'{:6.3f}'.format(compare.sampleConfidenceInterval / compare.avg * 100) + "% on " + compare.shortVm +
' ({:+7.3f}%, '.format(speedup * 100) + verdict + ")")
@ -727,6 +758,10 @@ def run(args, argsubcb):
arguments = args
argumentSubstituionCallback = argsubcb
if os.name == "nt" and arguments.callgrind:
print(f"{colored(Color.RED, 'ERROR')}: --callgrind is not supported on Windows. Please consider using this option on another OS, or Linux using WSL.")
sys.exit(1)
if arguments.report_metrics or arguments.print_influx_debugging:
import influxbench
influxReporter = influxbench.InfluxReporter(arguments)

View File

@ -57,4 +57,46 @@ function bench.runCode(f, description)
print(report)
end
-- This function acts a bit like a Unix "fork" operation
-- When it is first called it clones `scriptInstance` and starts executing
-- the cloned script parented to an Actor. When the cloned script calls "runScriptCodeUnderActor"
-- it will run 'f' and print out the provided 'description'.
--
-- The function returns 'true' if it was invoked from a script running under an Actor
-- and 'false' otherwise.
--
-- Example usage:
-- local bench = script and require(script.Parent.bench_support) or require("bench_support")
-- function testFunc()
-- ...
-- end
-- bench.runScriptCodeUnderActor(script, testFunc, "test function")
function bench.runScriptCodeUnderActor(scriptInstance, f, description)
if scriptInstance:GetActor() then
-- If this function was called from an Actor script, just run the function provided using runCode
bench.runCode(f, description)
return true
else
-- If this function was not called from an Actor script, clone the script and place it under
-- Actor instance.
-- Create an Actor to run the script under
local actor = Instance.new("Actor")
-- Clone this script (i.e. the bench_support module) and place it under the Actor where
-- the script script would expect it to be when using 'require'.
local benchModule = script:Clone()
benchModule.Parent = actor
-- Clone the scriptInstance
local actorScript = scriptInstance:Clone()
-- Enable the script since `scriptInstance` may be started by roblox-cli without ever being enabled.
actorScript.Disabled = false
actorScript.Parent = actor
-- Add the actor to the workspace which will start executing the cloned script.
-- Note: the script needs to be placed under a instance that implements 'IScriptFilter'
-- (which workspace does) or it will never start executing.
actor.Parent = workspace
return false
end
end
return bench

View File

@ -542,6 +542,20 @@ TEST_CASE_FIXTURE(AssemblyBuilderX64Fixture, "MiscInstructions")
SINGLE_COMPARE(bsf(eax, edx), 0x0f, 0xbc, 0xc2);
}
TEST_CASE_FIXTURE(AssemblyBuilderX64Fixture, "LabelLea")
{
CHECK(check(
[](AssemblyBuilderX64& build) {
Label fn;
build.lea(rax, fn);
build.ret();
build.setLabel(fn);
build.ret();
},
{0x48, 0x8d, 0x05, 0x01, 0x00, 0x00, 0x00, 0xc3, 0xc3}));
}
TEST_CASE("LogTest")
{
AssemblyBuilderX64 build(/* logText= */ true);
@ -561,6 +575,7 @@ TEST_CASE("LogTest")
Label start = build.setLabel();
build.cmp(rsi, rdi);
build.jcc(ConditionX64::Equal, start);
build.lea(rcx, start);
build.jmp(qword[rdx]);
build.vaddps(ymm9, ymm12, ymmword[rbp + 0xc]);
@ -605,6 +620,7 @@ TEST_CASE("LogTest")
.L1:
cmp rsi,rdi
je .L1
lea rcx,.L1
jmp qword ptr [rdx]
vaddps ymm9,ymm12,ymmword ptr [rbp+0Ch]
vaddpd ymm2,ymm7,qword ptr [.start-8]

View File

@ -49,6 +49,15 @@ static std::string compileFunction0Coverage(const char* source, int level)
return bcb.dumpFunction(0);
}
static std::string compileFunction0TypeTable(const char* source)
{
Luau::BytecodeBuilder bcb;
bcb.setDumpFlags(Luau::BytecodeBuilder::Dump_Code);
Luau::compileOrThrow(bcb, source);
return bcb.dumpTypeInfo();
}
TEST_SUITE_BEGIN("Compiler");
TEST_CASE("CompileToBytecode")
@ -5796,8 +5805,6 @@ RETURN R3 1
TEST_CASE("InlineRecurseArguments")
{
ScopedFastFlag sff("LuauCompileInlineDefer", true);
// the example looks silly but we preserve it verbatim as it was found by fuzzer for a previous version of the compiler
CHECK_EQ("\n" + compileFunction(R"(
local function foo(a, b)
@ -7071,4 +7078,56 @@ L1: RETURN R3 1
)");
}
TEST_CASE("EncodedTypeTable")
{
ScopedFastFlag sffs[] = {
{"BytecodeVersion4", true},
{"CompileFunctionType", true},
};
CHECK_EQ("\n" + compileFunction0TypeTable(R"(
function myfunc(test: string, num: number)
print(test)
end
function myfunc2(test: number?)
end
function myfunc3(test: string, n: number)
end
function myfunc4(test: string | number, n: number)
end
-- Promoted to function(any, any) since general unions are not supported.
-- Functions with all `any` parameters will have omitted type info.
function myfunc5(test: string | number, n: number | boolean)
end
myfunc('test')
)"),
R"(
0: function(string, number)
1: function(number?)
2: function(string, number)
3: function(any, number)
)");
CHECK_EQ("\n" + compileFunction0TypeTable(R"(
local Str = {
a = 1
}
-- Implicit `self` parameter is automatically assumed to be table type.
function Str:test(n: number)
print(self.a, n)
end
Str:test(234)
)"),
R"(
0: function({ }, number)
)");
}
TEST_SUITE_END();

View File

@ -341,7 +341,7 @@ TEST_CASE_FIXTURE(SimplifyFixture, "tables")
CHECK(t2 == intersect(t2, t1));
TypeId t3 = mkTable({});
// {tag : string} intersect {{}}
CHECK(t1 == intersect(t1, t3));
CHECK(t1 == intersect(t3, t1));
}