UNPKG

microvium

Version:

A compact, embeddable scripting engine for microcontrollers for executing small scripts written in a subset of JavaScript.

1,010 lines 155 kB
"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