UNPKG

microvium

Version:

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

423 lines 18.8 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; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.ValueWrapper = exports.VirtualMachineFriendly = void 0; const VM = __importStar(require("./virtual-machine")); const IL = __importStar(require("./il")); const utils_1 = require("./utils"); const snapshot_il_1 = require("./snapshot-il"); const lib_1 = require("../lib"); // import { SynchronousWebSocketServer } from './synchronous-ws-server'; const fs = __importStar(require("fs")); const builtin_globals_1 = require("./builtin-globals"); const encode_snapshot_1 = require("./encode-snapshot"); /** * A wrapper for VirtualMachine that automatically marshalls data between the * host and the VM (something like a membrane to interface to the VM) */ class VirtualMachineFriendly { constructor(resumeFromSnapshot, hostImportMap = {}, opts = {}) { this.moduleCache = new WeakMap(); this.vmExport = (exportID, value) => { if (typeof exportID !== 'number' || (exportID | 0) !== exportID || exportID < 0 || exportID > 65535) throw new Error(`ID for \`vmExport\` must be an integer in the range 0 to 65535. Received ${exportID}`); const vmValue = hostValueToVM(this.vm, value); this.vm.vmExport(exportID, vmValue); }; this.vmImport = (...args) => { if (args.length < 1) throw new Error('vmImport expects at least 1 argument'); const [id, defaultImplementation] = args; if (typeof id !== 'number' || (id | 0) !== id || id < 0 || id > 65535) throw new Error(`ID for \`vmImport\` must be an integer in the range 0 to 65535. Received ${id}`); const hostImplementation = defaultImplementation && hostFunctionToVMHandler(this.vm, defaultImplementation); const result = this.vm.vmImport(id, hostImplementation); return vmValueToHost(this.vm, result, `<host function ${id}>`); }; let innerResolve; if (typeof hostImportMap !== 'function') { if (typeof hostImportMap !== 'object' || hostImportMap === null) { return (0, utils_1.invalidOperation)('`importMap` must be a resolution function or an import table'); } const importTable = hostImportMap; innerResolve = (hostFunctionID) => { if (!importTable.hasOwnProperty(hostFunctionID)) { return undefined; } return hostFunctionToVMHandler(this.vm, importTable[hostFunctionID]); }; } else { const resolve = hostImportMap; innerResolve = (hostFunctionID) => { const f = resolve(hostFunctionID); return f && hostFunctionToVMHandler(this.vm, f); }; } const debugServer = undefined; // TODO: This code doesn't work yet, and also it is not bundler friendly so I'm commenting it out. // let debugServer: SynchronousWebSocketServer | undefined; // if (opts.debugConfiguration) { // debugServer = new SynchronousWebSocketServer(opts.debugConfiguration.port, { // verboseLogging: false // }); // console.log(colors.yellow(`Microvium-debug is waiting for a client to connect on ws://127.0.0.1:${opts.debugConfiguration.port}`)) // debugServer.waitForConnection(); // console.log('Microvium-debug client connected'); // } this.vm = new VM.VirtualMachine(resumeFromSnapshot, innerResolve, opts, debugServer); this._global = new Proxy({}, new GlobalWrapper(this.vm)); (0, builtin_globals_1.addBuiltinGlobals)(this, opts.noLib); } getMemoryStats() { throw new Error('getMemoryStats is only available at runtime'); } static create(hostImportMap = lib_1.defaultHostEnvironment, opts = {}) { return new VirtualMachineFriendly(undefined, hostImportMap, opts); } evaluateModule(moduleSource) { const self = this; if (this.moduleCache.has(moduleSource)) { const innerModuleObject = this.vm.evaluateModule(this.moduleCache.get(moduleSource)); const outerModuleObject = vmValueToHost(self.vm, innerModuleObject, undefined); return outerModuleObject; } const innerModuleSource = { sourceText: moduleSource.sourceText, debugFilename: moduleSource.debugFilename, importDependency: moduleSource.importDependency && wrapImportHook(moduleSource.importDependency) }; this.moduleCache.set(moduleSource, innerModuleSource); const innerModuleObject = this.vm.evaluateModule(innerModuleSource); const outerModuleObject = vmValueToHost(self.vm, innerModuleObject, undefined); return outerModuleObject; function wrapImportHook(fetch) { return (source) => { const innerFetchResult = fetch(source); if (!innerFetchResult) { return undefined; } return hostValueToVM(self.vm, innerFetchResult); }; } } createSnapshotIL() { return this.vm.createSnapshotIL(); } createSnapshot(opts = {}) { let snapshotInfo = this.createSnapshotIL(); if (opts.optimizationHook) { snapshotInfo = opts.optimizationHook(snapshotInfo); } if (opts.outputSnapshotIL && opts.snapshotILFilename) { fs.writeFileSync(opts.snapshotILFilename, (0, snapshot_il_1.stringifySnapshotIL)(snapshotInfo, { commentSourceLocations: true, showComments: true, showStackDepth: true, showVariableNameHints: true, })); } const generateHTML = false; // For debugging const { snapshot, html } = (0, encode_snapshot_1.encodeSnapshot)(snapshotInfo, generateHTML, opts?.generateSourceMap ?? false); if (html) (0, utils_1.writeTextFile)('snapshot.html', html); return snapshot; } stringifyState() { return this.vm.stringifyState(); } resolveExport(exportID) { return vmValueToHost(this.vm, this.vm.resolveExport(exportID), `<export ${exportID}>`); } garbageCollect() { this.vm.garbageCollect(); } newObject() { return vmValueToHost(this.vm, this.vm.newObject(IL.nullValue, 0), undefined); } newArray() { return vmValueToHost(this.vm, this.vm.newArray(), undefined); } setArrayPrototype(value) { this.vm.setArrayPrototype(hostValueToVM(this.vm, value)); } get globalThis() { return this._global; } } exports.VirtualMachineFriendly = VirtualMachineFriendly; function hostFunctionToVMHandler(vm, func) { return { call(args) { if (!func) { return (0, utils_1.invalidOperation)('The given host function does not have a compile-time implementation.'); } const [object, ...innerArgs] = args.map(a => vmValueToHost(vm, a, undefined)); const result = func.apply(object, innerArgs); return hostValueToVM(vm, result); }, unwrap() { return func; } }; } function vmValueToHost(vm, value, nameHint) { switch (value.type) { case 'BooleanValue': case 'NumberValue': case 'UndefinedValue': case 'StringValue': case 'NullValue': return value.value; case 'FunctionValue': case 'HostFunctionValue': case 'ResumePoint': return ValueWrapper.wrap(vm, value, nameHint); case 'ClassValue': return ValueWrapper.wrap(vm, value, nameHint); case 'EphemeralFunctionValue': { const unwrapped = vm.unwrapEphemeralFunction(value); if (unwrapped === undefined) { // vmValueToHost can only be called with a value that actually corresponds // to the VM passed in. If unwrapping it does not give us anything, then // it's a bug. return (0, utils_1.unexpected)(); } else { // Ephemeral functions always refer to functions in the host anyway, so // there's no wrapping required. return unwrapped; } } case 'EphemeralObjectValue': { const unwrapped = vm.unwrapEphemeralObject(value); if (unwrapped === undefined) { // vmValueToHost can only be called with a value that actually corresponds // to the VM passed in. If unwrapping it does not give us anything, then // it's a bug. return (0, utils_1.unexpected)(); } else { // Ephemeral objects always refer to functions in the host anyway, so // there's no wrapping required. return unwrapped; } } case 'ReferenceValue': { return ValueWrapper.wrap(vm, value, nameHint); } case 'DeletedValue': { // I think that deleted values should not appear here, since they should // be converted to undefined or throw a TDZ error upon reading them out of // the source slot. return (0, utils_1.unexpected)(); } case 'ProgramAddressValue': { // These are internal values and should never cross the boundary return (0, utils_1.unexpected)(); } case 'NoOpFunction': { return hostNoOpFunction; } default: return (0, utils_1.assertUnreachable)(value); } } function hostValueToVM(vm, value, nameHint) { switch (typeof value) { case 'undefined': return IL.undefinedValue; case 'boolean': return vm.booleanValue(value); case 'number': return vm.numberValue(value); case 'string': return vm.stringValue(value); case 'function': { if (ValueWrapper.isWrapped(vm, value)) { return ValueWrapper.unwrap(vm, value); } else { return vm.ephemeralFunction(hostFunctionToVMHandler(vm, value), nameHint || value.name); } } case 'object': { if (value === null) { return IL.nullValue; } if (ValueWrapper.isWrapped(vm, value)) { return ValueWrapper.unwrap(vm, value); } else { const obj = value; return vm.ephemeralObject({ get(_object, prop) { return hostValueToVM(vm, obj[prop]); }, set(_object, prop, value) { obj[prop] = vmValueToHost(vm, value, nameHint ? nameHint + '.' + prop : undefined); }, keys(_obj) { return Reflect.ownKeys(value).filter(k => typeof k === 'string'); }, unwrap() { return obj; } }); } } default: return (0, utils_1.notImplemented)(); } } // Used as targets for proxies, so that `typeof` and `call` work as expected const dummyFunctionTarget = Object.freeze(() => { }); const dummyObjectTarget = Object.freeze({}); const dummyArrayTarget = Object.freeze([]); const dummyUint8ArrayTarget = Object.freeze(new Uint8Array()); const vmValueSymbol = Symbol('vmValue'); const vmSymbol = Symbol('vm'); const hostNoOpFunction = () => undefined; hostNoOpFunction[vmValueSymbol] = IL.noOpFunction; hostNoOpFunction[vmSymbol] = 'universal'; class ValueWrapper { constructor(vm, vmValue, nameHint) { this.vm = vm; this.vmValue = vmValue; this.nameHint = nameHint; /* This wrapper uses weakrefs. The wrapper has a strong host-reference * (node.js reference) to the IL.Value, but is a weak VM-reference * (Microvium reference) (i.e. the VM implementation doesn't know that the * host has a reference). In order to protect the VM from collecting the * IL.Value, we create a VM.Handle that keeps the value reachable within the * VM. * * We don't need a reference to the handle, since it's only used to "peg" * the value, but the handle is strongly referenced by the finalization * group, while this wrapper object is only weakly referenced by the * finalization group, allowing it to be collected when it's unreachable by * the rest of the application. When the wrapper is collected, the * finalization group will release the corresponding handle. */ const handle = vm.createHandle(vmValue); ValueWrapper.finalizationGroup.register(this, handle); } static isWrapped(vm, value) { return (typeof value === 'function' || typeof value === 'object') && value !== null && value[vmValueSymbol] && (value[vmSymbol] == 'universal' || value[vmSymbol] == vm); // It needs to be a wrapped value in the context of the particular VM in question } static wrap(vm, value, nameHint) { // We need to choose the appropriate proxy target so that things like // `Array.isArray` and `typeof x === 'function'` work as expected on the // proxied value. let proxyTarget; switch (value.type) { case 'ReferenceValue': { const dereferenced = vm.dereference(value); switch (dereferenced.type) { case 'ObjectAllocation': proxyTarget = dummyObjectTarget; break; case 'ArrayAllocation': proxyTarget = dummyArrayTarget; break; case 'Uint8ArrayAllocation': proxyTarget = dummyUint8ArrayTarget; break; case 'ClosureAllocation': proxyTarget = dummyFunctionTarget; break; default: (0, utils_1.assertUnreachable)(dereferenced); } break; } case 'HostFunctionValue': case 'ClassValue': case 'ResumePoint': case 'FunctionValue': proxyTarget = dummyFunctionTarget; break; default: (0, utils_1.assertUnreachable)(value); } return new Proxy(proxyTarget, new ValueWrapper(vm, value, nameHint)); } static unwrap(vm, value) { (0, utils_1.hardAssert)(ValueWrapper.isWrapped(vm, value)); return value[vmValueSymbol]; } get(_target, p, receiver) { if (p === vmValueSymbol) return this.vmValue; if (p === vmSymbol) return this.vm; if (p === Symbol.toPrimitive) return () => `<Microvium ${this.vm.getType(this.vmValue)} value>`; if (typeof p !== 'string') return (0, utils_1.invalidOperation)('Only string properties supported'); if (/^\d+$/.test(p)) { p = parseInt(p); } const result = this.vm.getProperty(this.vmValue, hostValueToVM(this.vm, p)); return vmValueToHost(this.vm, result, this.nameHint ? `${this.nameHint}.${p}` : undefined); } set(_target, p, value, receiver) { if (typeof p !== 'string') return (0, utils_1.invalidOperation)('Only string properties supported'); if (/^\d+$/.test(p)) { p = parseInt(p); } this.vm.setProperty(this.vmValue, hostValueToVM(this.vm, p), hostValueToVM(this.vm, value)); return true; } apply(_target, thisArg, argArray = []) { const args = [thisArg, ...argArray].map(a => hostValueToVM(this.vm, a)); const func = this.vmValue; if (func.type !== 'FunctionValue' && !this.vm.isClosure(func)) { return (0, utils_1.invalidOperation)('Target is not callable'); } const result = this.vm.runFunction(func, args); if (result.type === 'Exception') { throw vmValueToHost(this.vm, result.exception, undefined); } return vmValueToHost(this.vm, result, undefined); } } exports.ValueWrapper = ValueWrapper; ValueWrapper.finalizationGroup = new FinalizationRegistry(releaseHandle); class GlobalWrapper { constructor(vm) { this.vm = vm; } get(_target, p, receiver) { if (typeof p !== 'string') return (0, utils_1.invalidOperation)('Only string-valued global variables are supported'); return vmValueToHost(this.vm, this.vm.globalGet(p), p); } set(_target, p, value, receiver) { if (typeof p !== 'string') return (0, utils_1.invalidOperation)('Only string-valued global variables are supported'); this.vm.globalSet(p, hostValueToVM(this.vm, value, p)); return true; } apply(_target, thisArg, argArray = []) { return (0, utils_1.invalidOperation)('Target not callable'); } } function releaseHandle(handle) { handle.release(); } //# sourceMappingURL=virtual-machine-friendly.js.map