microvium
Version:
A compact, embeddable scripting engine for microcontrollers for executing small scripts written in a subset of JavaScript.
896 lines (890 loc) • 62 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;
};
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.appendCustomInstruction = exports.customInstruction = exports.writeFunctionBody = void 0;
const IL = __importStar(require("./il"));
const utils_1 = require("./utils");
const runtime_types_1 = require("./runtime-types");
const stringify_il_1 = require("./stringify-il");
const binary_region_1 = require("./binary-region");
const visual_buffer_1 = require("./visual-buffer");
const formats = __importStar(require("./snapshot-binary-html-formats"));
const escape_html_1 = __importDefault(require("escape-html"));
const bytecode_opcodes_1 = require("./bytecode-opcodes");
const fs_1 = __importDefault(require("fs"));
const encode_snapshot_1 = require("./encode-snapshot");
/*
writeFunctionBody is essentially concerned with the layout and emission of
bytecode instructions. It is a fairly complex process, and is broken down into
several passes. The complexity comes in part from the fact that Jump
instructions have different sizes depending on how far they jump, but how far
they jump depends on the address layout which in turn depends on the sizes of
the instructions in between the jump origin and target. This is a classic
chicken-and-egg problem, and the solution is to do multiple passes, refining the
estimates on each pass.
- Pass 1: Use the maximum possible instruction sizes to do an initial layout
estimate. After this pass, instructions are allowed to get smaller but not
allowed to get bigger, meaning that jump instructions will be overestimated.
- Pass 2 with isFinal=false: Use the initial layout estimate to get a more
accurate upper bounds on the instruction sizes of instructions. In particular,
Jump instructions will now be estimated based on the actual distance of the
jump, although the distance could still get smaller. Like Pass 1, instructions
are still allowed to get smaller after this pass but not bigger, and the
addresses are not final.
- Pass 2 with isFinal=true: Call Pass 2 again for each instruction. The Jump
instructions in this pass may be smaller than in Pass 2 because they consider
the addresses with the smaller instructions that came out of the first Pass 2.
During this pass, the address of each emitted instruction is final, so this is
the pass where padding can be accurately calculated for instructions and
blocks that require alignment. Prior to this pass, the maximum padding is
assumed, since instructions (with their padding) are allowed to get smaller
but not bigger, and padding is unpredictable so we also assume the worst
padding until the final layout. The actual instructions can't be emitted yet
because as we're performing this pass, we might know the final addresses of
earlier instructions but not yet know the final addresses of later
instructions, so Jump instructions can't be emitted yet. Instructions are not
allowed to change size after this pass because their address layout is final.
- Pass 3: Using the final address layout of Pass 2, emit the actual
instructions. Since this is the only pass where we have knowledge of all other
instruction addresses in the function, this is the only pass where we can
correctly emit Jump instructions. But the Jump instructions need to remain
consistent with the final address layout of Pass 2, so the size of the jump
instruction is not determined by the exact distance of the jump, but rather by
the distance of the jump as estimated in Pass 2. This means that the Jump
instructions may be larger than they need to be, but they will never be
smaller than they need to be.
This is done on a per-function basis. Call operations do not change size, so we
do not need a similar process for inter-function calls like we do for jumps.
The output is appended to the given BinaryRegion, which is like a buffer but
also supports emitting future references to addresses that are not yet known,
which is used for Call operations since the address of other functions is not
yet known. The `ctx.offsetOfFunction` function returns the future address of a
function, which is used to emit the Call instruction.
*/
function writeFunctionBody(output, func, ctx, addressableReferences) {
const emitter = new InstructionEmitter();
const funcId = func.id;
const functionBodyStart = output.currentOffset;
const metaByOperation = new Map();
const metaByBlock = new Map();
const blockOutputOrder = []; // Will be filled in the first pass
const addressableBlocks = findAddressableBlocks(func, addressableReferences);
pass1();
// Run a second pass (for the first time) to refine the estimates
pass2(false);
// dumpInstructionEmitData('before-second-pass2.txt', func, blockOutputOrder, metaByOperation, metaByBlock);
// Run the second pass again to refine the layout further. This is
// particularly for the case of forward jumps, which were previously estimated
// based on the maximum size of future operations but can now be based on a
// better estimate of future operations
for (const m of metaByOperation.values()) {
m.addressEstimate = m.address;
m.sizeEstimate = m.size;
m.address = undefined;
m.size = undefined;
}
for (const m of metaByBlock.values()) {
m.paddingBeforeBlock = undefined;
m.addressEstimate = m.address;
m.address = undefined;
}
// On this run all the operations and blocks should have their final address,
// so that on the final pass we know exactly what offsets to use for jumps and
// branches.
pass2(true);
// dumpInstructionEmitData('before-output-pass.txt', func, blockOutputOrder, metaByOperation, metaByBlock);
// Output pass to generate bytecode
outputPass();
function pass1() {
const blockQueue = Object.keys(func.blocks);
ctx.preferBlockToBeNext = nextBlockID => {
const originalIndex = blockQueue.indexOf(nextBlockID);
if (originalIndex === -1) {
// The block position has already been secured, and can't be changed
return;
}
// Move it to the beginning of the queue
blockQueue.splice(originalIndex, 1);
blockQueue.unshift(nextBlockID);
};
// The entry must be first
ctx.preferBlockToBeNext(func.entryBlockID);
// In a first pass, we estimate the layout based on the maximum possible size
// of each instruction. Instructions such as JUMP can take different forms
// depending on the distance of the jump, and the distance of the JUMP in turn
// depends on size of other instructions in between the jump origin and
// target, which may include other jumps etc.
let addressEstimate = 0;
while (blockQueue.length) {
const blockId = blockQueue.shift();
const block = func.blocks[blockId];
blockOutputOrder.push(blockId); // The same order will be used for subsequent passes
// Addressable blocks have 0-3 bytes padding + 2 byte header
if (addressableBlocks.has(blockId)) {
addressEstimate += 3 + 2;
}
// Within the context of this block, operations can request that certain other blocks should be next
metaByBlock.set(blockId, {
addressEstimate,
address: undefined
});
for (const [operationIndex, op] of block.operations.entries()) {
const { maxSize, emitPass2 } = emitPass1(emitter, ctx, op);
const operationMeta = {
op,
ilAddress: { type: 'ProgramAddressValue', funcId, blockId, operationIndex },
addressEstimate,
address: undefined,
sizeEstimate: maxSize,
size: undefined,
emitPass2,
emitPass3: undefined
};
metaByOperation.set(op, operationMeta);
addressEstimate += maxSize;
}
}
// After this pass, the order is fixed
ctx.preferBlockToBeNext = undefined;
}
function pass2(isFinal) {
// Pass2 is where we calculate the final block addresses
let currentOperationMeta;
const ctx = {
get address() { return address; },
get ilAddress() { return currentOperationMeta.ilAddress; },
isFinal,
tentativeOffsetOfBlock: (blockId) => {
const targetBlock = (0, utils_1.notUndefined)(metaByBlock.get(blockId));
const blockAddress = targetBlock.addressEstimate;
const operationAddress = currentOperationMeta.addressEstimate;
const operationSize = currentOperationMeta.sizeEstimate;
const jumpFrom = operationAddress + operationSize;
// The jump offset is measured from the end of the current operation, but
// we don't know exactly how big it is so we take the worst case distance
const maxOffset = (blockAddress > jumpFrom
? blockAddress - (jumpFrom - operationSize) // Most positive
: blockAddress - jumpFrom); // Most negative
return maxOffset;
}
};
let address = 0;
for (const blockId of blockOutputOrder) {
const block = func.blocks[blockId];
const blockMeta = (0, utils_1.notUndefined)(metaByBlock.get(blockId));
blockMeta.paddingBeforeBlock = undefined;
// Addressable blocks have 0-3 bytes padding + 2 byte header
if (addressableBlocks.has(blockId)) {
if (isFinal) {
// Padding to 2 less than a 4 byte boundary.
const paddingAmount = (4 - ((address + 2) % 4)) % 4;
address += paddingAmount + 2;
(0, utils_1.hardAssert)(address % 4 === 0);
blockMeta.paddingBeforeBlock = paddingAmount;
}
else {
// Assume maximum padding because we don't know whether there will
// be or not until the address is final
address += 3 + 2;
blockMeta.paddingBeforeBlock = 3;
}
}
blockMeta.address = address;
for (const op of block.operations) {
const opMeta = (0, utils_1.notUndefined)(metaByOperation.get(op));
currentOperationMeta = opMeta;
const pass2Output = opMeta.emitPass2(ctx);
opMeta.emitPass3 = pass2Output.emitPass3;
opMeta.size = pass2Output.size;
(0, utils_1.hardAssert)(opMeta.size <= opMeta.sizeEstimate);
opMeta.address = address;
address += pass2Output.size;
}
}
}
function outputPass() {
let currentOperationMeta;
const innerCtx = {
region: output,
get address() { return currentOperationMeta.address; },
get absoluteAddress() { return output.currentOffset; },
offsetOfBlock(blockId) {
const targetBlock = (0, utils_1.notUndefined)(metaByBlock.get(blockId));
const blockAddress = targetBlock.address;
const operationAddress = currentOperationMeta.address;
const operationSize = currentOperationMeta.size;
const jumpFrom = operationAddress + operationSize;
const offset = blockAddress - jumpFrom;
return offset;
},
addressOfBlock(blockId) {
const targetBlock = (0, utils_1.notUndefined)(metaByBlock.get(blockId));
const blockAddress = targetBlock.address;
// The addresses here are actually relative to the function start
return functionBodyStart.map(functionBodyStart => functionBodyStart + blockAddress);
},
declareAddressablePoint(ilAddress, physicalAddress, debugType) {
// This code seems a little crazy. So many layers of futures and
// wrappers. Is it really all necessary?
const ref = addressableReferences.get((0, encode_snapshot_1.programAddressToKey)(ilAddress)) ?? (0, utils_1.unexpected)();
const referenceable = physicalAddress.map(physicalAddress => {
const ref = {
offset: binary_region_1.Future.create(physicalAddress),
debugName: `${debugType} (${ilAddress.funcId}, ${ilAddress.blockId}, ${ilAddress.operationIndex})`,
getPointer(sourceRegion, debugName) {
// Should be 4-byte aligned
(0, utils_1.hardAssert)((physicalAddress & 0xFFFC) === physicalAddress);
// Bytecode pointer encoding. Note that this is independent of the
// sourceRegion because bytecode pointers always look the same.
const value = physicalAddress | 1;
return binary_region_1.Future.create(value);
},
};
return ref;
});
ref.assign(referenceable);
},
};
for (const blockId of blockOutputOrder) {
const block = func.blocks[blockId];
const blockMeta = metaByBlock.get(blockId) ?? (0, utils_1.unexpected)();
// Addressable blocks have 0-3 bytes padding + 2 byte header
if (addressableBlocks.has(blockId)) {
const ilAddress = addressableBlocks.get(blockId) ?? (0, utils_1.unexpected)();
// Padding to 2 less than a 4 byte boundary.
const paddingAmount = blockMeta.paddingBeforeBlock ?? (0, utils_1.unexpected)();
;
if (blockMeta.paddingBeforeBlock) {
output.append(paddingAmount, 'pad-to-(4n-2)', formats.paddingRow);
}
// Header, so that block is a valid addressable. This is the same header
// used by an async resume point. The header is mainly required by the
// bytecode decoder which traces any addresses to figure out what they
// are. In particular, this code is required to support the preservation
// of async stack variables in closures which may be encoded in the
// snapshot, which may include async catch blocks, but for simplicity
// I've applied it to all catch blocks.
const currentAddress = innerCtx.absoluteAddress;
const containingFunctionOffset = ctx.offsetOfFunction(ilAddress.funcId);
const functionHeaderWord = containingFunctionOffset.bind(containingFunctionOffset => currentAddress.map(currentAddress => {
const backDistance = currentAddress + 2 - containingFunctionOffset;
(0, utils_1.hardAssert)(backDistance > 0 && backDistance % 4 === 0);
(0, utils_1.hardAssert)((backDistance & 0x1FFC) === backDistance);
const functionHeaderWord = 0
| runtime_types_1.TeTypeCode.TC_REF_FUNCTION << 12 // TypeCode
| 1 << 11 // Flag to indicate continuation function
| backDistance >> 2; // Encodes number of quad-words to go back to find the original function
return functionHeaderWord;
}));
innerCtx.region.append(functionHeaderWord, 'Block header', formats.uHex16LERow);
// Addressable addresses are 4-byte aligned. The above padding should have ensured that
output.currentOffset.map(o => (0, utils_1.hardAssert)(o % 4 === 0));
innerCtx.declareAddressablePoint(ilAddress, output.currentOffset, 'Block address');
}
// Assert that the address we're actually putting the block at matches what we calculated
output.currentOffset.map(o => functionBodyStart.map(s => o - s === blockMeta.address));
ctx.addName(output.currentOffset, 'block', blockId);
for (const op of block.operations) {
const opMeta = (0, utils_1.notUndefined)(metaByOperation.get(op));
currentOperationMeta = opMeta;
const offsetBefore = output.currentOffset;
opMeta.emitPass3(innerCtx);
const offsetAfter = output.currentOffset;
offsetBefore.bind(offsetBefore_ => offsetAfter.map(offsetAfter => {
const measuredSize = offsetAfter - offsetBefore_;
(0, utils_1.hardAssert)(measuredSize === opMeta.size, `Operation changed from committed size of ${opMeta.size} to ${measuredSize}. at ${offsetBefore_}, ${op}, ${output}`);
}));
if (op.sourceLoc) {
ctx.sourceMapAdd?.({
start: offsetBefore,
end: offsetAfter,
source: op.sourceLoc,
op,
});
}
}
}
}
}
exports.writeFunctionBody = writeFunctionBody;
;
class InstructionEmitter {
operationArrayNew(_ctx, op) {
return instructionEx2Unsigned(bytecode_opcodes_1.vm_TeOpcodeEx2.VM_OP2_ARRAY_NEW, (op.staticInfo && op.staticInfo.minCapacity) || 0, op);
}
operationBinOp(_ctx, op, param) {
const [opcode1, opcode2] = ilBinOpCodeToVm[param];
return instructionPrimary(opcode1, opcode2, op);
}
operationBranch(ctx, op, consequentTargetBlockID, alternateTargetBlockID) {
ctx.preferBlockToBeNext(alternateTargetBlockID);
// Note: branch IL instructions are a bit more complicated than most because
// they consist of two bytecode instructions
return {
maxSize: 6,
emitPass2: ctx => {
let tentativeConseqOffset = ctx.tentativeOffsetOfBlock(consequentTargetBlockID);
/* 😨😨😨 The offset is measured from the end of the bytecode
* instruction, but since this is a composite instruction, we need to
* compensate for the fact that we're branching from halfway through the
* composite instruction.
*/
if (tentativeConseqOffset < 0) {
tentativeConseqOffset += 3; // 3 is the max size of the jump part of the composite instruction
}
const tentativeConseqOffsetIsFar = !(0, runtime_types_1.isSInt8)(tentativeConseqOffset);
const sizeOfBranchInstr = tentativeConseqOffsetIsFar ? 3 : 2;
const tentativeAltOffset = ctx.tentativeOffsetOfBlock(alternateTargetBlockID);
const tentativeAltOffsetDistance = getJumpDistance(tentativeAltOffset);
const sizeOfJumpInstr = tentativeAltOffsetDistance === 'far' ? 3 :
tentativeAltOffsetDistance === 'close' ? 2 :
tentativeAltOffsetDistance === 'zero' ? 0 :
(0, utils_1.unexpected)();
const size = sizeOfBranchInstr + sizeOfJumpInstr;
return {
size,
emitPass3: ctx => {
let label = '';
let binary = [];
const finalOffsetOfConseq = ctx.offsetOfBlock(consequentTargetBlockID) + sizeOfJumpInstr;
const finalOffsetOfAlt = ctx.offsetOfBlock(alternateTargetBlockID);
(0, utils_1.hardAssert)(Math.abs(finalOffsetOfConseq) <= Math.abs(tentativeConseqOffset));
(0, utils_1.hardAssert)(Math.abs(finalOffsetOfAlt) <= Math.abs(tentativeAltOffset));
// Stick to our committed shape for the BRANCH instruction
if (tentativeConseqOffsetIsFar) {
label += `VM_OP3_BRANCH_2(0x${finalOffsetOfConseq.toString(16)})`;
binary.push((bytecode_opcodes_1.vm_TeOpcode.VM_OP_EXTENDED_3 << 4) | bytecode_opcodes_1.vm_TeOpcodeEx3.VM_OP3_BRANCH_2, finalOffsetOfConseq & 0xFF, (finalOffsetOfConseq >> 8) & 0xFF);
}
else {
label += `VM_OP2_BRANCH_1(0x${finalOffsetOfConseq.toString(16)})`;
binary.push((bytecode_opcodes_1.vm_TeOpcode.VM_OP_EXTENDED_2 << 4) | bytecode_opcodes_1.vm_TeOpcodeEx2.VM_OP2_BRANCH_1, finalOffsetOfConseq & 0xFF);
}
// Stick to our committed shape for the JUMP instruction
switch (tentativeAltOffsetDistance) {
case 'zero': break; // No instruction at all
case 'close': {
label += `, VM_OP2_JUMP_1(0x${finalOffsetOfAlt.toString(16)})`;
binary.push((bytecode_opcodes_1.vm_TeOpcode.VM_OP_EXTENDED_2 << 4) | bytecode_opcodes_1.vm_TeOpcodeEx2.VM_OP2_JUMP_1, finalOffsetOfAlt & 0xFF);
break;
}
case 'far': {
label += `, VM_OP3_JUMP_2(0x${finalOffsetOfAlt.toString(16)})`;
binary.push((bytecode_opcodes_1.vm_TeOpcode.VM_OP_EXTENDED_3 << 4) | bytecode_opcodes_1.vm_TeOpcodeEx3.VM_OP3_JUMP_2, finalOffsetOfAlt & 0xFF, (finalOffsetOfAlt >> 8) & 0xFF);
break;
}
default: (0, utils_1.assertUnreachable)(tentativeAltOffsetDistance);
}
const html = (0, escape_html_1.default)((0, stringify_il_1.stringifyOperation)(op));
ctx.region.append({ binary: (0, visual_buffer_1.BinaryData)(binary), html }, label, formats.preformatted2);
}
};
}
};
}
operationClosureNew(ctx, op) {
return instructionEx1(bytecode_opcodes_1.vm_TeOpcodeEx1.VM_OP1_CLOSURE_NEW, op);
}
operationCall(ctx, op, argCount, isVoidCall) {
const staticInfo = op.staticInfo;
if (staticInfo?.target) {
const target = staticInfo.target;
if (target.type === 'FunctionValue') {
const functionID = target.value;
if (staticInfo.shortCall) {
// Short calls are single-byte instructions that use a nibble to
// reference into the short-call table, which provides the information
// about the function target and argument count
const shortCallIndex = ctx.getShortCallIndex({ type: 'InternalFunction', functionID: target.value, argCount });
return instructionPrimary(bytecode_opcodes_1.vm_TeOpcode.VM_OP_CALL_1, shortCallIndex, op);
}
else {
if (argCount <= 15) {
const targetOffset = ctx.offsetOfFunction(functionID);
return customInstruction(op, bytecode_opcodes_1.vm_TeOpcode.VM_OP_CALL_5, argCount, {
type: 'UInt16',
value: targetOffset
});
}
else {
/* Fall back to dynamic call */
}
}
}
else if (target.type === 'HostFunctionValue') {
const hostFunctionID = target.value;
const hostFunctionIndex = ctx.getImportIndexOfHostFunctionID(hostFunctionID);
if (staticInfo.shortCall) {
// Short calls are single-byte instructions that use a nibble to
// reference into the short-call table, which provides the information
// about the function target and argument count
const shortCallIndex = ctx.getShortCallIndex({ type: 'HostFunction', hostFunctionIndex, argCount });
return instructionPrimary(bytecode_opcodes_1.vm_TeOpcode.VM_OP_CALL_1, shortCallIndex, op);
}
else {
return instructionEx2Unsigned(bytecode_opcodes_1.vm_TeOpcodeEx2.VM_OP2_CALL_HOST, hostFunctionIndex, op);
}
}
else {
return (0, utils_1.invalidOperation)('Static call target can only be a function');
}
}
argCount = (0, runtime_types_1.UInt8)(argCount);
if (argCount > 127) {
(0, utils_1.invalidOperation)(`Too many arguments: ${argCount}`);
}
// The void-call flag is the high bit in the argument count
const param = argCount | (isVoidCall ? 0x80 : 0);
return instructionEx2Unsigned(bytecode_opcodes_1.vm_TeOpcodeEx2.VM_OP2_CALL_3, param, op);
}
operationNew(ctx, op, argCount) {
return customInstruction(op, bytecode_opcodes_1.vm_TeOpcode.VM_OP_EXTENDED_1, bytecode_opcodes_1.vm_TeOpcodeEx1.VM_OP1_NEW, {
type: 'UInt8', value: (0, runtime_types_1.UInt8)(argCount)
});
}
operationJump(ctx, op, targetBlockId) {
ctx.preferBlockToBeNext(targetBlockId);
return {
maxSize: 3,
emitPass2: ctx => {
const tentativeOffset = ctx.tentativeOffsetOfBlock(targetBlockId);
const distance = getJumpDistance(tentativeOffset);
const size = distance === 'zero' ? 0 :
distance === 'close' ? 2 :
distance === 'far' ? 3 :
(0, utils_1.unexpected)();
return {
size,
emitPass3: ctx => {
const offset = ctx.offsetOfBlock(targetBlockId);
(0, utils_1.hardAssert)(Math.abs(offset) <= Math.abs(tentativeOffset));
// Stick to our committed shape
switch (distance) {
case 'zero': return; // Jumping to where we are already, so no instruction required
case 'close':
appendInstructionEx2Signed(ctx.region, bytecode_opcodes_1.vm_TeOpcodeEx2.VM_OP2_JUMP_1, offset, op);
break;
case 'far':
appendInstructionEx3Signed(ctx.region, bytecode_opcodes_1.vm_TeOpcodeEx3.VM_OP3_JUMP_2, offset, op);
break;
default: return (0, utils_1.assertUnreachable)(distance);
}
}
};
}
};
}
operationScopePush(ctx, op, count) {
return customInstruction(op, bytecode_opcodes_1.vm_TeOpcode.VM_OP_EXTENDED_2, bytecode_opcodes_1.vm_TeOpcodeEx2.VM_OP2_EXTENDED_4, { type: 'UInt8', value: bytecode_opcodes_1.vm_TeOpcodeEx4.VM_OP4_SCOPE_PUSH }, { type: 'UInt8', value: count });
}
operationStartTry(ctx, op, catchBlockId) {
// The StartTry instruction is always 4 bytes
const size = 4;
return {
maxSize: size,
emitPass2: () => ({
size,
emitPass3: ctx => {
const address = ctx.addressOfBlock(catchBlockId);
// We already signalled above that the catch block must be 2-byte aligned
address.map(address => (0, utils_1.hardAssert)((address & 1) === 0));
appendCustomInstruction(ctx.region, op, bytecode_opcodes_1.vm_TeOpcode.VM_OP_EXTENDED_2, bytecode_opcodes_1.vm_TeOpcodeEx2.VM_OP2_EXTENDED_4, {
type: 'UInt8',
value: bytecode_opcodes_1.vm_TeOpcodeEx4.VM_OP4_START_TRY
}, {
type: 'UInt16',
value: address.map(address => address + 1) // Encode with a +1 so that when we push to the stack the GC will ignore it
});
}
})
};
}
operationEndTry(ctx, op) {
return instructionEx4(bytecode_opcodes_1.vm_TeOpcodeEx4.VM_OP4_END_TRY, op);
}
operationScopeClone(ctx, op) {
return customInstruction(op, bytecode_opcodes_1.vm_TeOpcode.VM_OP_EXTENDED_3, bytecode_opcodes_1.vm_TeOpcodeEx3.VM_OP3_SCOPE_CLONE);
}
operationScopeDiscard(ctx, op) {
return customInstruction(op, bytecode_opcodes_1.vm_TeOpcode.VM_OP_EXTENDED_3, bytecode_opcodes_1.vm_TeOpcodeEx3.VM_OP3_SCOPE_DISCARD);
}
operationScopeNew(ctx, op, count) {
return customInstruction(op, bytecode_opcodes_1.vm_TeOpcode.VM_OP_EXTENDED_1, bytecode_opcodes_1.vm_TeOpcodeEx1.VM_OP1_SCOPE_NEW, { type: 'UInt8', value: count });
}
operationScopePop(ctx, op) {
return customInstruction(op, bytecode_opcodes_1.vm_TeOpcode.VM_OP_EXTENDED_2, bytecode_opcodes_1.vm_TeOpcodeEx2.VM_OP2_EXTENDED_4, { type: 'UInt8', value: bytecode_opcodes_1.vm_TeOpcodeEx4.VM_OP4_SCOPE_POP });
}
operationScopeSave(ctx, op) {
return customInstruction(op, bytecode_opcodes_1.vm_TeOpcode.VM_OP_EXTENDED_2, bytecode_opcodes_1.vm_TeOpcodeEx2.VM_OP2_EXTENDED_4, { type: 'UInt8', value: bytecode_opcodes_1.vm_TeOpcodeEx4.VM_OP4_SCOPE_SAVE });
}
operationClassCreate(ctx, op) {
return instructionEx4(bytecode_opcodes_1.vm_TeOpcodeEx4.VM_OP4_CLASS_CREATE, op);
}
operationAsyncReturn(ctx, op) {
return instructionEx4(bytecode_opcodes_1.vm_TeOpcodeEx4.VM_OP4_ASYNC_RETURN, op);
}
operationAsyncComplete(ctx, op) {
return instructionEx4(bytecode_opcodes_1.vm_TeOpcodeEx4.VM_OP4_ASYNC_COMPLETE, op);
}
operationAsyncResume(outerCtx, op, slotCount, catchTarget) {
/*
The VM_OP3_ASYNC_RESUME instruction is the first instruction to be executed
in the continuation of an async function. In the bytecode, to make the
resume instruction callback, its address must be aligned to 4 bytes and it
must be preceded by a function header.
*/
return {
maxSize: +3 // 0-3 bytes padding for function header
+ 2 // 2 bytes for function header
+ 3 // 3 byte for VM_OP3_ASYNC_RESUME instruction
,
emitPass2: ctx => {
const containingFunctionOffset = outerCtx.offsetOfFunction(ctx.ilAddress.funcId);
const ilAddress = ctx.ilAddress;
// Address aligned such that the _end_ of the function header is 4-byte
// aligned.
const alignedAddress = ((ctx.address + 3 + 2) & 0xFFFC) - 2;
const padding = ctx.isFinal
? alignedAddress - ctx.address
: 3; // If the positioning is not final, the padding could increase later so assume worst
(0, utils_1.hardAssert)(padding >= 0 && padding <= 3);
const size = +padding
+ 2 // function header
+ 3; // VM_OP3_ASYNC_RESUME instruction
return {
size,
emitPass3: ctx => {
if (padding) {
ctx.region.append(padding, 'Padding before function header', formats.paddingRow);
}
const currentAddress = ctx.absoluteAddress;
const functionHeaderWord = containingFunctionOffset.bind(containingFunctionOffset => currentAddress.map(currentAddress => {
const backDistance = currentAddress + 2 - containingFunctionOffset;
(0, utils_1.hardAssert)(backDistance > 0 && backDistance % 4 === 0);
(0, utils_1.hardAssert)((backDistance & 0x1FFC) === backDistance);
const functionHeaderWord = 0
| runtime_types_1.TeTypeCode.TC_REF_FUNCTION << 12 // TypeCode
| 1 << 11 // Flag to indicate continuation function
| backDistance >> 2; // Encodes number of quad-words to go back to find the original function
return functionHeaderWord;
}));
ctx.region.append(functionHeaderWord, 'Continuation header', formats.uHex16LERow);
// Note: the address of the resume point is the address after the
// function header, which is also the address of the resume
// instruction itself. We need to associate the logical IL address
// with the absolute bytecode address that satisfies it.
ctx.declareAddressablePoint(ilAddress, ctx.absoluteAddress, 'Resume point');
const html = (0, escape_html_1.default)((0, stringify_il_1.stringifyOperation)(op));
const binary = [
(0, runtime_types_1.UInt4)(bytecode_opcodes_1.vm_TeOpcodeEx3.VM_OP3_ASYNC_RESUME) |
((0, runtime_types_1.UInt4)(bytecode_opcodes_1.vm_TeOpcode.VM_OP_EXTENDED_3) << 4),
(0, runtime_types_1.UInt8)(slotCount),
(0, runtime_types_1.UInt8)(catchTarget),
];
ctx.region.append({ html, binary }, `VM_OP3_ASYNC_RESUME(${slotCount})`, formats.preformatted(3));
}
};
}
};
}
operationAwaitCall(ctx, op, argCount) {
return customInstruction(op, bytecode_opcodes_1.vm_TeOpcode.VM_OP_EXTENDED_3, bytecode_opcodes_1.vm_TeOpcodeEx3.VM_OP3_AWAIT_CALL, { type: 'UInt8', value: (0, runtime_types_1.UInt7)(argCount) });
}
operationAwait(ctx, op) {
return customInstruction(op, bytecode_opcodes_1.vm_TeOpcode.VM_OP_EXTENDED_3, bytecode_opcodes_1.vm_TeOpcodeEx3.VM_OP3_AWAIT);
}
operationAsyncStart(ctx, op, slotCount, captureParent) {
const param = (0, runtime_types_1.UInt7)(slotCount) | (captureParent ? 0x80 : 0);
return customInstruction(op, bytecode_opcodes_1.vm_TeOpcode.VM_OP_EXTENDED_2, bytecode_opcodes_1.vm_TeOpcodeEx2.VM_OP2_EXTENDED_4, { type: 'UInt8', value: bytecode_opcodes_1.vm_TeOpcodeEx4.VM_OP4_ASYNC_START }, { type: 'UInt8', value: param });
}
operationTypeCodeOf(ctx, op) {
return instructionEx4(bytecode_opcodes_1.vm_TeOpcodeEx4.VM_OP4_TYPE_CODE_OF, op);
}
operationLiteral(ctx, op, param) {
const smallLiteralCode = tryGetSmallLiteralCode(param);
if (smallLiteralCode !== undefined) {
return instructionPrimary(bytecode_opcodes_1.vm_TeOpcode.VM_OP_LOAD_SMALL_LITERAL, smallLiteralCode, op);
}
else {
return instructionEx3Unsigned(bytecode_opcodes_1.vm_TeOpcodeEx3.VM_OP3_LOAD_LITERAL, ctx.encodeValue(param), op);
}
function tryGetSmallLiteralCode(param) {
switch (param.type) {
case 'NullValue': return bytecode_opcodes_1.vm_TeSmallLiteralValue.VM_SLV_NULL;
case 'UndefinedValue': return bytecode_opcodes_1.vm_TeSmallLiteralValue.VM_SLV_UNDEFINED;
case 'NumberValue':
if (Object.is(param.value, -0))
return undefined;
switch (param.value) {
case -1: return bytecode_opcodes_1.vm_TeSmallLiteralValue.VM_SLV_INT_MINUS_1;
case 0: return bytecode_opcodes_1.vm_TeSmallLiteralValue.VM_SLV_INT_0;
case 1: return bytecode_opcodes_1.vm_TeSmallLiteralValue.VM_SLV_INT_1;
case 2: return bytecode_opcodes_1.vm_TeSmallLiteralValue.VM_SLV_INT_2;
case 3: return bytecode_opcodes_1.vm_TeSmallLiteralValue.VM_SLV_INT_3;
case 4: return bytecode_opcodes_1.vm_TeSmallLiteralValue.VM_SLV_INT_4;
case 5: return bytecode_opcodes_1.vm_TeSmallLiteralValue.VM_SLV_INT_5;
default: return undefined;
}
case 'StringValue':
return undefined;
case 'BooleanValue':
return param.value
? bytecode_opcodes_1.vm_TeSmallLiteralValue.VM_SLV_TRUE
: bytecode_opcodes_1.vm_TeSmallLiteralValue.VM_SLV_FALSE;
case 'EphemeralFunctionValue':
case 'EphemeralObjectValue':
case 'ClassValue':
case 'FunctionValue':
case 'HostFunctionValue':
case 'DeletedValue':
case 'ReferenceValue':
case 'ProgramAddressValue':
case 'ResumePoint':
case 'NoOpFunction':
return undefined;
default:
return (0, utils_1.assertUnreachable)(param);
}
}
}
operationLoadArg(_ctx, op, index) {
if ((0, runtime_types_1.isUInt4)(index)) {
return instructionPrimary(bytecode_opcodes_1.vm_TeOpcode.VM_OP_LOAD_ARG_1, index, op);
}
else {
(0, utils_1.hardAssert)((0, runtime_types_1.isUInt8)(index));
return instructionEx2Unsigned(bytecode_opcodes_1.vm_TeOpcodeEx2.VM_OP2_LOAD_ARG_2, index, op);
}
}
operationLoadGlobal(ctx, op, globalSlotID) {
const slotIndex = ctx.indexOfGlobalSlot(globalSlotID);
return instructionEx3Unsigned(bytecode_opcodes_1.vm_TeOpcodeEx3.VM_OP3_LOAD_GLOBAL_3, slotIndex, op);
}
operationArrayGet(_ctx, _op) {
// The ArrayGet operation can only really be emitted by an optimizer,
// otherwise normal functioning will just use `ObjectGet`. So we
// don't support this yet.
return (0, utils_1.notImplemented)();
}
operationArraySet(_ctx, _op) {
// The ArraySet operation can only really be emitted by an optimizer,
// otherwise normal functioning will just use `ObjectSet`. So we
// don't support this yet.
return (0, utils_1.notImplemented)();
}
operationLoadScoped(ctx, op, index) {
if ((0, runtime_types_1.isUInt4)(index)) {
return instructionPrimary(bytecode_opcodes_1.vm_TeOpcode.VM_OP_LOAD_SCOPED_1, index, op);
}
else if ((0, runtime_types_1.isUInt8)(index)) {
return instructionEx2Unsigned(bytecode_opcodes_1.vm_TeOpcodeEx2.VM_OP2_LOAD_SCOPED_2, index, op);
}
else if ((0, runtime_types_1.isUInt16)(index)) {
return instructionEx3Unsigned(bytecode_opcodes_1.vm_TeOpcodeEx3.VM_OP3_LOAD_SCOPED_3, index, op);
}
else {
return (0, utils_1.unexpected)();
}
}
operationLoadVar(ctx, op, index) {
// In the IL, the index is relative to the stack base, while in the
// bytecode, it's relative to the stack pointer
const positionRelativeToSP = op.stackDepthBefore - index - 1;
if ((0, runtime_types_1.isUInt4)(positionRelativeToSP)) {
return instructionPrimary(bytecode_opcodes_1.vm_TeOpcode.VM_OP_LOAD_VAR_1, positionRelativeToSP, op);
}
if ((0, runtime_types_1.isUInt8)(positionRelativeToSP)) {
return instructionEx2Unsigned(bytecode_opcodes_1.vm_TeOpcodeEx2.VM_OP2_LOAD_VAR_2, positionRelativeToSP, op);
}
else {
return (0, utils_1.invalidOperation)('Variable index out of bounds: ' + index);
}
}
operationLoadReg(ctx, op, name) {
switch (name) {
case 'closure': return instructionEx4(bytecode_opcodes_1.vm_TeOpcodeEx4.VM_OP4_LOAD_REG_CLOSURE, op);
default: (0, utils_1.unexpected)();
}
}
operationNop(_ctx, op, nopSize) {
if (nopSize < 2)
return (0, utils_1.invalidOperation)('Cannot have less than 2-byte NOP instruction');
return fixedSizeInstruction(nopSize, region => {
if (nopSize === 2) {
// JUMP (0)
appendInstructionEx2Signed(region, bytecode_opcodes_1.vm_TeOpcodeEx2.VM_OP2_JUMP_1, 0, op);
return;
}
// JUMP
const offset = nopSize - 3; // The nop size less the size of the jump
(0, utils_1.hardAssert)((0, runtime_types_1.isSInt16)(offset));
const label = `VM_OP3_JUMP_2(${offset})`;
const value = binary_region_1.Future.map(offset, offset => {
const html = (0, escape_html_1.default)((0, stringify_il_1.stringifyOperation)(op));
const binary = [
(bytecode_opcodes_1.vm_TeOpcode.VM_OP_EXTENDED_3 << 4) | bytecode_opcodes_1.vm_TeOpcodeEx3.VM_OP3_JUMP_2,
offset & 0xFF,
(offset >> 8) & 0xFF
];
while (binary.length < nopSize)
binary.push(0);
return { html, binary };
});
region.append(value, label, formats.preformatted3);
});
}
operationObjectGet(_ctx, op) {
return instructionEx1(bytecode_opcodes_1.vm_TeOpcodeEx1.VM_OP1_OBJECT_GET_1, op);
}
operationObjectNew(_ctx, op) {
return instructionEx1(bytecode_opcodes_1.vm_TeOpcodeEx1.VM_OP1_OBJECT_NEW, op);
}
operationObjectSet(_ctx, op) {
return instructionEx1(bytecode_opcodes_1.vm_TeOpcodeEx1.VM_OP1_OBJECT_SET_1, op);
}
operationPop(_ctx, op, count) {
if (count > 1) {
return customInstruction(op, bytecode_opcodes_1.vm_TeOpcode.VM_OP_EXTENDED_3, bytecode_opcodes_1.vm_TeOpcodeEx3.VM_OP3_POP_N, { type: 'UInt8', value: count });
}
else {
return instructionEx1(bytecode_opcodes_1.vm_TeOpcodeEx1.VM_OP1_POP, op);
}
}
operationReturn(_ctx, op) {
if (op.opcode !== 'Return')
return (0, utils_1.unexpected)();
return instructionEx1(bytecode_opcodes_1.vm_TeOpcodeEx1.VM_OP1_RETURN, op);
}
operationThrow(_ctx, op) {
if (op.opcode !== 'Throw')
return (0, utils_1.unexpected)();
return instructionEx1(bytecode_opcodes_1.vm_TeOpcodeEx1.VM_OP1_THROW, op);
}
operationObjectKeys(_ctx, op) {
if (op.opcode !== 'ObjectKeys')
return (0, utils_1.unexpected)();
return instructionEx4(bytecode_opcodes_1.vm_TeOpcodeEx4.VM_OP4_OBJECT_KEYS, op);
}
operationUint8ArrayNew(_ctx, op) {
if (op.opcode !== 'Uint8ArrayNew')
return (0, utils_1.unexpected)();
return instructionEx4(bytecode_opcodes_1.vm_TeOpcodeEx4.VM_OP4_UINT8_ARRAY_NEW, op);
}
operationStoreGlobal(ctx, op, globalSlotID) {
const index = ctx.indexOfGlobalSlot(globalSlotID);
(0, utils_1.hardAssert)((0, runtime_types_1.isUInt16)(index));
return instructionEx3Unsigned(bytecode_opcodes_1.vm_TeOpcodeEx3.VM_OP3_STORE_GLOBAL_3, index, op);
}
operationStoreScoped(ctx, op, index) {
if ((0, runtime_types_1.isUInt4)(index)) {
return instructionPrimary(bytecode_opcodes_1.vm_TeOpcode.VM_OP_STORE_SCOPED_1, index, op);
}
else if ((0, runtime_types_1.isUInt8)(index)) {
return instructionEx2Unsigned(bytecode_opcodes_1.vm_TeOpcodeEx2.VM_OP2_STORE_SCOPED_2, index, op);
}
else {
(0, utils_1.hardAssert)((0, runtime_types_1.isUInt16)(index));
return instructionEx3Unsigned(bytecode_opcodes_1.vm_TeOpcodeEx3.VM_OP3_STORE_SCOPED_3, index, op);
}
}
operationStoreVar(_ctx, op, index) {
// Note: the index is relative to the stack depth _after_ popping
const indexRelativeToSP = op.stackDepthBefore - 2 - index;
if ((0, runtime_types_1.isUInt4)(indexRelativeToSP)) {
return instructionPrimary(bytecode_opcodes_1.vm_TeOpcode.VM_OP_STORE_VAR_1, indexRelativeToSP, op);
}
if (!(0, runtime_types_1.isUInt8)(indexRelativeToSP)) {
return (0, utils_1.invalidOperation)('Too many stack variables');
}
return instructionEx2Unsigned(bytecode_opcodes_1.vm_TeOpcodeEx2.VM_OP2_STORE_VAR_2, indexRelativeToSP, op);
}
operationUnOp(_ctx, op, param) {
const [opcode1, opcode2] = ilUnOpCodeToVm[param];
return instructionPrimary(opcode1, opcode2, op);
}
operationEnqueueJob(_ctx, op) {
return instructionEx4(bytecode_opcodes_1.vm_TeOpcodeEx4.VM_OP4_ENQUEUE_JOB, op);
}
}
function instructionPrimary(opcode, param, op) {
(0, utils_1.hardAssert)((0, runtime_types_1.isUInt4)(opcode));
(0, utils_1.hardAssert)((0, runtime_types_1.isUInt4)(param));
const label = `${bytecode_opcodes_1.vm_TeOpcode[opcode]}(0x${param.toString(16)})`;
const html = (0, escape_html_1.default)((0, stringify_il_1.stringifyOperation)(op));
const binary = (0, visual_buffer_1.BinaryData)([(opcode << 4) | param]);
return fixedSizeInstruction(1, r => r.append({ binary, html }, label, formats.preformatted1));
}
function instructionEx1(opcode, op) {
return fixedSizeInstruction(1, r => appendInstructionEx1(r, opcode, op));
}
function appendInstructionEx1(region, opcode, op) {
appendCustomInstruction(region, op, bytecode_opcodes_1.vm_TeOpcode.VM_OP_EXTENDED_1, opcode);
}
function instructionEx2Unsigned(opcode, param, op) {
return fixedSizeInstruction(2, r => appendInstructionEx2Unsigned(r, opcode, param, op));
}
function appendInstructionEx2Signed(region, opcode, param, op) {
appendCustomInstruction(region, op, bytecode_opcodes_1.vm_TeOpcode.VM_OP_EXTENDED_2, opcode, { type: 'SInt8', value: param });
}
function appendInstructionEx2Unsigned(region, opcode, param, op) {
appendCustomInstruction(region, op, bytecode_opcodes_1.vm_TeOpcode.VM_OP_EXTENDED_2, opcode, { type: 'UInt8', value: param });
}
function instructionEx3Unsigned(opcode, param, op) {
return fixedSizeInstruction(3, r => appendInstructionEx3Unsigned(r, opcode, param, op));
}
function appendInstructionEx3Signed(region, opcode, param, op) {
appendCustomInstruction(region, op, bytecode_opcodes_1.vm_TeOpcode.VM_OP_EXTENDED_3, opcode, { type: 'SInt16', value: param });
}
function appendInstructionEx3Unsigned(region, opcode, param, op) {
appendCustomInstruction(region, op, bytecode_opcodes_1.vm_TeOpcode.VM_OP_EXTENDED_3, opcode, { type: 'UInt16', value: param });
}
function appendInstructionEx4(region, opcode, op) {
appendCustomInstruction(region, op, bytecode_opcodes_1.vm_TeOpcode.VM_OP_EXTENDED_2, bytecode_opcodes_1.vm_TeOpcodeEx2.VM_OP2_EXTENDED_4, {
type: 'UInt8',
value: opcode
});
}
function instructionEx4(opcode, op) {
return fixedSizeInstruction(2, r => appendInstructionEx4(r, opcode, op));
}
function customInstruction(op, nibble1, nibble2, ...payload) {
let size = 1;
for (const payloadPart of payload) {
switch (payloadPart.type) {
case 'UInt8':
size += 1;
break;
case 'SInt8':
size += 1;
break;
case 'UInt16':
size += 2;
break;
case 'SInt16':
size += 2;
break;
default: return (0, utils_1.assertUnreachable)(payloadPart);