microvium
Version:
A compact, embeddable scripting engine for microcontrollers for executing small scripts written in a subset of JavaScript.
235 lines (198 loc) • 7.18 kB
JavaScript
document.addEventListener("DOMContentLoaded", main, false);
const snapshot = [0x07,0x1c,0x07,0x00,0x7c,0x00,0xaa,0xdb,0x03,0x00,0x00,0x00,0x1c,0x00,0x1e,0x00,0x22,0x00,0x22,0x00,0x28,0x00,0x2c,0x00,0x6a,0x00,0x72,0x00,0x01,0x00,0x01,0x00,0x5d,0x00,0x71,0x00,0x6d,0x00,0x01,0x00,0x39,0x00,0x31,0x00,0x00,0x00,0x05,0x40,0x70,0x75,0x73,0x68,0x00,0x00,0x0d,0x40,0x68,0x65,0x6c,0x6c,0x6f,0x2c,0x20,0x77,0x6f,0x72,0x6c,0x64,0x00,0x00,0x02,0x60,0x00,0x00,0x0d,0x50,0x04,0x31,0x30,0x30,0x88,0x1d,0x00,0x6b,0x12,0x6f,0x67,0x01,0x60,0x00,0x0d,0x50,0x03,0x89,0x00,0x00,0x01,0x88,0x39,0x00,0x78,0x02,0x67,0x01,0x60,0x00,0x49,0x00,0x02,0x00,0x19,0x00,0x01,0x00,0x08,0xc0,0x05,0x00,0x05,0x00,0x31,0x00,0x4d,0x00];
const noOpFunc = Object.freeze(() => {});
const notImplemented = () => { throw new Error('Not implemented') }
const assert = x => { if (!x) throw new Error('Assertion failed') }
const TextEncoder = typeof require !== 'undefined'
? require('util').TextEncoder // node.js
: globalThis.TextEncoder; // browser
const TextDecoder = typeof require !== 'undefined'
? require('util').TextDecoder // node.js
: globalThis.TextDecoder; // browser
const textEncoder = new TextEncoder();
const textDecoder = new TextDecoder();
async function runAsync() {
const memory = new WebAssembly.Memory({ initial: 4, maximum: 4 });
const memArray = new Uint8Array(memory.buffer);
const mem16 = new Uint16Array(memory.buffer);
const readWord = address => mem16[address >>> 1];
const writeWord = (address, value) => mem16[address >>> 1] = value;
const vmImports = new Map([
[1, print]
]);
const imports = {
env: {
memory: memory,
mvm_fatalError: (vm, code) => {
const msg = `Microvium fatal error: code ${code}`;
console.error(msg);
throw new Error(msg);
},
fmod: (x, y) => x % y,
pow: (x, y) => x ** y,
invokeHost,
importRequired: (id) => {
if (!vmImports.has(id)) {
throw new Error(`VM requires import ${id} but not provided`)
}
}
}
};
// -----------------------------------------------------------------
// NOTE! If running locally, you need to serve using the tool
// `npm i -g serve` and then the command `serve` in this folder,
// otherwise you will get a CORS error for fetch.
// -----------------------------------------------------------------
const wasmPromise = fetch("microvium.wasm");
const { instance } = await WebAssembly.instantiateStreaming(wasmPromise, imports)
const exports = instance.exports;
const {
allocator_init,
reserve_ram,
reserve_rom,
mvm_restore,
generalPurpose1,
generalPurpose2,
generalPurpose3,
mvm_resolveExports,
mvm_call,
mvm_newNumber,
} = exports;
const gp2 = generalPurpose2.value;
const gp3 = generalPurpose3.value;
const ramStart = reserve_ram.value;
const romStart = reserve_rom.value;
// ROM is positioned in memory directly after RAM
const ramSize = romStart - ramStart;
// The RAM must be linked at address 0 and be exactly 64kB. The allocator is
// hard-coded to use this range because it means that 16-bit Microvium
// addresses directly correspond to memory offsets without any modification.
assert(ramStart === 0);
assert(ramSize === 0x10000);
allocator_init(ramStart, ramSize);
// Copy the snapshot into ROM
assert(snapshot.length < 0x10000);
memArray.set(snapshot, romStart);
check(mvm_restore(
generalPurpose1, // *result
romStart, // snapshotBytecode
snapshot.length, // bytecodeSize
0, // context
2, // resolveImport
));
const vm = readWord(generalPurpose1);
const main = resolveExport(1);
check(mvm_call(vm, main, gp2, 0, 0));
debugger;
return { memArray }
function invokeHost(vm, hostFunctionID, out_vmpResult, vmpArgs, argCount) {
const hostArgs = [];
let vmpArg = vmpArgs;
for (let i = 0; i < argCount; i++) {
const vmArg = readWord(vmpArg);
const hostArg = valueToHost(vmArg);
hostArgs.push(hostArg);
vmpArg += 2;
}
const resolveTo = vmImports.get(hostFunctionID);
const result = resolveTo(...hostArgs);
const vmResult = valueToVM(result);
writeWord(out_vmpResult, vmResult);
return 0;
}
function resolveExport(id) {
writeWord(gp2, id);
check(mvm_resolveExports(
vm,
gp2, /* *ids */
gp3, /* *results */
1, /* count */
));
return readWord(gp3);
}
function print(s) {
console.log(s);
}
function valueToVM(hostValue) {
switch (typeof hostValue) {
case 'undefined': return 0x01;
case 'boolean': return hostValue ? 0x09 : 0x0D;
case 'number': {
// int14
if ((hostValue | 0) === hostValue && hostValue >= -02000 && hostValue <= 0x1FFF1) {
return (hostValue << 2) | 3;
}
return mvm_newNumber(vm, hostValue);
}
case 'string': {
// I'm thinking to directly inject the string into Microvium memory
// rather than going through mvm_newString, because mvm_newString would
// require 2 copies: one to get it into the WASM memory and one to copy
// it into Microvium.
notImplemented();
}
}
// TODO
notImplemented();
}
function valueToHost(vmValue) {
debugger;
// Int14
if ((vmValue & 3) === 3) {
// Use the 16th bit as the sign bit
return (vmValue << 16) >> 18;
}
let address;
// Short pointer
if ((vmValue & 1) === 0) {
address = vmValue;
}
// Bytecode-mapped pointer
else if ((vmValue & 3) === 1) {
// Well known values
if (vmValue <= 0x25) {
switch (vmValue) {
case 0x01: return undefined;
case 0x05: return null;
case 0x09: return true;
case 0x0D: return false;
case 0x11: return NaN;
case 0x15: return -0;
case 0x19: return undefined;
case 0x1D: return 'length';
case 0x21: return '__proto__';
case 0x25: return noOpFunc;
}
}
// TODO Indirection through handles
// Plain bytecode pointer
address = romStart + (vmValue & 0xFFFC);
}
const headerWord = readWord(address - 2);
const typeCode = headerWord >>> 12;
const size = headerWord & 0xFFF;
switch (typeCode) {
// Int32
case 0x1: return readWord(address) | readWord(address + 2);
// Float64
case 0x2: {
const temp = new Float64Array(memory.buffer, address, 1);
return temp[0];
}
// String
case 0x3:
case 0x4: {
const temp = new Uint8Array(memory.buffer, address, size - 1);
return textDecoder.decode(temp);
}
default: notImplemented();
}
}
}
function check(errorCode) {
if (errorCode !== 0) throw new Error(`Microvium Error: ${errorCode}`)
}
function main() {
runAsync()
.catch(console.error);
}