microvium
Version:
A compact, embeddable scripting engine for microcontrollers for executing small scripts written in a subset of JavaScript.
1,010 lines • 155 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 __exportStar = (this && this.__exportStar) || function(m, exports) {
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
};
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.VirtualMachine = void 0;
const IL = __importStar(require("./il"));
const VM = __importStar(require("./virtual-machine-types"));
const lodash_1 = __importDefault(require("lodash"));
const utils_1 = require("./utils");
const src_to_il_1 = require("./src-to-il/src-to-il");
const stringify_il_1 = require("./stringify-il");
const deep_freeze_1 = __importDefault(require("deep-freeze"));
const runtime_types_1 = require("./runtime-types");
const encode_snapshot_1 = require("./encode-snapshot");
const il_opcodes_1 = require("./il-opcodes");
__exportStar(require("./virtual-machine-types"), exports);
const fs_1 = __importDefault(require("fs"));
var ScopeVariablesReference;
(function (ScopeVariablesReference) {
ScopeVariablesReference[ScopeVariablesReference["GLOBALS"] = 1] = "GLOBALS";
ScopeVariablesReference[ScopeVariablesReference["FRAME"] = 2] = "FRAME";
ScopeVariablesReference[ScopeVariablesReference["OPERATION"] = 3] = "OPERATION";
})(ScopeVariablesReference || (ScopeVariablesReference = {}));
;
class VirtualMachine {
constructor(resumeFromSnapshot, resolveFFIImport, opts, debugServer) {
this.resolveFFIImport = resolveFFIImport;
this.allocations = new Map();
this.nextHeapID = 1;
this.globalVariables = new Map();
this.globalSlots = new Map();
this.hostFunctions = new Map();
this.catchTarget = IL.undefinedValue;
this.functions = new Map();
this.exports = new Map();
// Ephemeral functions are functions that are only relevant in the current
// epoch, and will throw as "not available" in the next epoch (after
// snapshotting).
this.ephemeralFunctions = new Map();
this.ephemeralObjects = new Map();
this.nextEphemeralFunctionNumericID = 0;
this.nextEphemeralObjectNumericID = 0;
// Handles are values declared outside the VM that add to the reachability
// graph of the VM (because they're reachable externally)
this.handles = new Set();
this.moduleCache = new Map();
this.jobQueue = IL.undefinedValue;
// Represents the `cpsCallback` register. Unlike most other registers, the
// `cpsCallback` register is not persisted across a function call and so I've
// put it as a class property rather than embedded in the frame. The other
// reason to put it here is that host functions don't get their own frame
// unless they call back into the VM, but you still need to know if they were
// cps-called.
this.cpsCallback = IL.deletedValue;
this.opts = {
overflowChecks: true,
executionFlags: [IL.ExecutionFlag.FloatSupport],
...opts
};
if (this.opts.overflowChecks) {
this.opts.executionFlags?.push(IL.ExecutionFlag.CompiledWithOverflowChecks);
}
if (resumeFromSnapshot) {
return (0, utils_1.notImplemented)();
}
if (debugServer) {
this.debuggerInstrumentation = {
debugServer,
breakpointsByFilePath: {},
executionState: 'starting'
};
this.doDebuggerInstrumentation();
}
this.builtins = {
arrayPrototype: IL.nullValue,
promisePrototype: this.createPromisePrototype(),
asyncContinue: this.createAsyncContinueFunction(),
asyncCatchBlock: this.createAsyncCatchBlock(),
asyncHostCallback: this.createAsyncHostCallbackFunction(),
};
this.addBuiltinGlobals();
}
evaluateModule(moduleSource) {
let moduleObject = this.moduleCache.get(moduleSource);
if (moduleObject) {
return moduleObject;
}
moduleObject = this.newObject(IL.nullValue, 0);
this.moduleCache.set(moduleSource, moduleObject);
const filename = moduleSource.debugFilename || '<no file>';
const { unit } = (0, src_to_il_1.compileScript)(filename, moduleSource.sourceText);
if (this.opts.outputIL && moduleSource.debugFilename && !moduleSource.debugFilename.startsWith('<') /* E.g. <builtins> */) {
fs_1.default.writeFileSync(moduleSource.debugFilename + '.il', (0, stringify_il_1.stringifyUnit)(unit, {
commentSourceLocations: true,
showComments: true,
showStackDepth: true,
showVariableNameHints: true,
}));
}
const importDependency = moduleSource.importDependency || (_specifier => undefined);
// A mapping from the name the unit uses to refer to an external module to
// the name we actually give it.
const moduleImports = new Map();
// Transitively import the dependencies
for (const { variableName, source: specifier } of unit.moduleImports) {
// `importDependency` takes a module specifier and returns the
// corresponding module object. It likely does so by in turn calling
// `evaluateModule` for the dependency.
const dependency = importDependency(specifier);
if (!dependency) {
throw new Error(`Cannot find module ${(0, utils_1.stringifyIdentifier)(specifier)}`);
}
if (variableName !== undefined) {
// Assign the module object reference to a global slot. References from
// the imported unit to the dependent module will be translated to point
// to this slot. It's not ideal that each importer creates it's own
// imported slots, but things get a bit complicated because dependencies
// are not necessarily IL modules (e.g. they could be ephemeral objects),
// and we have to get the ordering right with circular dependencies. I
// tried it and the additional complexity makes me uncomfortable.
const slotID = (0, utils_1.uniqueName)(specifier, n => this.globalSlots.has(n));
this.globalSlots.set(slotID, { value: dependency });
moduleImports.set(variableName, slotID);
}
}
const loadedUnit = this.loadUnit(unit, filename, moduleImports, undefined);
this.pushFrame({
type: 'ExternalFrame',
frameNumber: this.frame ? this.frame.frameNumber + 1 : 1,
callerFrame: this.frame,
result: IL.undefinedValue,
});
// Set up the call
// Note: I've marked this as a void call, but I'm not sure what happens to the result
this.callCommon(loadedUnit.entryFunction, [moduleObject], true, IL.undefinedValue);
// Execute
this.run();
this.popFrame();
this.tryRunJobQueue();
return moduleObject;
}
createSnapshotIL() {
if (this.frame !== undefined) {
return (0, utils_1.invalidOperation)('Cannot create a snapshot while the VM is active');
}
// We clone the following because, at least in general, the garbage
// collection may mutate them (for example freeing up unused allocations).
// The garbage collection is more aggressive than normal since it doesn't
// take into account global variables or handles (because these don't
// survive snapshotting), so we clone to avoid disrupting the VM state,
// which could technically be used further after the snapshot.
const allocations = lodash_1.default.clone(this.allocations);
const globalSlots = lodash_1.default.clone(this.globalSlots);
const frame = lodash_1.default.clone(this.frame);
const exports = lodash_1.default.clone(this.exports);
const hostFunctions = lodash_1.default.clone(this.hostFunctions);
const functions = lodash_1.default.clone(this.functions);
const builtins = lodash_1.default.clone(this.builtins);
// Global variables do not transfer across to the snapshot (only global slots)
const globalVariables = new Map();
// We don't include any handles in the snapshot
const handles = new Set();
// The elements in the module cache are only reachable by the corresponding
// sources, which are not available to the resumed snapshot
const moduleCache = new Map();
// Perform a GC cycle to clean up, so only reachable things are left. Note
// that the implementation of the GC does not modify values (e.g. for
// pointer updates), it only deletes shallow items in these tables.
garbageCollect({
globalVariables,
globalSlots,
frame,
handles,
exports,
moduleCache,
allocations,
hostFunctions,
functions,
builtins
});
const snapshot = {
globalSlots,
functions,
exports,
allocations,
flags: new Set(this.opts.executionFlags),
builtins
};
return (0, deep_freeze_1.default)(lodash_1.default.cloneDeep(snapshot));
}
tryRunJobQueue() {
// Don't run job queue if the VM is not idle
if (this.frame)
return;
while (true) {
// No more jobs
if (this.jobQueue.type === 'UndefinedValue')
return;
const job = this.dequeueJob();
if (!this.isCallableValue(job))
(0, utils_1.unexpected)();
this.runFunction(job, [], false);
}
}
dequeueJob() {
if (this.jobQueue.type !== 'ReferenceValue')
(0, utils_1.unexpected)();
const alloc = this.dereference(this.jobQueue);
// Single job
if (alloc.type === 'ClosureAllocation') {
const job = this.jobQueue;
this.jobQueue = IL.undefinedValue;
return job;
}
// One or more jobs in the linked-list cycle
alloc.type === 'ArrayAllocation' || (0, utils_1.unexpected)();
const firstCell = this.jobQueue;
const lastCell = this.getProperty(firstCell, IL.numberValue(0));
const job = this.getProperty(firstCell, IL.numberValue(1));
const nextCell = this.getProperty(firstCell, IL.numberValue(2));
// Only one item in the circular linked list
if (this.areValuesEqual(firstCell, lastCell)) {
this.jobQueue = IL.undefinedValue;
return job;
}
// Unlink the first cell
this.setProperty(lastCell, IL.numberValue(2), nextCell);
this.setProperty(nextCell, IL.numberValue(0), lastCell);
// Move the job queue forward
this.jobQueue = nextCell;
return job;
}
createSnapshot(generateSourceMap) {
const snapshotInfo = this.createSnapshotIL();
const { snapshot } = (0, encode_snapshot_1.encodeSnapshot)(snapshotInfo, false, generateSourceMap);
return snapshot;
}
ephemeralFunction(handler, nameHint) {
const id = nameHint
? (0, utils_1.uniqueName)(nameHint, n => this.ephemeralFunctions.has(n))
: this.nextEphemeralFunctionNumericID++;
this.ephemeralFunctions.set(id, handler);
return {
type: 'EphemeralFunctionValue',
value: id
};
}
ephemeralObject(handler, nameHint) {
const id = nameHint
? (0, utils_1.uniqueName)(nameHint, n => this.ephemeralObjects.has(n))
: this.nextEphemeralObjectNumericID++;
this.ephemeralObjects.set(id, handler);
return {
type: 'EphemeralObjectValue',
value: id
};
}
unwrapEphemeralFunction(ephemeral) {
const handler = this.ephemeralFunctions.get(ephemeral.value);
return handler && handler.unwrap();
}
unwrapEphemeralObject(ephemeral) {
const handler = this.ephemeralObjects.get(ephemeral.value);
return handler && handler.unwrap();
}
vmExport(exportID, value) {
if (this.exports.has(exportID)) {
return this.runtimeError(`Duplicate export ID: ${exportID}`);
}
this.exports.set(exportID, value);
}
vmImport(hostFunctionID, defaultImplementation) {
if (this.hostFunctions.has(hostFunctionID)) {
return {
type: 'HostFunctionValue',
value: hostFunctionID
};
}
// Ask the import map
let hostImplementation = this.resolveFFIImport(hostFunctionID);
// Otherwise use the default implementation
if (!hostImplementation) {
hostImplementation = defaultImplementation;
}
// Otherwise, just import a stub that throws. The runtime host will resolve
// a different implementation for the same ID, so this is just a placeholder.
if (!hostImplementation) {
hostImplementation = {
call(args) {
throw new Error(`Host implementation not provided for imported ID ${hostFunctionID}`);
},
unwrap() {
return undefined;
}
};
}
this.hostFunctions.set(hostFunctionID, hostImplementation);
return {
type: 'HostFunctionValue',
value: hostFunctionID
};
}
resolveExport(exportID) {
if (!this.exports.has(exportID)) {
return (0, utils_1.invalidOperation)(`Export not found: ${exportID}`);
}
return this.exports.get(exportID);
}
setArrayPrototype(value) {
this.builtins.arrayPrototype = value;
}
/**
* "Relocates" a unit into the global "address space". I.e. remaps all its
* global and function IDs to unique IDs in the VM, and remaps all its import
* references to the corresponding resolve imports.
*
* @see evaluateModule
*/
loadUnit(unit, unitNameHint,
// Given a variable name used by the unit to refer to an imported module,
// what actual global slot holds a reference to that module?
importResolutions, moduleHostContext) {
const self = this;
const missingGlobals = unit.freeVariables
.filter(g => !this.globalVariables.has(g));
if (missingGlobals.length > 0) {
return (0, utils_1.invalidOperation)(`Unit cannot be loaded because of missing required globals: ${missingGlobals.join(', ')}`);
}
// IDs are remapped when loading into the shared namespace of this VM
const remappedFunctionIDs = new Map();
const newFunctionIDs = new Set();
// Allocation slots for all the module-level variables, including functions
const moduleVariableResolutions = new Map();
for (const moduleVariable of unit.moduleVariables) {
const slotID = (0, utils_1.uniqueName)(moduleVariable, n => this.globalSlots.has(n));
this.globalSlots.set(slotID, { value: IL.undefinedValue });
moduleVariableResolutions.set(moduleVariable, slotID);
}
// Note: I used to prefix the name hints with the filename. I've stopped
// doing this because I think that the name will mostly only be inspected in
// situations where the meaning is obvious without the filename. By removing
// it, the generated IL is much cleaner to read. This is especially useful
// when manually inspecting test cases and output.
// Calculate new function IDs
for (const func of Object.values(unit.functions)) {
const newFunctionID = (0, utils_1.uniqueName)(func.id, n => this.functions.has(n) || newFunctionIDs.has(n));
remappedFunctionIDs.set(func.id, newFunctionID);
newFunctionIDs.add(newFunctionID);
}
// Functions implementations
for (const func of Object.values(unit.functions)) {
const newFunctionID = (0, utils_1.notUndefined)(remappedFunctionIDs.get(func.id));
const imported = importFunction(func);
this.functions.set(newFunctionID, imported);
}
return {
entryFunction: {
type: 'FunctionValue',
value: (0, utils_1.notUndefined)(remappedFunctionIDs.get(unit.entryFunctionID))
}
};
function importFunction(func) {
return {
...func,
id: (0, utils_1.notUndefined)(remappedFunctionIDs.get(func.id)),
moduleHostContext,
blocks: (0, utils_1.mapObject)(func.blocks, importBlock)
};
}
function importBlock(block) {
return {
...block,
operations: block.operations.map(importOperation)
};
}
function importOperation(operation) {
switch (operation.opcode) {
case 'LoadGlobal': return importGlobalOperation(operation);
case 'StoreGlobal': return importGlobalOperation(operation);
case 'Literal': return importLiteralOperation(operation);
default: return operation;
}
}
function importLiteralOperation(operation) {
const operand = operation.operands[0] ?? self.ilError('Literal operation must have 1 operand');
if (operand.type !== 'LiteralOperand')
return self.ilError('Literal operation must be have a literal operand');
const literal = operand.literal;
if (literal.type === 'FunctionValue') {
const oldFunctionId = literal.value;
const newFunctionId = remappedFunctionIDs.get(oldFunctionId) ?? self.ilError(`Literal operation refers to function \`${oldFunctionId}\` which is not defined`);
return {
...operation,
operands: [{
type: 'LiteralOperand',
literal: {
...literal,
value: newFunctionId
}
}]
};
}
else if (literal.type === 'ReferenceValue') {
// Like with functions, we can theoretically import a unit's
// "allocations" into the VM's allocations, with mapping table analogous
// to `remappedFunctionIDs` to get new allocation IDs. Then literal that
// reference allocations must also be remapped.
return (0, utils_1.notImplemented)('Reference literals');
}
else {
return operation;
}
}
function importGlobalOperation(operation) {
(0, utils_1.hardAssert)(operation.operands.length === 1);
const [nameOperand] = operation.operands;
if (nameOperand.type !== 'NameOperand')
return (0, utils_1.invalidOperation)('Malformed IL');
// Resolve the name to a global slot
const slotID = moduleVariableResolutions.get(nameOperand.name)
|| importResolutions.get(nameOperand.name)
|| self.globalVariables.get(nameOperand.name);
if (!slotID) {
return (0, utils_1.invalidOperation)(`Could not resolve variable: ${nameOperand.name}`);
}
;
return {
...operation,
operands: [{
type: 'NameOperand',
name: slotID
}]
};
}
}
run() {
const instr = this.debuggerInstrumentation;
while (this.frame && this.frame.type !== 'ExternalFrame') {
this.operationBeingExecuted = this.block.operations[this.nextOperationIndex];
const filePath = this.frame.filename;
if (this.operationBeingExecuted.sourceLoc) {
const { line: srcLine, column: srcColumn } = this.operationBeingExecuted.sourceLoc;
if (instr && filePath) {
const pauseBecauseOfEntry = instr.executionState === 'starting';
const pauseBecauseOfStep = instr.executionState === 'step';
const breakpointsOfFile = instr.breakpointsByFilePath[filePath] || [];
const pauseBecauseOfBreakpoint = breakpointsOfFile.some(bp => {
if (instr.executionState === 'continue') {
return bp.line === srcLine && instr.lastExecutedLine !== srcLine;
}
if (instr.executionState === 'step') {
return bp.line === srcLine && (!bp.column || bp.column === srcColumn);
}
return false;
});
if (pauseBecauseOfEntry) {
this.sendToDebugClient({ type: 'from-app:stop-on-entry' });
instr.executionState = 'paused';
}
else if (pauseBecauseOfBreakpoint) {
this.sendToDebugClient({ type: 'from-app:stop-on-breakpoint' });
instr.executionState = 'paused';
}
else if (pauseBecauseOfStep) {
this.sendToDebugClient({ type: 'from-app:stop-on-step' });
instr.executionState = 'paused';
}
console.log('paused bc of entry:', pauseBecauseOfEntry);
while (instr.executionState === 'paused') {
console.log('Before waiting for message');
const messageStr = instr.debugServer.receiveSocketEvent() || (0, utils_1.unexpected)();
const message = JSON.parse(messageStr);
console.log('Received:', messageStr);
if (message.type === 'from-debugger:step-request') {
instr.executionState = 'step';
}
if (message.type === 'from-debugger:continue-request') {
instr.executionState = 'continue';
}
}
}
if (instr) {
instr.lastExecutedLine = srcLine;
}
}
else {
if (instr) {
instr.lastExecutedLine = undefined;
}
}
this.step();
}
}
runFunction(func, args, runJobQueue = true) {
this.pushFrame({
type: 'ExternalFrame',
frameNumber: this.frame ? this.frame.frameNumber + 1 : 1,
callerFrame: this.frame,
result: IL.undefinedValue,
});
this.callCommon(func, args, false, IL.undefinedValue);
this.run();
if (this.exception) {
(0, utils_1.hardAssert)(this.frame === undefined);
const exception = this.exception ?? (0, utils_1.unexpected)();
this.exception = undefined;
return { type: 'Exception', exception };
}
if (this.frame === undefined || this.frame.type !== 'ExternalFrame') {
return (0, utils_1.unexpected)();
}
// Result of module script
const result = this.frame.result;
this.popFrame();
if (runJobQueue) {
this.tryRunJobQueue();
}
return result;
}
sendToDebugClient(message) {
if (this.debuggerInstrumentation) {
console.log(`To debug client: ${JSON.stringify(message)}`);
this.debuggerInstrumentation.debugServer.send(JSON.stringify(message));
}
}
setupDebugServerListener(cb) {
if (this.debuggerInstrumentation) {
this.debuggerInstrumentation.debugServer.on('message', messageStr => {
cb(JSON.parse(messageStr));
});
}
}
doDebuggerInstrumentation() {
while (true) {
console.log('Waiting for a debug session to start');
// Block until a client connects
const messageStr = this.debuggerInstrumentation.debugServer.receiveSocketEvent() || (0, utils_1.unexpected)();
const message = JSON.parse(messageStr);
if (message.type === 'from-debugger:start-session') {
break;
}
}
this.setupDebugServerListener(message => {
console.log('non-blocking message:', message);
switch (message.type) {
case 'from-debugger:stack-request': {
if (!this.frame || this.frame.type === 'ExternalFrame') {
// TODO | HIGH | Raf: Document why encountering an external frame is
// an unexpected case
return (0, utils_1.unexpected)(`frame is ${JSON.stringify(this.frame, null, 2)}`);
}
const stackTraceFrames = [];
let frame = this.frame;
while (frame !== undefined) {
if (frame.type === 'InternalFrame') {
stackTraceFrames.push({
filePath: frame.filename || '<unknown>',
line: frame.operationBeingExecuted.sourceLoc?.line || 0,
column: frame.operationBeingExecuted.sourceLoc?.column || 0
});
}
else {
stackTraceFrames.push({
filePath: '<external file>',
// Do Babel-emitted source lines and columns start with 1? If so,
// then 0, 0 makes sense as an external location
line: 0,
column: 0
});
}
frame = frame.callerFrame;
}
this.sendToDebugClient({ type: 'from-app:stack', data: stackTraceFrames });
break;
}
case 'from-debugger:set-and-verify-breakpoints': {
// TODO Verification (e.g. breakpoints on whitespace)
const { filePath, breakpoints } = message.data;
console.log('SET-AND-VERIFY-BREAKPOINTS');
console.log(JSON.stringify({ filePath, breakpoints }, null, 2));
if (this.debuggerInstrumentation) {
this.debuggerInstrumentation.breakpointsByFilePath[filePath] = breakpoints;
this.sendToDebugClient({
type: 'from-app:verified-breakpoints',
data: breakpoints
});
}
break;
}
case 'from-debugger:get-breakpoints': {
const { filePath } = message.data;
console.log('GET BREAKPOINTS');
console.log(JSON.stringify({ filePath }), null, 2);
if (this.debuggerInstrumentation) {
this.sendToDebugClient({
type: 'from-app:breakpoints',
data: {
breakpoints: this.debuggerInstrumentation.breakpointsByFilePath[filePath] || []
}
});
}
break;
}
case 'from-debugger:scopes-request': {
console.log('GET SCOPES');
if (this.debuggerInstrumentation) {
const scopes = [{
name: 'Globals',
variablesReference: ScopeVariablesReference.GLOBALS,
expensive: false
}, {
name: 'Current Frame',
variablesReference: ScopeVariablesReference.FRAME,
expensive: false
}, {
name: 'Current Operation',
variablesReference: ScopeVariablesReference.OPERATION,
expensive: false
}];
this.sendToDebugClient({ type: 'from-app:scopes', data: scopes });
}
break;
}
case 'from-debugger:variables-request': {
const refType = message.data;
const outputChannel = `from-app:variables:ref:${refType}`;
switch (refType) {
case ScopeVariablesReference.GLOBALS:
const globalEntries = [...this.globalVariables.entries()];
const globals = (0, lodash_1.default)(globalEntries)
.map(([name, id]) => ({ name, value: this.globalSlots.get(id) }))
.filter(({ value }) => value !== undefined)
.map(({ name, value }) => ({
name,
value: DebuggerHelpers.stringifyILValue(value.value),
// See the `DebuggerProtocol.Variable` type. For now, to
// simplify, variables don't reference other variables
variablesReference: 0
}))
.value();
return this.sendToDebugClient({ type: outputChannel, data: globals });
case ScopeVariablesReference.FRAME:
let frameVariables = [];
if (!this.frame || this.frame.type === 'ExternalFrame') {
frameVariables = [];
}
else {
frameVariables = (0, lodash_1.default)(this.frame.variables)
.map((variable, index) => ({
name: DebuggerHelpers.displayInternals(`Frame Variable ${index}`),
value: DebuggerHelpers.stringifyILValue(variable),
variablesReference: 0
}))
.value();
}
return this.sendToDebugClient({ type: outputChannel, data: frameVariables });
case ScopeVariablesReference.OPERATION:
let operationVariables = [];
if (!this.frame || this.frame.type === 'ExternalFrame') {
operationVariables = [];
}
else {
const operation = this.operationBeingExecuted;
operationVariables = [{
name: DebuggerHelpers.displayInternals('Opcode'),
value: operation.opcode, variablesReference: 0
},
...operation.operands.map((operand, index) => ({
name: DebuggerHelpers.displayInternals(`Operand ${index}`),
value: JSON.stringify(operand),
// See the `DebuggerProtocol.VariablePresentationHint` type
presentationHint: 'data',
variablesReference: 0,
}))];
}
return this.sendToDebugClient({ type: outputChannel, data: operationVariables });
default:
return [];
}
}
}
});
console.log('INSTRUMENTATION SETUP DONE');
}
/** Create a new handle, starting at ref-count 1. Needs to be released with a call to `release` */
createHandle(value) {
let refCount = 1;
const handle = {
get value() {
if (refCount <= 0) {
return (0, utils_1.invalidOperation)('Handle value has been released');
}
return value;
},
addRef: () => {
if (refCount <= 0) {
return (0, utils_1.invalidOperation)('Handle value has been released');
}
refCount++;
return handle;
},
release: () => {
if (refCount <= 0) {
return (0, utils_1.invalidOperation)('Handle value has been released');
}
const result = value;
if (--refCount === 0) {
this.handles.delete(handle);
value = undefined;
}
return result;
}
};
this.handles.add(handle);
return handle;
}
/**
* Gets the moduleContext provided to `runUnit` or `loadUnit` when the active
* function was loaded.
*/
callerModuleHostContext() {
if (!this.frame || this.frame.type !== 'InternalFrame') {
return (0, utils_1.invalidOperation)('Module context not accessible when no module is active.');
}
return this.frame.func.moduleHostContext;
}
dispatchOperation(operation, operands) {
const method = this[`operation${operation.opcode}`];
if (!method) {
return (0, utils_1.notImplemented)(`Opcode not implemented in compile-time VM: "${operation.opcode}"`);
}
if (operands.length < (0, il_opcodes_1.minOperandCount)(operation.opcode)) {
return (0, utils_1.unexpected)(`Operation does not provide enough operands`);
}
if (method.length !== (0, il_opcodes_1.maxOperandCount)(operation.opcode)) {
return (0, utils_1.unexpected)(`Opcode "${operation.opcode}" in compile-time VM is implemented with incorrect number of opcodes (${method.length} instead of expected ${(0, il_opcodes_1.maxOperandCount)(operation.opcode)}).`);
}
// Writing these out explicitly so that we get type errors if we add new operators
switch (operation.opcode) {
case 'ArrayGet': return this.operationArrayGet(operands[0]);
case 'ArrayNew': return this.operationArrayNew();
case 'ArraySet': return this.operationArraySet(operands[0]);
case 'AsyncComplete': return this.operationAsyncComplete();
case 'AsyncResume': return this.operationAsyncResume(operands[0], operands[1]);
case 'AsyncReturn': return this.operationAsyncReturn();
case 'AsyncStart': return this.operationAsyncStart(operands[0], operands[1]);
case 'Await': return this.operationAwait();
case 'AwaitCall': return this.operationAwaitCall(operands[0]);
case 'BinOp': return this.operationBinOp(operands[0]);
case 'Branch': return this.operationBranch(operands[0], operands[1]);
case 'Call': return this.operationCall(operands[0], operands[1]);
case 'ClassCreate': return this.operationClassCreate();
case 'ClosureNew': return this.operationClosureNew();
case 'EndTry': return this.operationEndTry();
case 'EnqueueJob': return this.operationEnqueueJob();
case 'Jump': return this.operationJump(operands[0]);
case 'Literal': return this.operationLiteral(operands[0]);
case 'LoadArg': return this.operationLoadArg(operands[0]);
case 'LoadGlobal': return this.operationLoadGlobal(operands[0]);
case 'LoadScoped': return this.operationLoadScoped(operands[0]);
case 'LoadReg': return this.operationLoadReg(operands[0]);
case 'LoadVar': return this.operationLoadVar(operands[0]);
case 'New': return this.operationNew(operands[0]);
case 'Nop': return this.operationNop(operands[0]);
case 'ObjectGet': return this.operationObjectGet();
case 'ObjectKeys': return this.operationObjectKeys();
case 'ObjectNew': return this.operationObjectNew();
case 'ObjectSet': return this.operationObjectSet();
case 'Pop': return this.operationPop(operands[0]);
case 'Return': return this.operationReturn();
case 'ScopeClone': return this.operationScopeClone();
case 'ScopeDiscard': return this.operationScopeDiscard();
case 'ScopeNew': return this.operationScopeNew(operands[0]);
case 'ScopePop': return this.operationScopePop();
case 'ScopePush': return this.operationScopePush(operands[0]);
case 'ScopeSave': return this.operationScopeSave();
case 'StartTry': return this.operationStartTry(operands[0]);
case 'StoreGlobal': return this.operationStoreGlobal(operands[0]);
case 'StoreScoped': return this.operationStoreScoped(operands[0]);
case 'StoreVar': return this.operationStoreVar(operands[0]);
case 'Throw': return this.operationThrow();
case 'TypeCodeOf': return this.operationTypeCodeOf();
case 'Uint8ArrayNew': return this.operationUint8ArrayNew();
case 'UnOp': return this.operationUnOp(operands[0]);
default: return (0, utils_1.assertUnreachable)(operation);
}
}
step() {
const op = this.operationBeingExecuted;
this.nextOperationIndex++;
if (!op) {
return this.ilError('Did not expect to reach end of block without a control instruction (Branch, Jump, or Return).');
}
const operationMeta = IL.opcodes[op.opcode];
if (!operationMeta) {
return this.ilError(`Unknown opcode "${op.opcode}".`);
}
if (op.operands.length < (0, il_opcodes_1.minOperandCount)(op.opcode)) {
return this.ilError(`Expected ${operationMeta.operands.length} operands to operation \`${op.opcode}\`, but received ${op.operands.length} operands.`);
}
const stackDepthBeforeOp = this.variables.length;
if (op.stackDepthBefore !== undefined && stackDepthBeforeOp !== op.stackDepthBefore) {
return this.ilError(`Stack depth before opcode "${op.opcode}" is expected to be ${op.stackDepthBefore} but is actually ${stackDepthBeforeOp}`);
}
// Note: for the moment, optional operands are always trailing, so they'll just be omitted
const operands = op.operands.map((o, i) => this.resolveOperand(o, operationMeta.operands[i]));
this.opts.trace && this.opts.trace(op);
this.dispatchOperation(op, operands);
// If we haven't returned to the outside world, then we can check the stack balance
// Note: we don't look at the stack balance for Call instructions because they create a completely new stack of variables.
if (this.frame && this.frame.type === 'InternalFrame'
&& op.opcode !== 'Call'
&& op.opcode !== 'New'
&& op.opcode !== 'Return'
&& op.opcode !== 'AsyncReturn'
&& op.opcode !== 'Throw'
&& op.opcode !== 'EndTry'
&& op.opcode !== 'AwaitCall'
&& op.opcode !== 'Await'
&& op.opcode !== 'AsyncResume'
&& op.opcode !== 'AsyncComplete') {
const stackDepthAfter = this.variables.length;
if (op.stackDepthAfter !== undefined && stackDepthAfter !== op.stackDepthAfter) {
return this.ilError(`Stack depth after opcode "${op.opcode}" is expected to be ${op.stackDepthAfter} but is actually ${stackDepthAfter}`);
}
const stackChange = stackDepthAfter - stackDepthBeforeOp;
const expectedStackChange = IL.calcDynamicStackChangeOfOp(op);
if (expectedStackChange !== undefined && stackChange !== expectedStackChange) {
return this.ilError(`Expected opcode "${op.opcode}" to change the stack by ${expectedStackChange} slots, but instead it changed by ${stackChange}`);
}
}
}
resolveOperand(operand, expectedType) {
switch (expectedType) {
case 'LabelOperand':
if (operand.type !== 'LabelOperand') {
return this.ilError('Expected label operand');
}
return operand.targetBlockId;
case 'CountOperand':
if (operand.type !== 'CountOperand') {
return this.ilError('Expected count operand');
}
return operand.count;
case 'FlagOperand':
if (operand.type !== 'FlagOperand') {
return this.ilError('Expected count operand');
}
return operand.flag;
case 'IndexOperand':
if (operand.type !== 'IndexOperand') {
return this.ilError('Expected index operand');
}
return operand.index;
case 'NameOperand':
if (operand.type !== 'NameOperand') {
return this.ilError('Expected name operand');
}
return operand.name;
case 'LiteralOperand':
if (operand.type !== 'LiteralOperand') {
return this.ilError('Expected literal operand');
}
return operand.literal;
case 'OpOperand':
if (operand.type !== 'OpOperand') {
return this.ilError('Expected sub-operation operand');
}
return operand.subOperation;
default: (0, utils_1.assertUnreachable)(expectedType);
}
}
operationArrayNew() {
this.push(this.newArray());
}
operationUint8ArrayNew() {
const lengthValue = this.pop();
if (lengthValue.type !== 'NumberValue') {
this.runtimeError('New Uint8Array must be created with integer length');
}
const length = lengthValue.value;
if ((length | 0) !== length) {
this.runtimeError('New Uint8Array must be created with integer length');
}
if (length < 0 || length > 0xFFF - 3) {
this.runtimeError('Uint8Array length out of range');
}
this.push(this.newUint8Array(length));
}
operationArrayGet(index) {
const pArray = this.pop();
if (pArray.type !== 'ReferenceValue')
return this.ilError('Using ArrayGet on a non-array');
const array = this.dereference(pArray);
if (array.type !== 'ArrayAllocation')
return this.ilError('Using ArrayGet on a non-array');
array.lengthIsFixed || this.ilError('Using ArrayGet on variable-length-array');
index >= 0 && index < array.items.length || this.ilError('ArrayGet index out of bounds');
let value = array.items[index];
// Holes in the array
if (value === undefined)
value = IL.undefinedValue;
this.push(value);
}
operationArraySet(index) {
const value = this.pop();
const pArray = this.pop();
if (pArray.type !== 'ReferenceValue')
return this.ilError('Using ArraySet on a non-array');
const array = this.dereference(pArray);
if (array.type !== 'ArrayAllocation')
return this.ilError('Using ArraySet on a non-array');
array.lengthIsFixed || this.ilError('Using ArraySet on variable-length-array');
index >= 0 && index < array.items.length || this.ilError('ArraySet index out of bounds');
array.items[index] = value;
}
operationAsyncResume(slotCount, catchTarget) {
// The AsyncResume instruction should be the first instruction in the new frame
(0, utils_1.hardAssert)(this.internalFrame.variables.length === 0);
// Synchronous return value
this.push(IL.undefinedValue);
// Push the async catch target (note that async functions are resumed from
// the job queue so there should be no outer catch block)
const stackDepth = this.stackPointer;
(0, utils_1.hardAssert)(this.catchTarget.type === 'UndefinedValue');
this.push(IL.numberValue(0));
this.push(this.builtins.asyncCatchBlock);
this.catchTarget = stackDepth;
// Restore state of local temporaries
for (let i = 0; i < slotCount; i++) {
// Note: +2 to skip over the continuation and callback in the async scope
const [slotArr, slotI] = this.findScopedVariable(i + 2);
const value = slotArr[slotI];
this.push(value);
}
this.catchTarget = this.slotIndexToStackDepth(this.stackDepthToSlotIndex(this.stackPointer) - catchTarget);
// Push the return value or error to the stack.
// Note: the signature here is (this, isSuccess, value)
this.operationLoadArg(2);
this.operationLoadArg(1); // isSuccess
const isSuccess = this.pop();
if (!this.isTruthy(isSuccess)) {
this.operationThrow(); // Throw the error
}
}
addressOfFunctionEntry(funcValue) {
if (funcValue.type !== 'FunctionValue')
(0, utils_1.unexpected)();
const funcId = funcValue.value;
const func = this.functions.get(funcId) ?? (0, utils_1.unexpected)();
const blockId = func.entryBlockID;
return {
type: 'ProgramAddressValue',
blockId,
funcId,
operationIndex: 0,
};
}
operationAsyncReturn() {
const result = this.pop();
(0, utils_1.hardAssert)(this.variables.length >= 3);
// Pop the async catch target to variable slot 1 (the slot at which it was pushed)
while (this.variables.length > 1) {
this.operationEndTry();
}
(0, utils_1.hardAssert)(this.variables.length === 1);
this.push(result);
this.push(IL.trueValue); // isSuccess
this.operationAsyncComplete();
}
operationAsyncComplete() {
const isSuccess = this.isTruthy(this.pop()) ? IL.trueValue : IL.falseValue;
const resultOrError = this.pop();
const callbackOrPromise = this.getScoped(1);
const type = this.deepTypeOf(callbackOrPromise);
// If no callback to run then the async function was
// void-called, meaning that nobody is going to use the synchronous result
// and there is nothing to do in the job queue.
if (type === 'NoOpFunction') {
/* Nothing to do */
}
else if (type === 'ClosureAllocation') {
// T