microvium
Version:
A compact, embeddable scripting engine for microcontrollers for executing small scripts written in a subset of JavaScript.
423 lines • 18.8 kB
JavaScript
"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
__setModuleDefault(result, mod);
return result;
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.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