luau/Compiler/src/BytecodeBuilder.cpp
2023-05-05 12:57:12 -07:00

2294 lines
65 KiB
C++

// 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 "Luau/StringUtils.h"
#include <algorithm>
#include <string.h>
namespace Luau
{
static_assert(LBC_VERSION_TARGET >= LBC_VERSION_MIN && LBC_VERSION_TARGET <= LBC_VERSION_MAX, "Invalid bytecode version setup");
static_assert(LBC_VERSION_MAX <= 127, "Bytecode version should be 7-bit so that we can extend the serialization to use varint transparently");
static const uint32_t kMaxConstantCount = 1 << 23;
static const uint32_t kMaxClosureCount = 1 << 15;
static const int kMaxJumpDistance = 1 << 23;
static int log2(int v)
{
LUAU_ASSERT(v);
int r = 0;
while (v >= (2 << r))
r++;
return r;
}
static void writeByte(std::string& ss, unsigned char value)
{
ss.append(reinterpret_cast<const char*>(&value), sizeof(value));
}
static void writeInt(std::string& ss, int value)
{
ss.append(reinterpret_cast<const char*>(&value), sizeof(value));
}
static void writeDouble(std::string& ss, double value)
{
ss.append(reinterpret_cast<const char*>(&value), sizeof(value));
}
static void writeVarInt(std::string& ss, unsigned int value)
{
do
{
writeByte(ss, (value & 127) | ((value > 127) << 7));
value >>= 7;
} while (value);
}
static int getOpLength(LuauOpcode op)
{
switch (op)
{
case LOP_GETGLOBAL:
case LOP_SETGLOBAL:
case LOP_GETIMPORT:
case LOP_GETTABLEKS:
case LOP_SETTABLEKS:
case LOP_NAMECALL:
case LOP_JUMPIFEQ:
case LOP_JUMPIFLE:
case LOP_JUMPIFLT:
case LOP_JUMPIFNOTEQ:
case LOP_JUMPIFNOTLE:
case LOP_JUMPIFNOTLT:
case LOP_NEWTABLE:
case LOP_SETLIST:
case LOP_FORGLOOP:
case LOP_LOADKX:
case LOP_FASTCALL2:
case LOP_FASTCALL2K:
case LOP_JUMPXEQKNIL:
case LOP_JUMPXEQKB:
case LOP_JUMPXEQKN:
case LOP_JUMPXEQKS:
return 2;
default:
return 1;
}
}
inline bool isJumpD(LuauOpcode op)
{
switch (op)
{
case LOP_JUMP:
case LOP_JUMPIF:
case LOP_JUMPIFNOT:
case LOP_JUMPIFEQ:
case LOP_JUMPIFLE:
case LOP_JUMPIFLT:
case LOP_JUMPIFNOTEQ:
case LOP_JUMPIFNOTLE:
case LOP_JUMPIFNOTLT:
case LOP_FORNPREP:
case LOP_FORNLOOP:
case LOP_FORGPREP:
case LOP_FORGLOOP:
case LOP_FORGPREP_INEXT:
case LOP_FORGPREP_NEXT:
case LOP_JUMPBACK:
case LOP_JUMPXEQKNIL:
case LOP_JUMPXEQKB:
case LOP_JUMPXEQKN:
case LOP_JUMPXEQKS:
return true;
default:
return false;
}
}
inline bool isSkipC(LuauOpcode op)
{
switch (op)
{
case LOP_LOADB:
return true;
default:
return false;
}
}
inline bool isFastCall(LuauOpcode op)
{
switch (op)
{
case LOP_FASTCALL:
case LOP_FASTCALL1:
case LOP_FASTCALL2:
case LOP_FASTCALL2K:
return true;
default:
return false;
}
}
static int getJumpTarget(uint32_t insn, uint32_t pc)
{
LuauOpcode op = LuauOpcode(LUAU_INSN_OP(insn));
if (isJumpD(op))
return int(pc + LUAU_INSN_D(insn) + 1);
else if (isFastCall(op))
return int(pc + LUAU_INSN_C(insn) + 2);
else if (isSkipC(op) && LUAU_INSN_C(insn))
return int(pc + LUAU_INSN_C(insn) + 1);
else if (op == LOP_JUMPX)
return int(pc + LUAU_INSN_E(insn) + 1);
else
return -1;
}
bool BytecodeBuilder::StringRef::operator==(const StringRef& other) const
{
return (data && other.data) ? (length == other.length && memcmp(data, other.data, length) == 0) : (data == other.data);
}
bool BytecodeBuilder::TableShape::operator==(const TableShape& other) const
{
return length == other.length && memcmp(keys, other.keys, length * sizeof(keys[0])) == 0;
}
size_t BytecodeBuilder::StringRefHash::operator()(const StringRef& v) const
{
return hashRange(v.data, v.length);
}
size_t BytecodeBuilder::ConstantKeyHash::operator()(const ConstantKey& key) const
{
// finalizer from MurmurHash64B
const uint32_t m = 0x5bd1e995;
uint32_t h1 = uint32_t(key.value);
uint32_t h2 = uint32_t(key.value >> 32) ^ (key.type * m);
h1 ^= h2 >> 18;
h1 *= m;
h2 ^= h1 >> 22;
h2 *= m;
h1 ^= h2 >> 17;
h1 *= m;
h2 ^= h1 >> 19;
h2 *= m;
// ... truncated to 32-bit output (normally hash is equal to (uint64_t(h1) << 32) | h2, but we only really need the lower 32-bit half)
return size_t(h2);
}
size_t BytecodeBuilder::TableShapeHash::operator()(const TableShape& v) const
{
// FNV-1a inspired hash (note that we feed integers instead of bytes)
uint32_t hash = 2166136261;
for (size_t i = 0; i < v.length; ++i)
{
hash ^= v.keys[i];
hash *= 16777619;
}
return hash;
}
BytecodeBuilder::BytecodeBuilder(BytecodeEncoder* encoder)
: constantMap({Constant::Type_Nil, ~0ull})
, tableShapeMap(TableShape())
, protoMap(~0u)
, stringTable({nullptr, 0})
, encoder(encoder)
{
LUAU_ASSERT(stringTable.find(StringRef{"", 0}) == nullptr);
// preallocate some buffers that are very likely to grow anyway; this works around std::vector's inefficient growth policy for small arrays
insns.reserve(32);
lines.reserve(32);
constants.reserve(16);
protos.reserve(16);
functions.reserve(8);
}
uint32_t BytecodeBuilder::beginFunction(uint8_t numparams, bool isvararg)
{
LUAU_ASSERT(currentFunction == ~0u);
uint32_t id = uint32_t(functions.size());
Function func;
func.numparams = numparams;
func.isvararg = isvararg;
functions.push_back(func);
currentFunction = id;
hasLongJumps = false;
debugLine = 0;
return id;
}
void BytecodeBuilder::endFunction(uint8_t maxstacksize, uint8_t numupvalues)
{
LUAU_ASSERT(currentFunction != ~0u);
Function& func = functions[currentFunction];
func.maxstacksize = maxstacksize;
func.numupvalues = numupvalues;
#ifdef LUAU_ASSERTENABLED
validate();
#endif
// very approximate: 4 bytes per instruction for code, 1 byte for debug line, and 1-2 bytes for aux data like constants plus overhead
func.data.reserve(32 + insns.size() * 7);
writeFunction(func.data, currentFunction);
currentFunction = ~0u;
// this call is indirect to make sure we only gain link time dependency on dumpCurrentFunction when needed
if (dumpFunctionPtr)
func.dump = (this->*dumpFunctionPtr)(func.dumpinstoffs);
insns.clear();
lines.clear();
constants.clear();
protos.clear();
jumps.clear();
tableShapes.clear();
debugLocals.clear();
debugUpvals.clear();
constantMap.clear();
tableShapeMap.clear();
protoMap.clear();
debugRemarks.clear();
debugRemarkBuffer.clear();
}
void BytecodeBuilder::setMainFunction(uint32_t fid)
{
LUAU_ASSERT(fid < functions.size());
mainFunction = fid;
}
int32_t BytecodeBuilder::addConstant(const ConstantKey& key, const Constant& value)
{
if (int32_t* cache = constantMap.find(key))
return *cache;
uint32_t id = uint32_t(constants.size());
if (id >= kMaxConstantCount)
return -1;
constantMap[key] = int32_t(id);
constants.push_back(value);
return int32_t(id);
}
unsigned int BytecodeBuilder::addStringTableEntry(StringRef value)
{
unsigned int& index = stringTable[value];
// note: bytecode serialization format uses 1-based table indices, 0 is reserved to mean nil
if (index == 0)
{
index = uint32_t(stringTable.size());
if ((dumpFlags & Dump_Code) != 0)
debugStrings.push_back(value);
}
return index;
}
int32_t BytecodeBuilder::addConstantNil()
{
Constant c = {Constant::Type_Nil};
ConstantKey k = {Constant::Type_Nil};
return addConstant(k, c);
}
int32_t BytecodeBuilder::addConstantBoolean(bool value)
{
Constant c = {Constant::Type_Boolean};
c.valueBoolean = value;
ConstantKey k = {Constant::Type_Boolean, value};
return addConstant(k, c);
}
int32_t BytecodeBuilder::addConstantNumber(double value)
{
Constant c = {Constant::Type_Number};
c.valueNumber = value;
ConstantKey k = {Constant::Type_Number};
static_assert(sizeof(k.value) == sizeof(value), "Expecting double to be 64-bit");
memcpy(&k.value, &value, sizeof(value));
return addConstant(k, c);
}
int32_t BytecodeBuilder::addConstantString(StringRef value)
{
unsigned int index = addStringTableEntry(value);
Constant c = {Constant::Type_String};
c.valueString = index;
ConstantKey k = {Constant::Type_String, index};
return addConstant(k, c);
}
int32_t BytecodeBuilder::addImport(uint32_t iid)
{
Constant c = {Constant::Type_Import};
c.valueImport = iid;
ConstantKey k = {Constant::Type_Import, iid};
return addConstant(k, c);
}
int32_t BytecodeBuilder::addConstantTable(const TableShape& shape)
{
if (int32_t* cache = tableShapeMap.find(shape))
return *cache;
uint32_t id = uint32_t(constants.size());
if (id >= kMaxConstantCount)
return -1;
Constant value = {Constant::Type_Table};
value.valueTable = uint32_t(tableShapes.size());
tableShapeMap[shape] = int32_t(id);
tableShapes.push_back(shape);
constants.push_back(value);
return int32_t(id);
}
int32_t BytecodeBuilder::addConstantClosure(uint32_t fid)
{
Constant c = {Constant::Type_Closure};
c.valueClosure = fid;
ConstantKey k = {Constant::Type_Closure, fid};
return addConstant(k, c);
}
int16_t BytecodeBuilder::addChildFunction(uint32_t fid)
{
if (int16_t* cache = protoMap.find(fid))
return *cache;
uint32_t id = uint32_t(protos.size());
if (id >= kMaxClosureCount)
return -1;
protoMap[fid] = int16_t(id);
protos.push_back(fid);
return int16_t(id);
}
void BytecodeBuilder::emitABC(LuauOpcode op, uint8_t a, uint8_t b, uint8_t c)
{
uint32_t insn = uint32_t(op) | (a << 8) | (b << 16) | (c << 24);
insns.push_back(insn);
lines.push_back(debugLine);
}
void BytecodeBuilder::emitAD(LuauOpcode op, uint8_t a, int16_t d)
{
uint32_t insn = uint32_t(op) | (a << 8) | (uint16_t(d) << 16);
insns.push_back(insn);
lines.push_back(debugLine);
}
void BytecodeBuilder::emitE(LuauOpcode op, int32_t e)
{
uint32_t insn = uint32_t(op) | (uint32_t(e) << 8);
insns.push_back(insn);
lines.push_back(debugLine);
}
void BytecodeBuilder::emitAux(uint32_t aux)
{
insns.push_back(aux);
lines.push_back(debugLine);
}
size_t BytecodeBuilder::emitLabel()
{
return insns.size();
}
bool BytecodeBuilder::patchJumpD(size_t jumpLabel, size_t targetLabel)
{
LUAU_ASSERT(jumpLabel < insns.size());
unsigned int jumpInsn = insns[jumpLabel];
(void)jumpInsn;
LUAU_ASSERT(isJumpD(LuauOpcode(LUAU_INSN_OP(jumpInsn))));
LUAU_ASSERT(LUAU_INSN_D(jumpInsn) == 0);
LUAU_ASSERT(targetLabel <= insns.size());
int offset = int(targetLabel) - int(jumpLabel) - 1;
if (int16_t(offset) == offset)
{
insns[jumpLabel] |= uint16_t(offset) << 16;
}
else if (abs(offset) < kMaxJumpDistance)
{
// our jump doesn't fit into 16 bits; we will need to repatch the bytecode sequence with jump trampolines, see expandJumps
hasLongJumps = true;
}
else
{
return false;
}
jumps.push_back({uint32_t(jumpLabel), uint32_t(targetLabel)});
return true;
}
bool BytecodeBuilder::patchSkipC(size_t jumpLabel, size_t targetLabel)
{
LUAU_ASSERT(jumpLabel < insns.size());
unsigned int jumpInsn = insns[jumpLabel];
(void)jumpInsn;
LUAU_ASSERT(isSkipC(LuauOpcode(LUAU_INSN_OP(jumpInsn))) || isFastCall(LuauOpcode(LUAU_INSN_OP(jumpInsn))));
LUAU_ASSERT(LUAU_INSN_C(jumpInsn) == 0);
int offset = int(targetLabel) - int(jumpLabel) - 1;
if (uint8_t(offset) != offset)
{
return false;
}
insns[jumpLabel] |= offset << 24;
return true;
}
void BytecodeBuilder::setDebugFunctionName(StringRef name)
{
unsigned int index = addStringTableEntry(name);
functions[currentFunction].debugname = index;
if (dumpFunctionPtr)
functions[currentFunction].dumpname = std::string(name.data, name.length);
}
void BytecodeBuilder::setDebugFunctionLineDefined(int line)
{
functions[currentFunction].debuglinedefined = line;
}
void BytecodeBuilder::setDebugLine(int line)
{
debugLine = line;
}
void BytecodeBuilder::pushDebugLocal(StringRef name, uint8_t reg, uint32_t startpc, uint32_t endpc)
{
unsigned int index = addStringTableEntry(name);
DebugLocal local;
local.name = index;
local.reg = reg;
local.startpc = startpc;
local.endpc = endpc;
debugLocals.push_back(local);
}
void BytecodeBuilder::pushDebugUpval(StringRef name)
{
unsigned int index = addStringTableEntry(name);
DebugUpval upval;
upval.name = index;
debugUpvals.push_back(upval);
}
size_t BytecodeBuilder::getInstructionCount() const
{
return insns.size();
}
uint32_t BytecodeBuilder::getDebugPC() const
{
return uint32_t(insns.size());
}
void BytecodeBuilder::addDebugRemark(const char* format, ...)
{
if ((dumpFlags & Dump_Remarks) == 0)
return;
size_t offset = debugRemarkBuffer.size();
va_list args;
va_start(args, format);
vformatAppend(debugRemarkBuffer, format, args);
va_end(args);
// we null-terminate all remarks to avoid storing remark length
debugRemarkBuffer += '\0';
debugRemarks.emplace_back(uint32_t(insns.size()), uint32_t(offset));
dumpRemarks.emplace_back(debugLine, debugRemarkBuffer.c_str() + offset);
}
void BytecodeBuilder::finalize()
{
LUAU_ASSERT(bytecode.empty());
// preallocate space for bytecode blob
size_t capacity = 16;
for (auto& p : stringTable)
capacity += p.first.length + 2;
for (const Function& func : functions)
capacity += func.data.size();
bytecode.reserve(capacity);
// assemble final bytecode blob
uint8_t version = getVersion();
LUAU_ASSERT(version >= LBC_VERSION_MIN && version <= LBC_VERSION_MAX);
bytecode = char(version);
writeStringTable(bytecode);
writeVarInt(bytecode, uint32_t(functions.size()));
for (const Function& func : functions)
bytecode += func.data;
LUAU_ASSERT(mainFunction < functions.size());
writeVarInt(bytecode, mainFunction);
}
void BytecodeBuilder::writeFunction(std::string& ss, uint32_t id) const
{
LUAU_ASSERT(id < functions.size());
const Function& func = functions[id];
// header
writeByte(ss, func.maxstacksize);
writeByte(ss, func.numparams);
writeByte(ss, func.numupvalues);
writeByte(ss, func.isvararg);
// instructions
writeVarInt(ss, uint32_t(insns.size()));
for (size_t i = 0; i < insns.size();)
{
uint8_t op = LUAU_INSN_OP(insns[i]);
LUAU_ASSERT(op < LOP__COUNT);
int oplen = getOpLength(LuauOpcode(op));
uint8_t openc = encoder ? encoder->encodeOp(op) : op;
writeInt(ss, openc | (insns[i] & ~0xff));
for (int j = 1; j < oplen; ++j)
writeInt(ss, insns[i + j]);
i += oplen;
}
// constants
writeVarInt(ss, uint32_t(constants.size()));
for (const Constant& c : constants)
{
switch (c.type)
{
case Constant::Type_Nil:
writeByte(ss, LBC_CONSTANT_NIL);
break;
case Constant::Type_Boolean:
writeByte(ss, LBC_CONSTANT_BOOLEAN);
writeByte(ss, c.valueBoolean);
break;
case Constant::Type_Number:
writeByte(ss, LBC_CONSTANT_NUMBER);
writeDouble(ss, c.valueNumber);
break;
case Constant::Type_String:
writeByte(ss, LBC_CONSTANT_STRING);
writeVarInt(ss, c.valueString);
break;
case Constant::Type_Import:
writeByte(ss, LBC_CONSTANT_IMPORT);
writeInt(ss, c.valueImport);
break;
case Constant::Type_Table:
{
const TableShape& shape = tableShapes[c.valueTable];
writeByte(ss, LBC_CONSTANT_TABLE);
writeVarInt(ss, uint32_t(shape.length));
for (unsigned int i = 0; i < shape.length; ++i)
writeVarInt(ss, shape.keys[i]);
break;
}
case Constant::Type_Closure:
writeByte(ss, LBC_CONSTANT_CLOSURE);
writeVarInt(ss, c.valueClosure);
break;
default:
LUAU_ASSERT(!"Unsupported constant type");
}
}
// child protos
writeVarInt(ss, uint32_t(protos.size()));
for (uint32_t child : protos)
writeVarInt(ss, child);
// debug info
writeVarInt(ss, func.debuglinedefined);
writeVarInt(ss, func.debugname);
bool hasLines = true;
for (int line : lines)
if (line == 0)
{
hasLines = false;
break;
}
if (hasLines)
{
writeByte(ss, 1);
writeLineInfo(ss);
}
else
{
writeByte(ss, 0);
}
bool hasDebug = !debugLocals.empty() || !debugUpvals.empty();
if (hasDebug)
{
writeByte(ss, 1);
writeVarInt(ss, uint32_t(debugLocals.size()));
for (const DebugLocal& l : debugLocals)
{
writeVarInt(ss, l.name);
writeVarInt(ss, l.startpc);
writeVarInt(ss, l.endpc);
writeByte(ss, l.reg);
}
writeVarInt(ss, uint32_t(debugUpvals.size()));
for (const DebugUpval& l : debugUpvals)
{
writeVarInt(ss, l.name);
}
}
else
{
writeByte(ss, 0);
}
}
void BytecodeBuilder::writeLineInfo(std::string& ss) const
{
LUAU_ASSERT(!lines.empty());
// this function encodes lines inside each span as a 8-bit delta to span baseline
// span is always a power of two; depending on the line info input, it may need to be as low as 1
int span = 1 << 24;
// first pass: determine span length
for (size_t offset = 0; offset < lines.size(); offset += span)
{
size_t next = offset;
int min = lines[offset];
int max = lines[offset];
for (; next < lines.size() && next < offset + span; ++next)
{
min = std::min(min, lines[next]);
max = std::max(max, lines[next]);
if (max - min > 255)
break;
}
if (next < lines.size() && next - offset < size_t(span))
{
// since not all lines in the range fit in 8b delta, we need to shrink the span
// next iteration will need to reprocess some lines again since span changed
span = 1 << log2(int(next - offset));
}
}
// second pass: compute span base
int baselineOne = 0;
std::vector<int> baselineScratch;
int* baseline = &baselineOne;
size_t baselineSize = (lines.size() - 1) / span + 1;
if (baselineSize > 1)
{
// avoid heap allocation for single-element baseline which is most functions (<256 lines)
baselineScratch.resize(baselineSize);
baseline = baselineScratch.data();
}
for (size_t offset = 0; offset < lines.size(); offset += span)
{
size_t next = offset;
int min = lines[offset];
for (; next < lines.size() && next < offset + span; ++next)
min = std::min(min, lines[next]);
baseline[offset / span] = min;
}
// third pass: write resulting data
int logspan = log2(span);
writeByte(ss, uint8_t(logspan));
uint8_t lastOffset = 0;
for (size_t i = 0; i < lines.size(); ++i)
{
int delta = lines[i] - baseline[i >> logspan];
LUAU_ASSERT(delta >= 0 && delta <= 255);
writeByte(ss, uint8_t(delta) - lastOffset);
lastOffset = uint8_t(delta);
}
int lastLine = 0;
for (size_t i = 0; i < baselineSize; ++i)
{
writeInt(ss, baseline[i] - lastLine);
lastLine = baseline[i];
}
}
void BytecodeBuilder::writeStringTable(std::string& ss) const
{
std::vector<StringRef> strings(stringTable.size());
for (auto& p : stringTable)
{
LUAU_ASSERT(p.second > 0 && p.second <= strings.size());
strings[p.second - 1] = p.first;
}
writeVarInt(ss, uint32_t(strings.size()));
for (auto& s : strings)
{
writeVarInt(ss, uint32_t(s.length));
ss.append(s.data, s.length);
}
}
uint32_t BytecodeBuilder::getImportId(int32_t id0)
{
LUAU_ASSERT(unsigned(id0) < 1024);
return (1u << 30) | (id0 << 20);
}
uint32_t BytecodeBuilder::getImportId(int32_t id0, int32_t id1)
{
LUAU_ASSERT(unsigned(id0 | id1) < 1024);
return (2u << 30) | (id0 << 20) | (id1 << 10);
}
uint32_t BytecodeBuilder::getImportId(int32_t id0, int32_t id1, int32_t id2)
{
LUAU_ASSERT(unsigned(id0 | id1 | id2) < 1024);
return (3u << 30) | (id0 << 20) | (id1 << 10) | id2;
}
int BytecodeBuilder::decomposeImportId(uint32_t ids, int32_t& id0, int32_t& id1, int32_t& id2)
{
int count = ids >> 30;
id0 = count > 0 ? int(ids >> 20) & 1023 : -1;
id1 = count > 1 ? int(ids >> 10) & 1023 : -1;
id2 = count > 2 ? int(ids) & 1023 : -1;
return count;
}
uint32_t BytecodeBuilder::getStringHash(StringRef key)
{
// This hashing algorithm should match luaS_hash defined in VM/lstring.cpp for short inputs; we can't use that code directly to keep compiler and
// VM independent in terms of compilation/linking. The resulting string hashes are embedded into bytecode binary and result in a better initial
// guess for the field hashes which improves performance during initial code execution. We omit the long string processing here for simplicity, as
// it doesn't really matter on long identifiers.
const char* str = key.data;
size_t len = key.length;
unsigned int h = unsigned(len);
// original Lua 5.1 hash for compatibility (exact match when len<32)
for (size_t i = len; i > 0; --i)
h ^= (h << 5) + (h >> 2) + (uint8_t)str[i - 1];
return h;
}
void BytecodeBuilder::foldJumps()
{
// if our function has long jumps, some processing below can make jump instructions not-jumps (e.g. JUMP->RETURN)
// it's safer to skip this processing
if (hasLongJumps)
return;
for (Jump& jump : jumps)
{
uint32_t jumpLabel = jump.source;
uint32_t jumpInsn = insns[jumpLabel];
// follow jump target through forward unconditional jumps
// we only follow forward jumps to make sure the process terminates
uint32_t targetLabel = jumpLabel + 1 + LUAU_INSN_D(jumpInsn);
LUAU_ASSERT(targetLabel < insns.size());
uint32_t targetInsn = insns[targetLabel];
while (LUAU_INSN_OP(targetInsn) == LOP_JUMP && LUAU_INSN_D(targetInsn) >= 0)
{
targetLabel = targetLabel + 1 + LUAU_INSN_D(targetInsn);
LUAU_ASSERT(targetLabel < insns.size());
targetInsn = insns[targetLabel];
}
int offset = int(targetLabel) - int(jumpLabel) - 1;
// for unconditional jumps to RETURN, we can replace JUMP with RETURN
if (LUAU_INSN_OP(jumpInsn) == LOP_JUMP && LUAU_INSN_OP(targetInsn) == LOP_RETURN)
{
insns[jumpLabel] = targetInsn;
lines[jumpLabel] = lines[targetLabel];
}
else if (int16_t(offset) == offset)
{
insns[jumpLabel] &= 0xffff;
insns[jumpLabel] |= uint16_t(offset) << 16;
}
jump.target = targetLabel;
}
}
void BytecodeBuilder::expandJumps()
{
if (!hasLongJumps)
return;
// we have some jump instructions that couldn't be patched which means their offset didn't fit into 16 bits
// our strategy for replacing instructions is as follows: instead of
// OP jumpoffset
// we will synthesize a jump trampoline before our instruction (note that jump offsets are relative to next instruction):
// JUMP +1
// JUMPX jumpoffset
// OP -2
// the idea is that during forward execution, we will jump over JUMPX into OP; if OP decides to jump, it will jump to JUMPX
// JUMPX can carry a 24-bit jump offset
// jump trampolines expand the code size, which can increase existing jump distances.
// because of this, we may need to expand jumps that previously fit into 16-bit just fine.
// the worst-case expansion is 3x, so to be conservative we will repatch all jumps that have an offset >= 32767/3
const int kMaxJumpDistanceConservative = 32767 / 3;
// we will need to process jumps in order
std::sort(jumps.begin(), jumps.end(), [](const Jump& lhs, const Jump& rhs) {
return lhs.source < rhs.source;
});
// first, let's add jump thunks for every jump with a distance that's too big
// we will create new instruction buffers, with remap table keeping track of the moves: remap[oldpc] = newpc
std::vector<uint32_t> remap(insns.size());
std::vector<uint32_t> newinsns;
std::vector<int> newlines;
LUAU_ASSERT(insns.size() == lines.size());
newinsns.reserve(insns.size());
newlines.reserve(insns.size());
size_t currentJump = 0;
size_t pendingTrampolines = 0;
for (size_t i = 0; i < insns.size();)
{
uint8_t op = LUAU_INSN_OP(insns[i]);
LUAU_ASSERT(op < LOP__COUNT);
if (currentJump < jumps.size() && jumps[currentJump].source == i)
{
int offset = int(jumps[currentJump].target) - int(jumps[currentJump].source) - 1;
if (abs(offset) > kMaxJumpDistanceConservative)
{
// insert jump trampoline as described above; we keep JUMPX offset uninitialized in this pass
newinsns.push_back(LOP_JUMP | (1 << 16));
newinsns.push_back(LOP_JUMPX);
newlines.push_back(lines[i]);
newlines.push_back(lines[i]);
pendingTrampolines++;
}
currentJump++;
}
int oplen = getOpLength(LuauOpcode(op));
// copy instruction and line info to the new stream
for (int j = 0; j < oplen; ++j)
{
remap[i] = uint32_t(newinsns.size());
newinsns.push_back(insns[i]);
newlines.push_back(lines[i]);
i++;
}
}
LUAU_ASSERT(currentJump == jumps.size());
LUAU_ASSERT(pendingTrampolines > 0);
// now we need to recompute offsets for jump instructions - we could not do this in the first pass because the offsets are between *target*
// instructions
for (Jump& jump : jumps)
{
int offset = int(jump.target) - int(jump.source) - 1;
int newoffset = int(remap[jump.target]) - int(remap[jump.source]) - 1;
if (abs(offset) > kMaxJumpDistanceConservative)
{
// fix up jump trampoline
uint32_t& insnt = newinsns[remap[jump.source] - 1];
uint32_t& insnj = newinsns[remap[jump.source]];
LUAU_ASSERT(LUAU_INSN_OP(insnt) == LOP_JUMPX);
// patch JUMPX to JUMPX to target location; note that newoffset is the offset of the jump *relative to OP*, so we need to add 1 to make it
// relative to JUMPX
insnt &= 0xff;
insnt |= uint32_t(newoffset + 1) << 8;
// patch OP to OP -2
insnj &= 0xffff;
insnj |= uint16_t(-2) << 16;
pendingTrampolines--;
}
else
{
uint32_t& insn = newinsns[remap[jump.source]];
// make sure jump instruction had the correct offset before we started
LUAU_ASSERT(LUAU_INSN_D(insn) == offset);
// patch instruction with the new offset
LUAU_ASSERT(int16_t(newoffset) == newoffset);
insn &= 0xffff;
insn |= uint16_t(newoffset) << 16;
}
}
LUAU_ASSERT(pendingTrampolines == 0);
// this was hard, but we're done.
insns.swap(newinsns);
lines.swap(newlines);
}
std::string BytecodeBuilder::getError(const std::string& message)
{
// 0 acts as a special marker for error bytecode (it's equal to LBC_VERSION_TARGET for valid bytecode blobs)
std::string result;
result += char(0);
result += message;
return result;
}
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
return LBC_VERSION_TARGET;
}
#ifdef LUAU_ASSERTENABLED
void BytecodeBuilder::validate() const
{
validateInstructions();
validateVariadic();
}
void BytecodeBuilder::validateInstructions() const
{
#define VREG(v) LUAU_ASSERT(unsigned(v) < func.maxstacksize)
#define VREGRANGE(v, count) LUAU_ASSERT(unsigned(v + (count < 0 ? 0 : count)) <= func.maxstacksize)
#define VUPVAL(v) LUAU_ASSERT(unsigned(v) < func.numupvalues)
#define VCONST(v, kind) LUAU_ASSERT(unsigned(v) < constants.size() && constants[v].type == Constant::Type_##kind)
#define VCONSTANY(v) LUAU_ASSERT(unsigned(v) < constants.size())
#define VJUMP(v) LUAU_ASSERT(size_t(i + 1 + v) < insns.size() && insnvalid[i + 1 + v])
LUAU_ASSERT(currentFunction != ~0u);
const Function& func = functions[currentFunction];
// tag instruction offsets so that we can validate jumps
std::vector<uint8_t> insnvalid(insns.size(), 0);
for (size_t i = 0; i < insns.size();)
{
uint32_t insn = insns[i];
LuauOpcode op = LuauOpcode(LUAU_INSN_OP(insn));
insnvalid[i] = true;
i += getOpLength(op);
LUAU_ASSERT(i <= insns.size());
}
std::vector<uint8_t> openCaptures;
// validate individual instructions
for (size_t i = 0; i < insns.size();)
{
uint32_t insn = insns[i];
LuauOpcode op = LuauOpcode(LUAU_INSN_OP(insn));
switch (op)
{
case LOP_LOADNIL:
VREG(LUAU_INSN_A(insn));
break;
case LOP_LOADB:
VREG(LUAU_INSN_A(insn));
LUAU_ASSERT(LUAU_INSN_B(insn) == 0 || LUAU_INSN_B(insn) == 1);
VJUMP(LUAU_INSN_C(insn));
break;
case LOP_LOADN:
VREG(LUAU_INSN_A(insn));
break;
case LOP_LOADK:
VREG(LUAU_INSN_A(insn));
VCONSTANY(LUAU_INSN_D(insn));
break;
case LOP_MOVE:
VREG(LUAU_INSN_A(insn));
VREG(LUAU_INSN_B(insn));
break;
case LOP_GETGLOBAL:
case LOP_SETGLOBAL:
VREG(LUAU_INSN_A(insn));
VCONST(insns[i + 1], String);
break;
case LOP_GETUPVAL:
case LOP_SETUPVAL:
VREG(LUAU_INSN_A(insn));
VUPVAL(LUAU_INSN_B(insn));
break;
case LOP_CLOSEUPVALS:
VREG(LUAU_INSN_A(insn));
while (openCaptures.size() && openCaptures.back() >= LUAU_INSN_A(insn))
openCaptures.pop_back();
break;
case LOP_GETIMPORT:
VREG(LUAU_INSN_A(insn));
VCONST(LUAU_INSN_D(insn), Import);
// TODO: check insn[i + 1] for conformance with 10-bit import encoding
break;
case LOP_GETTABLE:
case LOP_SETTABLE:
VREG(LUAU_INSN_A(insn));
VREG(LUAU_INSN_B(insn));
VREG(LUAU_INSN_C(insn));
break;
case LOP_GETTABLEKS:
case LOP_SETTABLEKS:
VREG(LUAU_INSN_A(insn));
VREG(LUAU_INSN_B(insn));
VCONST(insns[i + 1], String);
break;
case LOP_GETTABLEN:
case LOP_SETTABLEN:
VREG(LUAU_INSN_A(insn));
VREG(LUAU_INSN_B(insn));
break;
case LOP_NEWCLOSURE:
{
VREG(LUAU_INSN_A(insn));
LUAU_ASSERT(unsigned(LUAU_INSN_D(insn)) < protos.size());
LUAU_ASSERT(protos[LUAU_INSN_D(insn)] < functions.size());
unsigned int numupvalues = functions[protos[LUAU_INSN_D(insn)]].numupvalues;
for (unsigned int j = 0; j < numupvalues; ++j)
{
LUAU_ASSERT(i + 1 + j < insns.size());
uint32_t cinsn = insns[i + 1 + j];
LUAU_ASSERT(LUAU_INSN_OP(cinsn) == LOP_CAPTURE);
}
}
break;
case LOP_NAMECALL:
VREG(LUAU_INSN_A(insn));
VREG(LUAU_INSN_B(insn));
VCONST(insns[i + 1], String);
LUAU_ASSERT(LUAU_INSN_OP(insns[i + 2]) == LOP_CALL);
break;
case LOP_CALL:
{
int nparams = LUAU_INSN_B(insn) - 1;
int nresults = LUAU_INSN_C(insn) - 1;
VREG(LUAU_INSN_A(insn));
VREGRANGE(LUAU_INSN_A(insn) + 1, nparams); // 1..nparams
VREGRANGE(LUAU_INSN_A(insn), nresults); // 1..nresults
}
break;
case LOP_RETURN:
{
int nresults = LUAU_INSN_B(insn) - 1;
VREGRANGE(LUAU_INSN_A(insn), nresults); // 0..nresults-1
}
break;
case LOP_JUMP:
VJUMP(LUAU_INSN_D(insn));
break;
case LOP_JUMPIF:
case LOP_JUMPIFNOT:
VREG(LUAU_INSN_A(insn));
VJUMP(LUAU_INSN_D(insn));
break;
case LOP_JUMPIFEQ:
case LOP_JUMPIFLE:
case LOP_JUMPIFLT:
case LOP_JUMPIFNOTEQ:
case LOP_JUMPIFNOTLE:
case LOP_JUMPIFNOTLT:
VREG(LUAU_INSN_A(insn));
VREG(insns[i + 1]);
VJUMP(LUAU_INSN_D(insn));
break;
case LOP_JUMPXEQKNIL:
case LOP_JUMPXEQKB:
VREG(LUAU_INSN_A(insn));
VJUMP(LUAU_INSN_D(insn));
break;
case LOP_JUMPXEQKN:
VREG(LUAU_INSN_A(insn));
VCONST(insns[i + 1] & 0xffffff, Number);
VJUMP(LUAU_INSN_D(insn));
break;
case LOP_JUMPXEQKS:
VREG(LUAU_INSN_A(insn));
VCONST(insns[i + 1] & 0xffffff, String);
VJUMP(LUAU_INSN_D(insn));
break;
case LOP_ADD:
case LOP_SUB:
case LOP_MUL:
case LOP_DIV:
case LOP_MOD:
case LOP_POW:
VREG(LUAU_INSN_A(insn));
VREG(LUAU_INSN_B(insn));
VREG(LUAU_INSN_C(insn));
break;
case LOP_ADDK:
case LOP_SUBK:
case LOP_MULK:
case LOP_DIVK:
case LOP_MODK:
case LOP_POWK:
VREG(LUAU_INSN_A(insn));
VREG(LUAU_INSN_B(insn));
VCONST(LUAU_INSN_C(insn), Number);
break;
case LOP_AND:
case LOP_OR:
VREG(LUAU_INSN_A(insn));
VREG(LUAU_INSN_B(insn));
VREG(LUAU_INSN_C(insn));
break;
case LOP_ANDK:
case LOP_ORK:
VREG(LUAU_INSN_A(insn));
VREG(LUAU_INSN_B(insn));
VCONSTANY(LUAU_INSN_C(insn));
break;
case LOP_CONCAT:
VREG(LUAU_INSN_A(insn));
VREG(LUAU_INSN_B(insn));
VREG(LUAU_INSN_C(insn));
LUAU_ASSERT(LUAU_INSN_B(insn) <= LUAU_INSN_C(insn));
break;
case LOP_NOT:
case LOP_MINUS:
case LOP_LENGTH:
VREG(LUAU_INSN_A(insn));
VREG(LUAU_INSN_B(insn));
break;
case LOP_NEWTABLE:
VREG(LUAU_INSN_A(insn));
break;
case LOP_DUPTABLE:
VREG(LUAU_INSN_A(insn));
VCONST(LUAU_INSN_D(insn), Table);
break;
case LOP_SETLIST:
{
int count = LUAU_INSN_C(insn) - 1;
VREG(LUAU_INSN_A(insn));
VREGRANGE(LUAU_INSN_B(insn), count);
}
break;
case LOP_FORNPREP:
case LOP_FORNLOOP:
// for loop protocol: A, A+1, A+2 are used for iteration
VREG(LUAU_INSN_A(insn) + 2);
VJUMP(LUAU_INSN_D(insn));
break;
case LOP_FORGPREP:
// forg loop protocol: A, A+1, A+2 are used for iteration protocol; A+3, ... are loop variables
VREG(LUAU_INSN_A(insn) + 2 + 1);
VJUMP(LUAU_INSN_D(insn));
break;
case LOP_FORGLOOP:
// forg loop protocol: A, A+1, A+2 are used for iteration protocol; A+3, ... are loop variables
VREG(LUAU_INSN_A(insn) + 2 + uint8_t(insns[i + 1]));
VJUMP(LUAU_INSN_D(insn));
LUAU_ASSERT(uint8_t(insns[i + 1]) >= 1);
break;
case LOP_FORGPREP_INEXT:
case LOP_FORGPREP_NEXT:
VREG(LUAU_INSN_A(insn) + 4); // forg loop protocol: A, A+1, A+2 are used for iteration protocol; A+3, A+4 are loop variables
VJUMP(LUAU_INSN_D(insn));
break;
case LOP_GETVARARGS:
{
int nresults = LUAU_INSN_B(insn) - 1;
VREGRANGE(LUAU_INSN_A(insn), nresults); // 0..nresults-1
}
break;
case LOP_DUPCLOSURE:
{
VREG(LUAU_INSN_A(insn));
VCONST(LUAU_INSN_D(insn), Closure);
unsigned int proto = constants[LUAU_INSN_D(insn)].valueClosure;
LUAU_ASSERT(proto < functions.size());
unsigned int numupvalues = functions[proto].numupvalues;
for (unsigned int j = 0; j < numupvalues; ++j)
{
LUAU_ASSERT(i + 1 + j < insns.size());
uint32_t cinsn = insns[i + 1 + j];
LUAU_ASSERT(LUAU_INSN_OP(cinsn) == LOP_CAPTURE);
LUAU_ASSERT(LUAU_INSN_A(cinsn) == LCT_VAL || LUAU_INSN_A(cinsn) == LCT_UPVAL);
}
}
break;
case LOP_PREPVARARGS:
LUAU_ASSERT(LUAU_INSN_A(insn) == func.numparams);
LUAU_ASSERT(func.isvararg);
break;
case LOP_BREAK:
break;
case LOP_JUMPBACK:
VJUMP(LUAU_INSN_D(insn));
break;
case LOP_LOADKX:
VREG(LUAU_INSN_A(insn));
VCONSTANY(insns[i + 1]);
break;
case LOP_JUMPX:
VJUMP(LUAU_INSN_E(insn));
break;
case LOP_FASTCALL:
VJUMP(LUAU_INSN_C(insn));
LUAU_ASSERT(LUAU_INSN_OP(insns[i + 1 + LUAU_INSN_C(insn)]) == LOP_CALL);
break;
case LOP_FASTCALL1:
VREG(LUAU_INSN_B(insn));
VJUMP(LUAU_INSN_C(insn));
LUAU_ASSERT(LUAU_INSN_OP(insns[i + 1 + LUAU_INSN_C(insn)]) == LOP_CALL);
break;
case LOP_FASTCALL2:
VREG(LUAU_INSN_B(insn));
VJUMP(LUAU_INSN_C(insn));
LUAU_ASSERT(LUAU_INSN_OP(insns[i + 1 + LUAU_INSN_C(insn)]) == LOP_CALL);
VREG(insns[i + 1]);
break;
case LOP_FASTCALL2K:
VREG(LUAU_INSN_B(insn));
VJUMP(LUAU_INSN_C(insn));
LUAU_ASSERT(LUAU_INSN_OP(insns[i + 1 + LUAU_INSN_C(insn)]) == LOP_CALL);
VCONSTANY(insns[i + 1]);
break;
case LOP_COVERAGE:
break;
case LOP_CAPTURE:
switch (LUAU_INSN_A(insn))
{
case LCT_VAL:
VREG(LUAU_INSN_B(insn));
break;
case LCT_REF:
VREG(LUAU_INSN_B(insn));
openCaptures.push_back(LUAU_INSN_B(insn));
break;
case LCT_UPVAL:
VUPVAL(LUAU_INSN_B(insn));
break;
default:
LUAU_ASSERT(!"Unsupported capture type");
}
break;
default:
LUAU_ASSERT(!"Unsupported opcode");
}
i += getOpLength(op);
LUAU_ASSERT(i <= insns.size());
}
// all CAPTURE REF instructions must have a CLOSEUPVALS instruction after them in the bytecode stream
// this doesn't guarantee safety as it doesn't perform basic block based analysis, but if this fails
// then the bytecode is definitely unsafe to run since the compiler won't generate backwards branches
// except for loop edges
LUAU_ASSERT(openCaptures.empty());
#undef VREG
#undef VREGEND
#undef VUPVAL
#undef VCONST
#undef VCONSTANY
#undef VJUMP
}
void BytecodeBuilder::validateVariadic() const
{
// validate MULTRET sequences: instructions that produce a variadic sequence and consume one must come in pairs
// we classify instructions into four groups: producers, consumers, neutral and others
// any producer (an instruction that produces more than one value) must be followed by 0 or more neutral instructions
// and a consumer (that consumes more than one value); these form a variadic sequence.
// except for producer, no instruction in the variadic sequence may be a jump target.
// from the execution perspective, producer adjusts L->top to point to one past the last result, neutral instructions
// leave L->top unmodified, and consumer adjusts L->top back to the stack frame end.
// consumers invalidate all values after L->top after they execute (which we currently don't validate)
bool variadicSeq = false;
std::vector<uint8_t> insntargets(insns.size(), 0);
for (size_t i = 0; i < insns.size();)
{
uint32_t insn = insns[i];
LuauOpcode op = LuauOpcode(LUAU_INSN_OP(insn));
int target = getJumpTarget(insn, uint32_t(i));
if (target >= 0 && !isFastCall(op))
{
LUAU_ASSERT(unsigned(target) < insns.size());
insntargets[target] = true;
}
i += getOpLength(op);
LUAU_ASSERT(i <= insns.size());
}
for (size_t i = 0; i < insns.size();)
{
uint32_t insn = insns[i];
LuauOpcode op = LuauOpcode(LUAU_INSN_OP(insn));
if (variadicSeq)
{
// no instruction inside the sequence, including the consumer, may be a jump target
// this guarantees uninterrupted L->top adjustment flow
LUAU_ASSERT(!insntargets[i]);
}
if (op == LOP_CALL)
{
// note: calls may end one variadic sequence and start a new one
if (LUAU_INSN_B(insn) == 0)
{
// consumer instruction ens a variadic sequence
LUAU_ASSERT(variadicSeq);
variadicSeq = false;
}
else
{
// CALL is not a neutral instruction so it can't be present in a variadic sequence unless it's a consumer
LUAU_ASSERT(!variadicSeq);
}
if (LUAU_INSN_C(insn) == 0)
{
// producer instruction starts a variadic sequence
LUAU_ASSERT(!variadicSeq);
variadicSeq = true;
}
}
else if (op == LOP_GETVARARGS && LUAU_INSN_B(insn) == 0)
{
// producer instruction starts a variadic sequence
LUAU_ASSERT(!variadicSeq);
variadicSeq = true;
}
else if ((op == LOP_RETURN && LUAU_INSN_B(insn) == 0) || (op == LOP_SETLIST && LUAU_INSN_C(insn) == 0))
{
// consumer instruction ends a variadic sequence
LUAU_ASSERT(variadicSeq);
variadicSeq = false;
}
else if (op == LOP_FASTCALL)
{
int callTarget = int(i + LUAU_INSN_C(insn) + 1);
LUAU_ASSERT(unsigned(callTarget) < insns.size() && LUAU_INSN_OP(insns[callTarget]) == LOP_CALL);
if (LUAU_INSN_B(insns[callTarget]) == 0)
{
// consumer instruction ends a variadic sequence; however, we can't terminate it yet because future analysis of CALL will do it
// during FASTCALL fallback, the instructions between this and CALL consumer are going to be executed before L->top so they must
// be neutral; as such, we will defer termination of variadic sequence until CALL analysis
LUAU_ASSERT(variadicSeq);
}
else
{
// FASTCALL is not a neutral instruction so it can't be present in a variadic sequence unless it's linked to CALL consumer
LUAU_ASSERT(!variadicSeq);
}
// note: if FASTCALL is linked to a CALL producer, the instructions between FASTCALL and CALL are technically not part of an executed
// variadic sequence since they are never executed if FASTCALL does anything, so it's okay to skip their validation until CALL
// (we can't simply start a variadic sequence here because that would trigger assertions during linked CALL validation)
}
else if (op == LOP_CLOSEUPVALS || op == LOP_NAMECALL || op == LOP_GETIMPORT || op == LOP_MOVE || op == LOP_GETUPVAL || op == LOP_GETGLOBAL ||
op == LOP_GETTABLEKS || op == LOP_COVERAGE)
{
// instructions inside a variadic sequence must be neutral (can't change L->top)
// while there are many neutral instructions like this, here we check that the instruction is one of the few
// that we'd expect to exist in FASTCALL fallback sequences or between consecutive CALLs for encoding reasons
}
else
{
LUAU_ASSERT(!variadicSeq);
}
i += getOpLength(op);
LUAU_ASSERT(i <= insns.size());
}
LUAU_ASSERT(!variadicSeq);
}
#endif
static bool printableStringConstant(const char* str, size_t len)
{
for (size_t i = 0; i < len; ++i)
{
if (unsigned(str[i]) < ' ')
return false;
}
return true;
}
void BytecodeBuilder::dumpConstant(std::string& result, int k) const
{
LUAU_ASSERT(unsigned(k) < constants.size());
const Constant& data = constants[k];
switch (data.type)
{
case Constant::Type_Nil:
formatAppend(result, "nil");
break;
case Constant::Type_Boolean:
formatAppend(result, "%s", data.valueBoolean ? "true" : "false");
break;
case Constant::Type_Number:
formatAppend(result, "%.17g", data.valueNumber);
break;
case Constant::Type_String:
{
const StringRef& str = debugStrings[data.valueString - 1];
if (printableStringConstant(str.data, str.length))
{
if (str.length < 32)
formatAppend(result, "'%.*s'", int(str.length), str.data);
else
formatAppend(result, "'%.*s'...", 32, str.data);
}
break;
}
case Constant::Type_Import:
{
int id0 = -1, id1 = -1, id2 = -1;
if (int count = decomposeImportId(data.valueImport, id0, id1, id2))
{
{
const Constant& id = constants[id0];
LUAU_ASSERT(id.type == Constant::Type_String && id.valueString <= debugStrings.size());
const StringRef& str = debugStrings[id.valueString - 1];
formatAppend(result, "%.*s", int(str.length), str.data);
}
if (count > 1)
{
const Constant& id = constants[id1];
LUAU_ASSERT(id.type == Constant::Type_String && id.valueString <= debugStrings.size());
const StringRef& str = debugStrings[id.valueString - 1];
formatAppend(result, ".%.*s", int(str.length), str.data);
}
if (count > 2)
{
const Constant& id = constants[id2];
LUAU_ASSERT(id.type == Constant::Type_String && id.valueString <= debugStrings.size());
const StringRef& str = debugStrings[id.valueString - 1];
formatAppend(result, ".%.*s", int(str.length), str.data);
}
}
break;
}
case Constant::Type_Table:
formatAppend(result, "{...}");
break;
case Constant::Type_Closure:
{
const Function& func = functions[data.valueClosure];
if (!func.dumpname.empty())
formatAppend(result, "'%s'", func.dumpname.c_str());
break;
}
}
}
void BytecodeBuilder::dumpInstruction(const uint32_t* code, std::string& result, int targetLabel) const
{
uint32_t insn = *code++;
switch (LUAU_INSN_OP(insn))
{
case LOP_LOADNIL:
formatAppend(result, "LOADNIL R%d\n", LUAU_INSN_A(insn));
break;
case LOP_LOADB:
if (LUAU_INSN_C(insn))
formatAppend(result, "LOADB R%d %d +%d\n", LUAU_INSN_A(insn), LUAU_INSN_B(insn), LUAU_INSN_C(insn));
else
formatAppend(result, "LOADB R%d %d\n", LUAU_INSN_A(insn), LUAU_INSN_B(insn));
break;
case LOP_LOADN:
formatAppend(result, "LOADN R%d %d\n", LUAU_INSN_A(insn), LUAU_INSN_D(insn));
break;
case LOP_LOADK:
formatAppend(result, "LOADK R%d K%d [", LUAU_INSN_A(insn), LUAU_INSN_D(insn));
dumpConstant(result, LUAU_INSN_D(insn));
result.append("]\n");
break;
case LOP_MOVE:
formatAppend(result, "MOVE R%d R%d\n", LUAU_INSN_A(insn), LUAU_INSN_B(insn));
break;
case LOP_GETGLOBAL:
formatAppend(result, "GETGLOBAL R%d K%d [", LUAU_INSN_A(insn), *code);
dumpConstant(result, *code);
result.append("]\n");
code++;
break;
case LOP_SETGLOBAL:
formatAppend(result, "SETGLOBAL R%d K%d [", LUAU_INSN_A(insn), *code);
dumpConstant(result, *code);
result.append("]\n");
code++;
break;
case LOP_GETUPVAL:
formatAppend(result, "GETUPVAL R%d %d\n", LUAU_INSN_A(insn), LUAU_INSN_B(insn));
break;
case LOP_SETUPVAL:
formatAppend(result, "SETUPVAL R%d %d\n", LUAU_INSN_A(insn), LUAU_INSN_B(insn));
break;
case LOP_CLOSEUPVALS:
formatAppend(result, "CLOSEUPVALS R%d\n", LUAU_INSN_A(insn));
break;
case LOP_GETIMPORT:
formatAppend(result, "GETIMPORT R%d %d [", LUAU_INSN_A(insn), LUAU_INSN_D(insn));
dumpConstant(result, LUAU_INSN_D(insn));
result.append("]\n");
code++; // AUX
break;
case LOP_GETTABLE:
formatAppend(result, "GETTABLE R%d R%d R%d\n", LUAU_INSN_A(insn), LUAU_INSN_B(insn), LUAU_INSN_C(insn));
break;
case LOP_SETTABLE:
formatAppend(result, "SETTABLE R%d R%d R%d\n", LUAU_INSN_A(insn), LUAU_INSN_B(insn), LUAU_INSN_C(insn));
break;
case LOP_GETTABLEKS:
formatAppend(result, "GETTABLEKS R%d R%d K%d [", LUAU_INSN_A(insn), LUAU_INSN_B(insn), *code);
dumpConstant(result, *code);
result.append("]\n");
code++;
break;
case LOP_SETTABLEKS:
formatAppend(result, "SETTABLEKS R%d R%d K%d [", LUAU_INSN_A(insn), LUAU_INSN_B(insn), *code);
dumpConstant(result, *code);
result.append("]\n");
code++;
break;
case LOP_GETTABLEN:
formatAppend(result, "GETTABLEN R%d R%d %d\n", LUAU_INSN_A(insn), LUAU_INSN_B(insn), LUAU_INSN_C(insn) + 1);
break;
case LOP_SETTABLEN:
formatAppend(result, "SETTABLEN R%d R%d %d\n", LUAU_INSN_A(insn), LUAU_INSN_B(insn), LUAU_INSN_C(insn) + 1);
break;
case LOP_NEWCLOSURE:
formatAppend(result, "NEWCLOSURE R%d P%d\n", LUAU_INSN_A(insn), LUAU_INSN_D(insn));
break;
case LOP_NAMECALL:
formatAppend(result, "NAMECALL R%d R%d K%d [", LUAU_INSN_A(insn), LUAU_INSN_B(insn), *code);
dumpConstant(result, *code);
result.append("]\n");
code++;
break;
case LOP_CALL:
formatAppend(result, "CALL R%d %d %d\n", LUAU_INSN_A(insn), LUAU_INSN_B(insn) - 1, LUAU_INSN_C(insn) - 1);
break;
case LOP_RETURN:
formatAppend(result, "RETURN R%d %d\n", LUAU_INSN_A(insn), LUAU_INSN_B(insn) - 1);
break;
case LOP_JUMP:
formatAppend(result, "JUMP L%d\n", targetLabel);
break;
case LOP_JUMPIF:
formatAppend(result, "JUMPIF R%d L%d\n", LUAU_INSN_A(insn), targetLabel);
break;
case LOP_JUMPIFNOT:
formatAppend(result, "JUMPIFNOT R%d L%d\n", LUAU_INSN_A(insn), targetLabel);
break;
case LOP_JUMPIFEQ:
formatAppend(result, "JUMPIFEQ R%d R%d L%d\n", LUAU_INSN_A(insn), *code++, targetLabel);
break;
case LOP_JUMPIFLE:
formatAppend(result, "JUMPIFLE R%d R%d L%d\n", LUAU_INSN_A(insn), *code++, targetLabel);
break;
case LOP_JUMPIFLT:
formatAppend(result, "JUMPIFLT R%d R%d L%d\n", LUAU_INSN_A(insn), *code++, targetLabel);
break;
case LOP_JUMPIFNOTEQ:
formatAppend(result, "JUMPIFNOTEQ R%d R%d L%d\n", LUAU_INSN_A(insn), *code++, targetLabel);
break;
case LOP_JUMPIFNOTLE:
formatAppend(result, "JUMPIFNOTLE R%d R%d L%d\n", LUAU_INSN_A(insn), *code++, targetLabel);
break;
case LOP_JUMPIFNOTLT:
formatAppend(result, "JUMPIFNOTLT R%d R%d L%d\n", LUAU_INSN_A(insn), *code++, targetLabel);
break;
case LOP_ADD:
formatAppend(result, "ADD R%d R%d R%d\n", LUAU_INSN_A(insn), LUAU_INSN_B(insn), LUAU_INSN_C(insn));
break;
case LOP_SUB:
formatAppend(result, "SUB R%d R%d R%d\n", LUAU_INSN_A(insn), LUAU_INSN_B(insn), LUAU_INSN_C(insn));
break;
case LOP_MUL:
formatAppend(result, "MUL R%d R%d R%d\n", LUAU_INSN_A(insn), LUAU_INSN_B(insn), LUAU_INSN_C(insn));
break;
case LOP_DIV:
formatAppend(result, "DIV R%d R%d R%d\n", LUAU_INSN_A(insn), LUAU_INSN_B(insn), LUAU_INSN_C(insn));
break;
case LOP_MOD:
formatAppend(result, "MOD R%d R%d R%d\n", LUAU_INSN_A(insn), LUAU_INSN_B(insn), LUAU_INSN_C(insn));
break;
case LOP_POW:
formatAppend(result, "POW R%d R%d R%d\n", LUAU_INSN_A(insn), LUAU_INSN_B(insn), LUAU_INSN_C(insn));
break;
case LOP_ADDK:
formatAppend(result, "ADDK R%d R%d K%d [", LUAU_INSN_A(insn), LUAU_INSN_B(insn), LUAU_INSN_C(insn));
dumpConstant(result, LUAU_INSN_C(insn));
result.append("]\n");
break;
case LOP_SUBK:
formatAppend(result, "SUBK R%d R%d K%d [", LUAU_INSN_A(insn), LUAU_INSN_B(insn), LUAU_INSN_C(insn));
dumpConstant(result, LUAU_INSN_C(insn));
result.append("]\n");
break;
case LOP_MULK:
formatAppend(result, "MULK R%d R%d K%d [", LUAU_INSN_A(insn), LUAU_INSN_B(insn), LUAU_INSN_C(insn));
dumpConstant(result, LUAU_INSN_C(insn));
result.append("]\n");
break;
case LOP_DIVK:
formatAppend(result, "DIVK R%d R%d K%d [", LUAU_INSN_A(insn), LUAU_INSN_B(insn), LUAU_INSN_C(insn));
dumpConstant(result, LUAU_INSN_C(insn));
result.append("]\n");
break;
case LOP_MODK:
formatAppend(result, "MODK R%d R%d K%d [", LUAU_INSN_A(insn), LUAU_INSN_B(insn), LUAU_INSN_C(insn));
dumpConstant(result, LUAU_INSN_C(insn));
result.append("]\n");
break;
case LOP_POWK:
formatAppend(result, "POWK R%d R%d K%d [", LUAU_INSN_A(insn), LUAU_INSN_B(insn), LUAU_INSN_C(insn));
dumpConstant(result, LUAU_INSN_C(insn));
result.append("]\n");
break;
case LOP_AND:
formatAppend(result, "AND R%d R%d R%d\n", LUAU_INSN_A(insn), LUAU_INSN_B(insn), LUAU_INSN_C(insn));
break;
case LOP_OR:
formatAppend(result, "OR R%d R%d R%d\n", LUAU_INSN_A(insn), LUAU_INSN_B(insn), LUAU_INSN_C(insn));
break;
case LOP_ANDK:
formatAppend(result, "ANDK R%d R%d K%d [", LUAU_INSN_A(insn), LUAU_INSN_B(insn), LUAU_INSN_C(insn));
dumpConstant(result, LUAU_INSN_C(insn));
result.append("]\n");
break;
case LOP_ORK:
formatAppend(result, "ORK R%d R%d K%d [", LUAU_INSN_A(insn), LUAU_INSN_B(insn), LUAU_INSN_C(insn));
dumpConstant(result, LUAU_INSN_C(insn));
result.append("]\n");
break;
case LOP_CONCAT:
formatAppend(result, "CONCAT R%d R%d R%d\n", LUAU_INSN_A(insn), LUAU_INSN_B(insn), LUAU_INSN_C(insn));
break;
case LOP_NOT:
formatAppend(result, "NOT R%d R%d\n", LUAU_INSN_A(insn), LUAU_INSN_B(insn));
break;
case LOP_MINUS:
formatAppend(result, "MINUS R%d R%d\n", LUAU_INSN_A(insn), LUAU_INSN_B(insn));
break;
case LOP_LENGTH:
formatAppend(result, "LENGTH R%d R%d\n", LUAU_INSN_A(insn), LUAU_INSN_B(insn));
break;
case LOP_NEWTABLE:
formatAppend(result, "NEWTABLE R%d %d %d\n", LUAU_INSN_A(insn), LUAU_INSN_B(insn) == 0 ? 0 : 1 << (LUAU_INSN_B(insn) - 1), *code++);
break;
case LOP_DUPTABLE:
formatAppend(result, "DUPTABLE R%d %d\n", LUAU_INSN_A(insn), LUAU_INSN_D(insn));
break;
case LOP_SETLIST:
formatAppend(result, "SETLIST R%d R%d %d [%d]\n", LUAU_INSN_A(insn), LUAU_INSN_B(insn), LUAU_INSN_C(insn) - 1, *code++);
break;
case LOP_FORNPREP:
formatAppend(result, "FORNPREP R%d L%d\n", LUAU_INSN_A(insn), targetLabel);
break;
case LOP_FORNLOOP:
formatAppend(result, "FORNLOOP R%d L%d\n", LUAU_INSN_A(insn), targetLabel);
break;
case LOP_FORGPREP:
formatAppend(result, "FORGPREP R%d L%d\n", LUAU_INSN_A(insn), targetLabel);
break;
case LOP_FORGLOOP:
formatAppend(result, "FORGLOOP R%d L%d %d%s\n", LUAU_INSN_A(insn), targetLabel, uint8_t(*code), int(*code) < 0 ? " [inext]" : "");
code++;
break;
case LOP_FORGPREP_INEXT:
formatAppend(result, "FORGPREP_INEXT R%d L%d\n", LUAU_INSN_A(insn), targetLabel);
break;
case LOP_FORGPREP_NEXT:
formatAppend(result, "FORGPREP_NEXT R%d L%d\n", LUAU_INSN_A(insn), targetLabel);
break;
case LOP_GETVARARGS:
formatAppend(result, "GETVARARGS R%d %d\n", LUAU_INSN_A(insn), LUAU_INSN_B(insn) - 1);
break;
case LOP_DUPCLOSURE:
formatAppend(result, "DUPCLOSURE R%d K%d [", LUAU_INSN_A(insn), LUAU_INSN_D(insn));
dumpConstant(result, LUAU_INSN_D(insn));
result.append("]\n");
break;
case LOP_BREAK:
formatAppend(result, "BREAK\n");
break;
case LOP_JUMPBACK:
formatAppend(result, "JUMPBACK L%d\n", targetLabel);
break;
case LOP_LOADKX:
formatAppend(result, "LOADKX R%d K%d [", LUAU_INSN_A(insn), *code);
dumpConstant(result, *code);
result.append("]\n");
code++;
break;
case LOP_JUMPX:
formatAppend(result, "JUMPX L%d\n", targetLabel);
break;
case LOP_FASTCALL:
formatAppend(result, "FASTCALL %d L%d\n", LUAU_INSN_A(insn), targetLabel);
break;
case LOP_FASTCALL1:
formatAppend(result, "FASTCALL1 %d R%d L%d\n", LUAU_INSN_A(insn), LUAU_INSN_B(insn), targetLabel);
break;
case LOP_FASTCALL2:
formatAppend(result, "FASTCALL2 %d R%d R%d L%d\n", LUAU_INSN_A(insn), LUAU_INSN_B(insn), *code, targetLabel);
code++;
break;
case LOP_FASTCALL2K:
formatAppend(result, "FASTCALL2K %d R%d K%d L%d [", LUAU_INSN_A(insn), LUAU_INSN_B(insn), *code, targetLabel);
dumpConstant(result, *code);
result.append("]\n");
code++;
break;
case LOP_COVERAGE:
formatAppend(result, "COVERAGE\n");
break;
case LOP_CAPTURE:
formatAppend(result, "CAPTURE %s %c%d\n",
LUAU_INSN_A(insn) == LCT_UPVAL ? "UPVAL"
: LUAU_INSN_A(insn) == LCT_REF ? "REF"
: LUAU_INSN_A(insn) == LCT_VAL ? "VAL"
: "",
LUAU_INSN_A(insn) == LCT_UPVAL ? 'U' : 'R', LUAU_INSN_B(insn));
break;
case LOP_JUMPXEQKNIL:
formatAppend(result, "JUMPXEQKNIL R%d L%d%s\n", LUAU_INSN_A(insn), targetLabel, *code >> 31 ? " NOT" : "");
code++;
break;
case LOP_JUMPXEQKB:
formatAppend(result, "JUMPXEQKB R%d %d L%d%s\n", LUAU_INSN_A(insn), *code & 1, targetLabel, *code >> 31 ? " NOT" : "");
code++;
break;
case LOP_JUMPXEQKN:
formatAppend(result, "JUMPXEQKN R%d K%d L%d%s [", LUAU_INSN_A(insn), *code & 0xffffff, targetLabel, *code >> 31 ? " NOT" : "");
dumpConstant(result, *code & 0xffffff);
result.append("]\n");
code++;
break;
case LOP_JUMPXEQKS:
formatAppend(result, "JUMPXEQKS R%d K%d L%d%s [", LUAU_INSN_A(insn), *code & 0xffffff, targetLabel, *code >> 31 ? " NOT" : "");
dumpConstant(result, *code & 0xffffff);
result.append("]\n");
code++;
break;
default:
LUAU_ASSERT(!"Unsupported opcode");
}
}
std::string BytecodeBuilder::dumpCurrentFunction(std::vector<int>& dumpinstoffs) const
{
if ((dumpFlags & Dump_Code) == 0)
return std::string();
int lastLine = -1;
size_t nextRemark = 0;
std::string result;
if (dumpFlags & Dump_Locals)
{
for (size_t i = 0; i < debugLocals.size(); ++i)
{
const DebugLocal& l = debugLocals[i];
LUAU_ASSERT(l.startpc < l.endpc);
LUAU_ASSERT(l.startpc < lines.size());
LUAU_ASSERT(l.endpc <= lines.size()); // endpc is exclusive in the debug info, but it's more intuitive to print inclusive data
// it would be nice to emit name as well but it requires reverse lookup through stringtable
formatAppend(result, "local %d: reg %d, start pc %d line %d, end pc %d line %d\n", int(i), l.reg, l.startpc, lines[l.startpc],
l.endpc - 1, lines[l.endpc - 1]);
}
}
std::vector<int> labels(insns.size(), -1);
// annotate valid jump targets with 0
for (size_t i = 0; i < insns.size();)
{
int target = getJumpTarget(insns[i], uint32_t(i));
if (target >= 0)
{
LUAU_ASSERT(size_t(target) < insns.size());
labels[target] = 0;
}
i += getOpLength(LuauOpcode(LUAU_INSN_OP(insns[i])));
LUAU_ASSERT(i <= insns.size());
}
int nextLabel = 0;
// compute label ids (sequential integers for all jump targets)
for (size_t i = 0; i < labels.size(); ++i)
if (labels[i] == 0)
labels[i] = nextLabel++;
dumpinstoffs.resize(insns.size() + 1, -1);
for (size_t i = 0; i < insns.size();)
{
const uint32_t* code = &insns[i];
uint8_t op = LUAU_INSN_OP(*code);
dumpinstoffs[i] = int(result.size());
if (op == LOP_PREPVARARGS)
{
// Don't emit function header in bytecode - it's used for call dispatching and doesn't contain "interesting" information
i++;
continue;
}
if (dumpFlags & Dump_Remarks)
{
while (nextRemark < debugRemarks.size() && debugRemarks[nextRemark].first == i)
{
formatAppend(result, "REMARK %s\n", debugRemarkBuffer.c_str() + debugRemarks[nextRemark].second);
nextRemark++;
}
}
if (dumpFlags & Dump_Source)
{
int line = lines[i];
if (line > 0 && line != lastLine)
{
LUAU_ASSERT(size_t(line - 1) < dumpSource.size());
formatAppend(result, "%5d: %s\n", line, dumpSource[line - 1].c_str());
lastLine = line;
}
}
if (dumpFlags & Dump_Lines)
formatAppend(result, "%d: ", lines[i]);
if (labels[i] != -1)
formatAppend(result, "L%d: ", labels[i]);
int target = getJumpTarget(*code, uint32_t(i));
dumpInstruction(code, result, target >= 0 ? labels[target] : -1);
i += getOpLength(LuauOpcode(op));
LUAU_ASSERT(i <= insns.size());
}
dumpinstoffs[insns.size()] = int(result.size());
return result;
}
void BytecodeBuilder::setDumpSource(const std::string& source)
{
dumpSource.clear();
size_t pos = 0;
while (pos != std::string::npos)
{
size_t next = source.find('\n', pos);
if (next == std::string::npos)
{
dumpSource.push_back(source.substr(pos));
pos = next;
}
else
{
dumpSource.push_back(source.substr(pos, next - pos));
pos = next + 1;
}
if (!dumpSource.back().empty() && dumpSource.back().back() == '\r')
dumpSource.back().pop_back();
}
}
std::string BytecodeBuilder::dumpFunction(uint32_t id) const
{
LUAU_ASSERT(id < functions.size());
return functions[id].dump;
}
std::string BytecodeBuilder::dumpEverything() const
{
std::string result;
for (size_t i = 0; i < functions.size(); ++i)
{
std::string debugname = functions[i].dumpname.empty() ? "??" : functions[i].dumpname;
formatAppend(result, "Function %d (%s):\n", int(i), debugname.c_str());
result += functions[i].dump;
result += "\n";
}
return result;
}
std::string BytecodeBuilder::dumpSourceRemarks() const
{
std::string result;
size_t nextRemark = 0;
std::vector<std::pair<int, std::string>> remarks = dumpRemarks;
std::sort(remarks.begin(), remarks.end());
for (size_t i = 0; i < dumpSource.size(); ++i)
{
const std::string& line = dumpSource[i];
size_t indent = 0;
while (indent < line.length() && (line[indent] == ' ' || line[indent] == '\t'))
indent++;
while (nextRemark < remarks.size() && remarks[nextRemark].first == int(i + 1))
{
formatAppend(result, "%.*s-- remark: %s\n", int(indent), line.c_str(), remarks[nextRemark].second.c_str());
nextRemark++;
// skip duplicate remarks (due to inlining/unrolling)
while (nextRemark < remarks.size() && remarks[nextRemark] == remarks[nextRemark - 1])
nextRemark++;
}
result += line;
if (i + 1 < dumpSource.size())
result += '\n';
}
return result;
}
void BytecodeBuilder::annotateInstruction(std::string& result, uint32_t fid, uint32_t instpos) const
{
if ((dumpFlags & Dump_Code) == 0)
return;
LUAU_ASSERT(fid < functions.size());
const Function& function = functions[fid];
const std::string& dump = function.dump;
const std::vector<int>& dumpinstoffs = function.dumpinstoffs;
uint32_t next = instpos + 1;
LUAU_ASSERT(next < dumpinstoffs.size());
// Skip locations of multi-dword instructions
while (next < dumpinstoffs.size() && dumpinstoffs[next] == -1)
next++;
formatAppend(result, "%.*s", dumpinstoffs[next] - dumpinstoffs[instpos], dump.data() + dumpinstoffs[instpos]);
}
} // namespace Luau