microvium
Version:
A compact, embeddable scripting engine for microcontrollers for executing small scripts written in a subset of JavaScript.
1,135 lines • 102 kB
JavaScript
"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
__setModuleDefault(result, mod);
return result;
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.decodeSnapshot = void 0;
const IL = __importStar(require("./il"));
const snapshot_il_1 = require("./snapshot-il");
const utils_1 = require("./utils");
const smart_buffer_1 = require("smart-buffer");
const crc_1 = require("crc");
const runtime_types_1 = require("./runtime-types");
const _ = __importStar(require("lodash"));
const stringify_il_1 = require("./stringify-il");
const bytecode_opcodes_1 = require("./bytecode-opcodes");
const snapshot_1 = require("./snapshot");
const il_opcodes_1 = require("./il-opcodes");
/** Decode a snapshot (bytecode) to IL */
function decodeSnapshot(snapshot) {
const buffer = smart_buffer_1.SmartBuffer.fromBuffer(snapshot.data);
let region = [];
let regionStack = [];
let regionName;
let regionStart = 0;
const gcAllocationsRegion = [];
const romAllocationsRegion = [];
const processedAllocationsByOffset = new Map();
const resumePointByPhysicalAddress = new Map();
const reconstructionInfo = (snapshot instanceof snapshot_1.SnapshotClass
? snapshot.reconstructionInfo
: undefined);
let handlesBeginOffset = Infinity;
beginRegion('Header');
const bytecodeVersion = readHeaderField8('bytecodeVersion');
const headerSize = readHeaderField8('headerSize');
const requiredEngineVersion = readHeaderField8('requiredEngineVersion');
readHeaderField8('reserved');
const bytecodeSize = readHeaderField16('bytecodeSize', false);
const expectedCRC = readHeaderField16('expectedCRC', true);
const requiredFeatureFlags = readHeaderField32('requiredFeatureFlags', false);
if (bytecodeSize !== buffer.length) {
return (0, utils_1.invalidOperation)(`Invalid bytecode file (bytecode size mismatch)`);
}
if (headerSize !== snapshot_il_1.HEADER_SIZE) {
return (0, utils_1.invalidOperation)(`Invalid bytecode file (header size unexpected)`);
}
const actualCRC = (0, crc_1.crc16ccitt)(snapshot.data.slice(8));
if (actualCRC !== expectedCRC) {
return (0, utils_1.invalidOperation)(`Invalid bytecode file (CRC mismatch)`);
}
if (bytecodeVersion !== snapshot_il_1.ENGINE_MAJOR_VERSION) {
return (0, utils_1.invalidOperation)(`Bytecode version ${bytecodeVersion} is not supported`);
}
const snapshotInfo = {
globalSlots: new Map(),
functions: new Map(),
exports: new Map(),
allocations: new Map(),
flags: new Set(),
builtins: {
promisePrototype: IL.undefinedValue,
arrayPrototype: IL.undefinedValue,
asyncCatchBlock: IL.undefinedValue,
asyncContinue: IL.undefinedValue,
asyncHostCallback: IL.undefinedValue,
}
};
// Section offsets
const sectionOffsets = {};
for (let i = 0; i < runtime_types_1.mvm_TeBytecodeSection.BCS_SECTION_COUNT; i++) {
sectionOffsets[i] = readHeaderField16(runtime_types_1.mvm_TeBytecodeSection[i], true);
}
endRegion('Header');
if (requiredEngineVersion !== snapshot_il_1.ENGINE_MINOR_VERSION) {
return (0, utils_1.invalidOperation)(`Engine version ${requiredEngineVersion} is not supported (expected ${snapshot_il_1.ENGINE_MINOR_VERSION})`);
}
// Note: we could in future decode the ROM and HEAP sections explicitly, since
// the heap is now parsable. But for the moment, just the reachable
// allocations in these are decoded, and they're done so as part of
// interpreting the corresponding pointers that reference each allocation.
decodeFlags();
decodeImportTable();
decodeExportTable();
decodeShortCallTable();
decodeBuiltins();
decodeStringTable();
decodeGlobals();
region.push({
offset: undefined,
size: undefined,
content: {
type: 'Region',
regionName: 'ROM allocations',
value: romAllocationsRegion
}
});
region.push({
offset: undefined,
size: undefined,
content: {
type: 'Region',
regionName: 'GC allocations',
value: gcAllocationsRegion
}
});
(0, utils_1.hardAssert)(regionStack.length === 0); // Make sure all regions have ended
finalizeRegions(region, 0, buffer.length);
const disassembly = {
bytecodeSize: snapshot.data.length,
components: region
};
return {
snapshotInfo,
disassembly: stringifyDisassembly(disassembly)
};
function decodeBuiltins() {
const { offset: builtinsOffset, size } = getSectionInfo(runtime_types_1.mvm_TeBytecodeSection.BCS_BUILTINS);
(0, utils_1.hardAssert)(size === runtime_types_1.mvm_TeBuiltins.BIN_BUILTIN_COUNT * 2);
const builtins = {};
beginRegion('Builtins');
for (let i = 0; i < runtime_types_1.mvm_TeBuiltins.BIN_BUILTIN_COUNT; i++) {
const builtinOffset = builtinsOffset + i * 2;
const builtinValue = buffer.readUInt16LE(builtinOffset);
const value = decodeValue(builtinValue);
region.push({
offset: builtinOffset,
size: 2,
content: {
type: 'LabeledValue',
label: `[${runtime_types_1.mvm_TeBuiltins[i]}]`,
value
}
});
const logicalValue = getLogicalValue(value);
if (logicalValue.type === 'DeletedValue') {
return (0, utils_1.unexpected)();
}
builtins[i] = logicalValue;
}
endRegion('Builtins');
snapshotInfo.builtins.promisePrototype = builtins[runtime_types_1.mvm_TeBuiltins.BIN_PROMISE_PROTOTYPE];
snapshotInfo.builtins.arrayPrototype = builtins[runtime_types_1.mvm_TeBuiltins.BIN_ARRAY_PROTO];
snapshotInfo.builtins.asyncCatchBlock = builtins[runtime_types_1.mvm_TeBuiltins.BIN_ASYNC_CATCH_BLOCK];
snapshotInfo.builtins.asyncHostCallback = builtins[runtime_types_1.mvm_TeBuiltins.BIN_ASYNC_HOST_CALLBACK];
snapshotInfo.builtins.asyncContinue = builtins[runtime_types_1.mvm_TeBuiltins.BIN_ASYNC_CONTINUE];
}
function decodeFlags() {
for (let i = 0; i < 32; i++) {
if (requiredFeatureFlags & (1 << i)) {
snapshotInfo.flags.add(i);
}
}
}
function decodeExportTable() {
const { offset, size } = getSectionInfo(runtime_types_1.mvm_TeBytecodeSection.BCS_EXPORT_TABLE);
buffer.readOffset = offset;
const exportCount = size / 4;
beginRegion('Export Table');
for (let i = 0; i < exportCount; i++) {
const offset = buffer.readOffset;
const exportID = buffer.readUInt16LE();
const exportValue = buffer.readUInt16LE();
const value = decodeValue(exportValue);
region.push({
offset,
size: 4,
content: {
type: 'LabeledValue',
label: `[${exportID}]`,
value
}
});
const logicalValue = getLogicalValue(value);
if (logicalValue.type !== 'DeletedValue') {
snapshotInfo.exports.set(exportID, logicalValue);
}
}
endRegion('Export Table');
}
function decodeShortCallTable() {
const { size } = getSectionInfo(runtime_types_1.mvm_TeBytecodeSection.BCS_SHORT_CALL_TABLE);
if (size > 0) {
return (0, utils_1.notImplemented)(); // TODO
}
}
function decodeStringTable() {
const { size, offset } = getSectionInfo(runtime_types_1.mvm_TeBytecodeSection.BCS_STRING_TABLE);
buffer.readOffset = offset;
const stringTableCount = size / 2;
beginRegion('String Table');
for (let i = 0; i < stringTableCount; i++) {
let value = readValue(`[${i}]`);
}
endRegion('String Table');
}
function getName(offset, type) {
if (reconstructionInfo) {
const name = reconstructionInfo.names[type]?.[offset];
if (!name) {
// Note: Names should either come consistently from the reconstruction
// info or not at all. We can't mix unless we do extra work for
// namespace clashes.
return (0, utils_1.invalidOperation)(`Name not found for bytecode offset ${stringifyOffset(offset)}`);
}
else {
return name;
}
}
else {
return undefined;
}
}
function hasName(offset, type) {
if (reconstructionInfo) {
return offset in reconstructionInfo.names[type];
}
else {
return undefined;
}
}
function decodeGlobals() {
const { offset, size } = getSectionInfo(runtime_types_1.mvm_TeBytecodeSection.BCS_GLOBALS);
const globalVariableCount = size / 2;
buffer.readOffset = offset;
beginRegion('Globals');
for (let i = 0; i < globalVariableCount; i++) {
const slotOffset = offset + i * 2;
// Handles are at the end of the globals
const isHandle = slotOffset >= handlesBeginOffset;
if (isHandle) {
readValue(`Handle`);
}
else {
const value = readValue(`[${i}]`);
if (value.type === 'DeletedValue')
continue;
snapshotInfo.globalSlots.set(getNameOfGlobal(i), {
value,
indexHint: i
});
}
}
endRegion('Globals');
}
function decodeImportTable() {
const { offset, size } = getSectionInfo(runtime_types_1.mvm_TeBytecodeSection.BCS_IMPORT_TABLE);
buffer.readOffset = offset;
const importCount = size / 2;
beginRegion('Import Table');
for (let i = 0; i < importCount; i++) {
const offset = buffer.readOffset;
const u16 = buffer.readUInt16LE();
region.push({
offset,
size: 2,
content: {
type: 'Attribute',
label: `[${i}]`,
value: u16
}
});
}
endRegion('Import Table');
}
function getSectionInfo(section) {
const offset = getSectionOffset(section);
const size = getSectionSize(section);
const end = offset + size;
return { offset, size, end };
function getSectionOffset(section) {
return sectionOffsets[section] ?? (0, utils_1.unexpected)();
}
function getSectionSize(section) {
if (section < runtime_types_1.mvm_TeBytecodeSection.BCS_SECTION_COUNT - 1) {
return getSectionOffset(section + 1) - getSectionOffset(section);
}
else {
(0, utils_1.hardAssert)(section === runtime_types_1.mvm_TeBytecodeSection.BCS_SECTION_COUNT - 1);
return bytecodeSize - getSectionOffset(section);
}
}
}
function beginRegion(name) {
regionStack.push({ region, regionName, regionStart });
const newRegion = [];
region.push({
offset: buffer.readOffset,
size: undefined,
content: {
type: 'Region',
regionName: name,
value: newRegion
}
});
region = newRegion;
regionStart = buffer.readOffset;
regionName = name;
}
function endRegion(name) {
(0, utils_1.hardAssert)(regionName === name);
(0, utils_1.hardAssert)(regionStack.length > 0);
({ region, regionName, regionStart } = regionStack.pop());
}
function finalizeRegions(region, start, end) {
if (region.length === 0)
return undefined;
// Calculate offset for nested regions
for (const component of region) {
// Nested region
if (component.content.type === 'Region') {
const finalizeResult = finalizeRegions(component.content.value);
// Delete empty region
if (!finalizeResult) {
component.size = 0;
}
else {
if (component.offset === undefined) {
component.offset = finalizeResult.offset;
}
else if (finalizeResult.offset < component.offset) {
component.content.value.push({
offset: component.offset,
size: finalizeResult.offset - component.offset,
content: { type: 'RegionOverflow' }
});
console.error(`!! WARNING: Region overflow at 0x${finalizeResult.offset.toString(16).padStart(4, '0')} by ${component.offset - finalizeResult.offset} bytes`);
}
if (component.size === undefined) {
component.size = finalizeResult.size;
}
else if (finalizeResult.size > component.size) {
component.content.value.push({
offset: component.offset + finalizeResult.size,
size: component.size - finalizeResult.size,
content: { type: 'RegionOverflow' }
});
console.error(`!! WARNING: Region overflow at 0x${(component.offset + component.size).toString(16).padStart(4, '0')} by ${component.offset - finalizeResult.offset} bytes`);
}
else if (component.size > finalizeResult.size) {
component.content.value.push({
offset: component.offset + component.size,
size: component.size - finalizeResult.size,
content: { type: 'UnusedSpace' }
});
}
}
}
}
const sortedComponents = _.sortBy(region, component => component.offset);
// Clear out and rebuild
region.splice(0, region.length);
const regionStart = start !== undefined ? start : sortedComponents[0].offset;
let cursor = regionStart;
for (const component of sortedComponents) {
// Skip empty regions
if (component.content.type === 'Region' && component.content.value.length === 0) {
continue;
}
if (component.offset > cursor) {
region.push({
offset: cursor,
size: component.offset - cursor,
content: { type: 'UnusedSpace' }
});
}
else if (cursor > component.offset) {
region.push({
offset: cursor,
size: -(cursor - component.offset),
content: { type: 'OverlapWarning', offsetStart: component.offset, offsetEnd: cursor }
});
console.error(`!! WARNING: Entry overlap at 0x${component.offset} by ${cursor - component.offset} bytes`);
}
region.push(component);
cursor = component.offset + component.size;
}
if (end !== undefined && cursor < end) {
region.push({
offset: cursor,
size: end - cursor,
content: { type: 'UnusedSpace' }
});
cursor = end;
}
return { size: cursor - regionStart, offset: regionStart };
}
function readValue(label) {
const offset = buffer.readOffset;
const u16 = buffer.readUInt16LE();
const value = decodeValue(u16);
region.push({
offset,
size: 2,
content: { type: 'LabeledValue', label, value }
});
return getLogicalValue(value);
}
function readLogicalAt(offset, region, label, shallow = false) {
const value = readValueAt(offset, region, label, shallow);
const logical = getLogicalValue(value);
return logical;
}
function readValueAt(offset, region, label, shallow = false) {
const u16 = buffer.readUInt16LE(offset);
const value = decodeValue(u16, shallow);
region.push({
offset,
size: 2,
content: { type: 'LabeledValue', label, value }
});
return value;
}
function decodeValue(u16, shallow = false) {
if ((u16 & 1) === 0) {
return decodeShortPtr(u16, shallow);
}
else if ((u16 & 3) === 1) {
if (u16 < runtime_types_1.vm_TeWellKnownValues.VM_VALUE_WELLKNOWN_END) {
return decodeWellKnown(u16);
}
else {
return decodeBytecodeMappedPtr(u16 & 0xFFFC, shallow);
}
}
else {
(0, utils_1.hardAssert)((u16 & 3) === 3);
return decodeVirtualInt14(u16);
}
}
function decodeBytecodeMappedPtr(offset, shallow) {
// If the pointer points to a global variable, it is treated as logically
// pointing to the thing that global variable points to.
const { offset: globalsOffset, end: globalsEnd } = getSectionInfo(runtime_types_1.mvm_TeBytecodeSection.BCS_GLOBALS);
const isHandle = offset >= globalsOffset && offset < globalsEnd;
if (isHandle) {
const handle16 = buffer.readUInt16LE(offset);
handlesBeginOffset = Math.min(handlesBeginOffset, offset);
const handleValue = decodeValue(handle16);
if (handleValue.type === 'DeletedValue')
return (0, utils_1.unexpected)();
return {
type: 'Pointer',
offset,
target: handleValue
};
}
const { offset: romOffset, end: romEnd } = getSectionInfo(runtime_types_1.mvm_TeBytecodeSection.BCS_ROM);
if (offset >= romOffset && offset < romEnd) {
return {
type: 'Pointer',
offset,
target: decodeAllocationAtOffset(offset, 'bytecode', shallow)
};
}
return (0, utils_1.invalidOperation)('Pointer out of range');
}
function decodeWellKnown(u16) {
const value = u16;
switch (value) {
case runtime_types_1.vm_TeWellKnownValues.VM_VALUE_UNDEFINED: return IL.undefinedValue;
case runtime_types_1.vm_TeWellKnownValues.VM_VALUE_NULL: return IL.nullValue;
case runtime_types_1.vm_TeWellKnownValues.VM_VALUE_TRUE: return IL.trueValue;
case runtime_types_1.vm_TeWellKnownValues.VM_VALUE_FALSE: return IL.falseValue;
case runtime_types_1.vm_TeWellKnownValues.VM_VALUE_NAN: return IL.numberValue(NaN);
case runtime_types_1.vm_TeWellKnownValues.VM_VALUE_NEG_ZERO: return IL.numberValue(-0);
case runtime_types_1.vm_TeWellKnownValues.VM_VALUE_DELETED: return IL.deletedValue;
case runtime_types_1.vm_TeWellKnownValues.VM_VALUE_STR_LENGTH: return IL.stringValue('length');
case runtime_types_1.vm_TeWellKnownValues.VM_VALUE_STR_PROTO: return IL.stringValue('__proto__');
case runtime_types_1.vm_TeWellKnownValues.VM_VALUE_NO_OP_FUNC: return IL.noOpFunction;
case runtime_types_1.vm_TeWellKnownValues.VM_VALUE_WELLKNOWN_END: return (0, utils_1.unexpected)();
default: return (0, utils_1.unexpected)();
}
}
function decodeShortPtr(u16, shallow) {
(0, utils_1.hardAssert)((u16 & 0xFFFE) === u16);
const offset = getSectionInfo(runtime_types_1.mvm_TeBytecodeSection.BCS_HEAP).offset + u16;
return {
type: 'Pointer',
offset,
target: decodeAllocationAtOffset(offset, 'gc', shallow)
};
}
function decodeVirtualInt14(u16) {
u16 = u16 >> 2;
const value = u16 >= 0x2000 ? u16 - 0x4000 : u16;
return { type: 'NumberValue', value };
}
function offsetToAllocationID(offset) {
const name = getName(offset, 'allocation');
return name !== undefined ? parseInt(name) : offset;
}
function readHeaderField8(name) {
const address = buffer.readOffset;
const value = buffer.readUInt8();
region.push({
offset: address,
size: 1,
content: {
type: 'HeaderField',
name,
isOffset: false,
value
}
});
return value;
}
function readHeaderField16(name, isOffset) {
const address = buffer.readOffset;
const value = buffer.readUInt16LE();
region.push({
offset: address,
size: 2,
content: {
type: 'HeaderField',
name,
isOffset,
value
}
});
return value;
}
function readHeaderField32(name, isOffset) {
const address = buffer.readOffset;
const value = buffer.readUInt32LE();
region.push({
offset: address,
size: 4,
content: {
type: 'HeaderField',
name,
isOffset,
value
}
});
return value;
}
function decodeAllocationAtOffset(offset, section, shallow) {
if (shallow) {
return undefined;
}
if (processedAllocationsByOffset.has(offset)) {
return processedAllocationsByOffset.get(offset);
}
const value = decodeAllocationContent(offset, section);
// The decode is supposed to insert the value. It needs to do this itself
// because it needs to happen before nested allocations are pursued. The
// exception is resume points which look like allocations at first but are
// actually value types that reference into a function allocation.
(0, utils_1.hardAssert)(value.type === 'ResumePoint' || processedAllocationsByOffset.get(offset) === value);
return value;
}
function readAllocationHeader(allocationOffset, region) {
const headerWord = buffer.readUInt16LE(allocationOffset - 2);
const size = (headerWord & 0xFFF); // Size excluding header
const typeCode = headerWord >> 12;
// Allocation header
region.push({
offset: allocationOffset - 2,
size: 2,
content: { type: 'AllocationHeaderAttribute', text: `Size: ${size}, Type: ${runtime_types_1.TeTypeCode[typeCode]}` }
});
return { size, typeCode };
}
function getAllocationRegionForSection(section) {
switch (section) {
case 'bytecode': return romAllocationsRegion;
case 'gc': return gcAllocationsRegion;
default: return (0, utils_1.assertUnreachable)(section);
}
}
function decodeAllocationContent(offset, section) {
const region = getAllocationRegionForSection(section);
// Note: in the case of functions, the size bits are repurposed and don't
// represent the actual allocation size
const { size, typeCode } = readAllocationHeader(offset, region);
switch (typeCode) {
case runtime_types_1.TeTypeCode.TC_REF_TOMBSTONE: return (0, utils_1.unexpected)();
case runtime_types_1.TeTypeCode.TC_REF_INT32: return decodeInt32(region, offset, size);
case runtime_types_1.TeTypeCode.TC_REF_FLOAT64: return decodeFloat64(region, offset, size);
case runtime_types_1.TeTypeCode.TC_REF_STRING:
case runtime_types_1.TeTypeCode.TC_REF_INTERNED_STRING: return decodeString(region, offset, size);
case runtime_types_1.TeTypeCode.TC_REF_PROPERTY_LIST: return decodePropertyList(region, offset, size, section);
case runtime_types_1.TeTypeCode.TC_REF_ARRAY: return decodeArray(region, offset, size, false, section);
case runtime_types_1.TeTypeCode.TC_REF_FIXED_LENGTH_ARRAY: return decodeArray(region, offset, size, true, section);
case runtime_types_1.TeTypeCode.TC_REF_FUNCTION: return decodeFunction(region, offset, size);
case runtime_types_1.TeTypeCode.TC_REF_HOST_FUNC: return decodeHostFunction(region, offset, size);
case runtime_types_1.TeTypeCode.TC_REF_CLOSURE: return decodeClosure(region, offset, size, section);
case runtime_types_1.TeTypeCode.TC_REF_SYMBOL: return (0, utils_1.reserved)();
case runtime_types_1.TeTypeCode.TC_REF_VIRTUAL: return (0, utils_1.reserved)();
case runtime_types_1.TeTypeCode.TC_REF_UINT8_ARRAY: return decodeUint8Array(region, offset, size, section);
case runtime_types_1.TeTypeCode.TC_REF_CLASS: return decodeClass(region, offset, size);
case runtime_types_1.TeTypeCode.TC_REF_RESERVED_1: return (0, utils_1.unexpected)();
default: return (0, utils_1.unexpected)();
}
}
function decodeInt32(region, offset, size) {
const i = buffer.readInt32LE(offset);
const value = {
type: 'NumberValue',
value: i
};
processedAllocationsByOffset.set(offset, value);
region.push({
offset: offset,
size: size,
content: {
type: 'LabeledValue',
label: 'Value',
value
}
});
return value;
}
function decodeFloat64(region, offset, size) {
const n = buffer.readDoubleLE(offset);
const value = {
type: 'NumberValue',
value: n
};
processedAllocationsByOffset.set(offset, value);
region.push({
offset: offset,
size: size,
content: {
type: 'LabeledValue',
label: 'Value',
value
}
});
return value;
}
function decodeFunction(region, offset, sizeBits) {
/*
`decodeFunction` is called when it finds an allocation with a
TC_REF_FUNCTION header type-code. This used to mean that we've encountered
the main entry point for a function, but now the introduction of async-await
means that a function has multiple entry points.
*/
const isResumePoint = Boolean(sizeBits & 0x0800);
if (isResumePoint) {
return decodeResumePoint(region, offset, sizeBits);
}
else {
return decodeFunctionFromMainEntry(region, offset, sizeBits);
}
}
function decodeResumePoint(region, offset, sizeBits) {
// Hack: a resume point is the only thing that looks like an allocation but
// actually points into an existing allocation. So even though there is a
// header, we don't want this shown in the disassembly because it will show
// up again when we decode the main function.
const allocationHeader = region.pop();
(0, utils_1.hardAssert)(allocationHeader?.content.type === 'AllocationHeaderAttribute');
const resumeAddress = offset;
sizeBits = sizeBits & ~0x800;
(0, utils_1.hardAssert)((resumeAddress & 0xFFFC) == resumeAddress);
// Read function header that precedes the instruction
(0, utils_1.hardAssert)(((runtime_types_1.TeTypeCode.TC_REF_FUNCTION << 12) | 0x0800 | sizeBits) === buffer.readUInt16LE(resumeAddress - 2));
// The back-pointer reuses the bits that are normally used for the allocation size
const backPointer = sizeBits << 2;
const mainFunctionAddress = resumeAddress - backPointer;
// Sanity-check that we've found the main function address correctly
const mainHeader = buffer.readUInt16LE(mainFunctionAddress - 2);
(0, utils_1.hardAssert)(mainHeader >> 12 === runtime_types_1.TeTypeCode.TC_REF_FUNCTION);
(0, utils_1.hardAssert)((mainHeader & 0x0800) === 0); // Not a continuation
// Synthesize an mvm_Value representing the function pointer so we
// can use the common decoding logic
const mainFunctionValue = mainFunctionAddress | 1;
// Note: the `decodeValue` function as a side effect will actually decode
// the main function, if it's not already decoding. This is because the
// resume point is reachable independently of the main entry point to the
// function, and so we could have encountered this `VM_OP3_ASYNC_RESUME`
// before encountering the main function (or we might only encounter the
// main function through one of its resume points).
const mainFunction = getLogicalValue(decodeValue(mainFunctionValue));
(0, utils_1.hardAssert)(mainFunction.type === 'FunctionValue');
// As a consequence of decoding the main function, all the resume points in
// the function will also be decoded, so we can do a lookup to find this
// resume point.
const resumePoint = resumePointByPhysicalAddress.get(resumeAddress);
(0, utils_1.hardAssert)(resumePoint?.type === 'ResumePoint');
return resumePoint;
}
function decodeFunctionFromMainEntry(region, offset, sizeBits) {
const functionID = getName(offset, 'allocation') || stringifyOffset(offset);
const functionValue = {
type: 'FunctionValue',
value: functionID
};
processedAllocationsByOffset.set(offset, functionValue);
// Note: for functions, the size bits have been repurposed
const isContinuation = Boolean(sizeBits & 0x0800);
(0, utils_1.hardAssert)(!isContinuation); // Not supported yet
const maxStackDepth = sizeBits & 0x00FF;
const ilFunc = {
type: 'Function',
id: functionID,
blocks: {},
maxStackDepth: maxStackDepth,
entryBlockID: offsetToBlockID(offset),
};
snapshotInfo.functions.set(ilFunc.id, ilFunc);
const functionBodyRegion = [{
offset,
size: 0,
content: {
type: 'Attribute',
label: 'maxStackDepth',
value: maxStackDepth
}
}, {
offset,
size: 0,
content: {
type: 'Attribute',
label: 'isContinuation',
value: isContinuation ? 1 : 0
}
}];
const { blocks, size } = decodeInstructions(functionBodyRegion, offset, functionID);
ilFunc.blocks = blocks;
region.push({
offset,
size,
content: {
type: 'Region',
regionName: `Function ${functionID}`,
value: functionBodyRegion
}
});
return functionValue;
}
function decodeInstructions(region, offset, functionID) {
const originalReadOffset = buffer.readOffset;
buffer.readOffset = offset;
// Because the size bits of the function header are repurposed, we need to
// infer the size by the last reachable instruction in the instruction graph.
let functionEndOffset = offset;
const instructionsCovered = new Set();
const blockEntryOffsets = new Set();
const instructionsByOffset = new Map();
const decodingBlock = new Map();
// The entry point is at the beginning (and this will also explore the whole
// reachable instruction graph)
decodeBlock(offset, 0, undefined);
buffer.readOffset = originalReadOffset;
const blocks = divideInstructionsIntoBlocks();
const size = functionEndOffset - offset;
return { blocks, size };
function divideInstructionsIntoBlocks() {
const blocks = {};
const instructionsInOrder = _.sortBy([...instructionsByOffset], ([offset]) => offset);
(0, utils_1.hardAssert)(instructionsInOrder.length > 0);
const [firstOffset, [firstInstruction, firstInstrDisassembly]] = instructionsInOrder[0];
let block = {
expectedStackDepthAtEntry: firstInstruction.stackDepthBefore,
id: offsetToBlockID(firstOffset),
operations: []
};
let blockRegion = [];
region.push({
offset: firstOffset,
size: undefined,
content: {
type: 'Region',
regionName: `Block ${block.id}`,
value: blockRegion
}
});
blocks[block.id] = block;
const iter = instructionsInOrder[Symbol.iterator]();
let iterResult = iter.next();
while (!iterResult.done) {
const [instructionOffset, [instruction, disassembly, instructionSize]] = iterResult.value;
const isStartOfBlock = block.operations.length === 0;
// Resume points are special because they're addressable, so we need to
// register them globally. Catch blocks are also special because they're
// addressable. We don't know which blocks are catch blocks, so we just
// map all the blocks (the start of every block).
if (instruction.opcode === 'AsyncResume' || isStartOfBlock) {
resumePointByPhysicalAddress.set(instructionOffset, {
type: 'ResumePoint',
address: {
type: 'ProgramAddressValue',
funcId: functionID,
blockId: block.id,
operationIndex: block.operations.length
}
});
}
block.operations.push(instruction);
blockRegion.push({
offset: instructionOffset,
size: instructionSize,
content: {
type: 'Annotation',
text: disassembly
}
});
iterResult = iter.next();
if (!iterResult.done) {
const [nextOffset, [nextInstruction]] = iterResult.value;
// Start a new block?
if (blockEntryOffsets.has(nextOffset)) {
// End off previous block
if (!il_opcodes_1.blockTerminatingOpcodes.has(instruction.opcode)) {
// Add implicit jump/fall-through
block.operations.push({
opcode: 'Jump',
stackDepthBefore: instruction.stackDepthBefore,
stackDepthAfter: instruction.stackDepthAfter,
operands: [{
type: 'LabelOperand',
targetBlockId: offsetToBlockID(nextOffset)
}]
});
blockRegion.push({
offset: nextOffset,
size: 0,
content: {
type: 'Annotation',
text: '<implicit fallthrough>'
}
});
}
// Create new block
block = {
expectedStackDepthAtEntry: nextInstruction.stackDepthBefore,
id: offsetToBlockID(nextOffset),
operations: []
};
blocks[block.id] = block;
blockRegion = [];
region.push({
offset: nextOffset,
size: undefined,
content: {
type: 'Region',
regionName: `Block ${block.id}`,
value: blockRegion
}
});
}
}
}
return blocks;
}
function decodeBlock(blockOffset, stackDepth, tryStack) {
if (decodingBlock.has(blockOffset)) {
(0, utils_1.hardAssert)(decodingBlock.get(blockOffset).stackDepth === stackDepth, `Inconsistent stack depth at block 0x${blockOffset.toString(16)}`);
return;
}
decodingBlock.set(blockOffset, { stackDepth });
const blockLinks = [];
const prevOffset = buffer.readOffset;
buffer.readOffset = blockOffset;
blockEntryOffsets.add(blockOffset);
while (true) {
if (instructionsCovered.has(buffer.readOffset)) {
break;
}
instructionsCovered.add(buffer.readOffset);
const instructionOffset = buffer.readOffset;
const stackDepthBefore = stackDepth;
const decodeResult = decodeInstruction(region, stackDepthBefore, tryStack);
if (buffer.readOffset > functionEndOffset) {
functionEndOffset = buffer.readOffset;
}
const opcode = decodeResult.operation.opcode;
const op = {
...decodeResult.operation,
opcode: opcode,
stackDepthBefore: (0, utils_1.notUndefined)(stackDepthBefore),
stackDepthAfter: undefined,
};
// Special case for StartTry and EndTry because they jump stack depths
if (opcode === 'StartTry') {
tryStack = { outer: tryStack, stackDepthBeforeTry: stackDepth };
stackDepth += 2;
}
else if (opcode === 'EndTry') {
stackDepth = (0, utils_1.notUndefined)(tryStack).stackDepthBeforeTry;
tryStack = (0, utils_1.notUndefined)(tryStack).outer;
}
else {
const calculateStackDepthChange = IL.calcStaticStackChangeOfOp(op) ?? (0, utils_1.unexpected)();
stackDepth += calculateStackDepthChange;
}
op.stackDepthAfter = stackDepth;
const size = buffer.readOffset - instructionOffset;
const disassembly = decodeResult.disassembly || (0, stringify_il_1.stringifyOperation)(op);
instructionsByOffset.set(instructionOffset, [op, disassembly, size]);
// Control flow operations
if (decodeResult.jumpTo) {
blockLinks.push(...decodeResult.jumpTo.targets);
if (!decodeResult.jumpTo.alsoContinue)
break;
}
}
buffer.readOffset = prevOffset;
for (const { offset, stackDepth, tryStack } of blockLinks) {
decodeBlock(offset, stackDepth, tryStack);
}
}
}
function decodeHostFunction(region, offset, size) {
const hostFunctionIndex = buffer.readUInt16LE(offset);
const { offset: importTableOffset, size: importTableSize } = getSectionInfo(runtime_types_1.mvm_TeBytecodeSection.BCS_IMPORT_TABLE);
const hostFunctionIDOffset = importTableOffset + hostFunctionIndex * 2;
(0, utils_1.hardAssert)(hostFunctionIDOffset < importTableOffset + importTableSize);
const hostFunctionID = buffer.readUInt16LE(hostFunctionIDOffset);
const hostFunctionValue = {
type: 'HostFunctionValue',
value: hostFunctionID
};
processedAllocationsByOffset.set(offset, hostFunctionValue);
region.push({
offset: offset,
size: size,
content: {
type: 'Attribute',
label: 'Value',
value: `Import Table [${hostFunctionIndex}] (&${stringifyOffset(hostFunctionIDOffset)})`
}
});
return hostFunctionValue;
}
function decodeString(region, offset, size) {
const origOffset = buffer.readOffset;
buffer.readOffset = offset;
const str = buffer.readString(size - 1, 'utf8');
buffer.readOffset = origOffset;
const value = {
type: 'StringValue',
value: str
};
processedAllocationsByOffset.set(offset, value);
region.push({
offset: offset,
size: size,
content: {
type: 'LabeledValue',
label: 'Value',
value
}
});
return value;
}
function decodePropertyList(region, offset, size, section) {
const allocationID = offsetToAllocationID(offset);
const object = {
type: 'ObjectAllocation',
allocationID,
prototype: undefined,
properties: {},
memoryRegion: getAllocationMemoryRegion(section),
keysAreFixed: false,
internalSlots: [IL.deletedValue, IL.deletedValue]
};
snapshotInfo.allocations.set(allocationID, object);
const ref = {
type: 'ReferenceValue',
value: allocationID
};
processedAllocationsByOffset.set(offset, ref);
const objRegion = [];
let groupRegion = objRegion;
let groupOffset = offset;
let groupSize = size;
let internalSlotIndex = 2;
while (true) {
const dpNext = decodeValue(buffer.readUInt16LE(groupOffset), true);
if (dpNext.type === 'DeletedValue')
return (0, utils_1.unexpected)();
groupRegion.push({
size: 2,
offset: groupOffset,
content: {
type: 'LabeledValue',
label: 'dpNext',
value: dpNext
}
});
const dpProto = readLogicalAt(groupOffset + 2, groupRegion, 'dpProto');
if (groupOffset === offset) { // First group
object.prototype = dpProto;
}
else if (dpProto.type !== 'NullValue') {
return (0, utils_1.invalidOperation)('Only the first TsPropertyList in the chain should have a prototype.');
}
const propsOffset = groupOffset + 4;
const propCount = (groupSize - 4) / 4; // Each key-value pair is 4 bytes
for (let i = 0; i < propCount; i++) {
const propOffset = propsOffset + i * 4;
const key = readLogicalAt(propOffset, groupRegion, 'key');
const value = readValueAt(propOffset + 2, groupRegion, 'value');
const logical = getLogicalValue(value);
// Internal slots
if (key.type === 'NumberValue' && (0, runtime_types_1.isSInt14)(key.value) && key.value < 0) {
// Both the key and value are considered to be distinct internal
// slots, so we can use the key slot for storage (as long as it's
// storing a negative int14)
object.internalSlots[internalSlotIndex++] = key;
object.internalSlots[internalSlotIndex++] = logical;
continue;
}
if (key.type !== 'StringValue') {
return (0, utils_1.invalidOperation)('Expected property key to be string');
}
const keyStr = key.value;
if (logical.type !== 'DeletedValue') {
object.properties[keyStr] = logical;
}
}
// Next group, if there is one
if (dpNext.type === 'NullValue') {
break;
}
if (dpNext.type !== 'Pointer') {
return (0, utils_1.unexpected)();
}
// TODO: Test this path
groupRegion = [];
groupOffset = dpNext.offset;
const { size, typeCode } = readAllocationHeader(groupOffset, region);
groupSize = size;
(0, utils_1.hardAssert)(typeCode === runtime_types_1.TeTypeCode.TC_REF_PROPERTY_LIST);
region.push({
offset: groupOffset,
size: undefined,
content: {
type: 'Region',
regionName: `Child TsPropertyList`,
value: groupRegion
}
});
}
region.push({
offset: offset,
size: size,
content: {
type: 'Region',
regionName: `TsPropertyList`,
value: objRegion
}
});
return ref;
}
function decodeClass(region, offset, size) {
(0, utils_1.hardAssert)(size === 4);
const classRegion = [];
const constructorFunc = readLogicalAt(offset, classRegion, 'constructorFunc');
const staticProps = readLogicalAt(offset + 2, classRegion, 'staticProps');
region.push({
offset,
size,
content: {
type: 'Region',
regionName: 'Class',
value: classRegion
}
});
const value = {
type: 'ClassValue',
staticProps,
constructorFunc
};
processedAllocationsByOffset.set(offset, value);
return value;
}
function decodeClosure(region, offset, size, section) {
const memoryRegion = getAllocationMemoryRegion(section);
const allocationID = offsetToAllocationID(offset);
const closure = {
type: 'ClosureAllocation',
allocationID,
slots: [],
memoryRegion,
};
snapshotInfo.allocations.set(allocationID, closure);
const ref = {
type: 'ReferenceValue',
value: allocationID
};
processedAllocationsByOffset.set(offset, ref);
const closureRegion = [];
const length = size / 2;
for (let i = 0; i < length; i++) {
const itemOffset = offset + i * 2;
const itemRaw = buffer.readUInt16LE(itemOffset);
const item = decodeValue(itemRaw);
const logical = getLogicalValue(item);
closure.slots[i] = logical;
closureRegion.push({
offset: itemOffset,
size: 2,
content: {
type: 'LabeledValue',
label: `closure[${i}]`,
value: item
}
});
}
region.push({
offset,
size,
content: {
type: 'Region',
regionName: 'TsClosure',
value: closureRegion
}
});
return ref;
}
fu