waforth/src/web/WAForth.ts

214 lines
5.7 KiB
TypeScript
Raw Normal View History

2022-04-17 09:28:53 +02:00
import wasmModule from "../waforth.wat";
2019-03-10 10:37:01 +01:00
const isSafari =
typeof navigator != "undefined" &&
/^((?!chrome|android).)*safari/i.test(navigator.userAgent);
2018-05-13 17:07:54 +02:00
2022-04-14 23:00:45 +02:00
// eslint-disable-next-line no-unused-vars
2022-04-14 21:46:06 +02:00
const arrayToBase64 =
2022-04-14 23:00:45 +02:00
typeof Buffer === "undefined"
? function arrayToBase64(bytes: Uint8Array) {
2019-03-10 14:32:41 +01:00
var binary = "";
var len = bytes.byteLength;
for (var i = 0; i < len; i++) {
binary += String.fromCharCode(bytes[i]);
}
return window.btoa(binary);
2022-04-14 21:46:06 +02:00
}
: function arrayToBase64(s: Uint8Array) {
2022-04-14 21:46:06 +02:00
return Buffer.from(s).toString("base64");
2019-03-10 14:32:41 +01:00
};
2022-04-14 21:46:06 +02:00
function loadString(memory: WebAssembly.Memory, addr: number, len: number) {
return String.fromCharCode.apply(
null,
new Uint8Array(memory.buffer, addr, len) as any
);
}
2022-04-17 17:19:15 +02:00
/**
2022-05-05 20:52:18 +02:00
* JavaScript shell around the WAForth WebAssembly module.
2022-04-17 17:19:15 +02:00
*
2022-05-05 20:52:18 +02:00
* Provides higher-level functions to interact with the WAForth WebAssembly module.
2022-04-17 17:19:15 +02:00
*
* To the WebAssembly module, provides the infrastructure to dynamically load WebAssembly modules and
* the I/O primitives with the UI.
* */
2022-04-14 21:46:06 +02:00
class WAForth {
core?: WebAssembly.Instance;
buffer?: number[];
fns: Record<string, (v: Stack) => void>;
stack?: Stack;
2019-03-10 10:37:01 +01:00
2022-05-05 20:52:18 +02:00
/**
* Callback that is called when a character needs to be emitted.
*
* `c` is the ASCII code of the character to be emitted.
*/
onEmit?: (c: string) => void;
constructor() {
this.fns = {};
}
async load() {
let table: WebAssembly.Table;
let memory: WebAssembly.Memory;
2018-06-03 15:17:38 +02:00
const buffer = (this.buffer = []);
2018-05-12 21:09:19 +02:00
const instance = await WebAssembly.instantiate(wasmModule, {
shell: {
////////////////////////////////////////
// I/O
////////////////////////////////////////
2018-05-13 17:07:54 +02:00
2022-05-05 20:52:18 +02:00
emit: (c: number) => {
if (this.onEmit) {
this.onEmit(String.fromCharCode(c));
}
},
2018-05-13 17:07:54 +02:00
2019-03-12 09:04:39 +01:00
getc: () => {
2018-06-03 15:17:38 +02:00
if (buffer.length === 0) {
return -1;
}
return buffer.pop();
},
debug: (d: number) => {
2019-03-14 16:32:27 +01:00
console.log("DEBUG: ", d, String.fromCharCode(d));
},
2018-05-13 17:07:54 +02:00
2019-03-12 09:22:33 +01:00
key: () => {
let c: string | null = null;
2019-03-12 09:22:33 +01:00
while (c == null || c == "") {
c = window.prompt("Enter character");
}
return c.charCodeAt(0);
},
accept: (p: number, n: number) => {
const input = (window.prompt("Enter text") || "").substring(0, n);
2019-03-12 09:22:33 +01:00
const target = new Uint8Array(memory.buffer, p, input.length);
for (let i = 0; i < input.length; ++i) {
target[i] = input.charCodeAt(i);
}
console.log("ACCEPT", p, n, input.length);
return input.length;
},
////////////////////////////////////////
// Loader
////////////////////////////////////////
2018-05-13 17:07:54 +02:00
load: (offset: number, length: number, index: number) => {
let data = new Uint8Array(
(this.core!.exports.memory as WebAssembly.Memory).buffer,
offset,
length
);
if (isSafari) {
// On Safari, using the original Uint8Array triggers a bug.
// Taking an element-by-element copy of the data first.
let dataCopy = [];
for (let i = 0; i < length; ++i) {
dataCopy.push(data[i]);
2018-05-12 21:09:19 +02:00
}
data = new Uint8Array(dataCopy);
}
if (index >= table.length) {
table.grow(table.length); // Double size
2018-05-12 21:09:19 +02:00
}
2022-04-14 21:46:06 +02:00
// console.log("Load", index, arrayToBase64(data));
try {
var module = new WebAssembly.Module(data);
new WebAssembly.Instance(module, {
env: { table, memory },
});
} catch (e) {
console.error(e);
throw e;
}
2022-04-13 17:02:46 +02:00
},
////////////////////////////////////////
// Generic call
////////////////////////////////////////
call: () => {
const len = pop();
const addr = pop();
const fname = loadString(memory, addr, len);
const fn = this.fns[fname];
if (!fn) {
console.error("Unbound SCALL: %s", fname);
} else {
fn(this.stack!);
}
},
2022-04-13 17:02:46 +02:00
},
});
this.core = instance.instance;
const pop = (): number => {
return (this.core!.exports.pop as any)();
};
const popString = (): string => {
const len = pop();
const addr = pop();
return loadString(memory, addr, len);
};
const push = (n: number): void => {
(this.core!.exports.push as any)(n);
};
this.stack = {
pop,
popString,
push,
};
table = this.core.exports.table as WebAssembly.Table;
memory = this.core.exports.memory as WebAssembly.Memory;
2018-05-12 21:09:19 +02:00
}
read(s: string) {
2018-05-12 21:09:19 +02:00
const data = new TextEncoder().encode(s);
2018-06-03 15:17:38 +02:00
for (let i = data.length - 1; i >= 0; --i) {
this.buffer!.push(data[i]);
2018-05-12 21:09:19 +02:00
}
}
2018-05-13 17:07:54 +02:00
/**
* Read data `s` into the input buffer, and interpret.
*/
interpret(s: string) {
2018-05-13 17:07:54 +02:00
this.read(s);
2019-03-12 14:26:38 +01:00
try {
return (this.core!.exports.interpret as any)();
2019-03-12 14:26:38 +01:00
} catch (e) {
// Exceptions thrown from the core means QUIT or ABORT is called, or an error
// has occurred. Assume what has been done has been done, and ignore here.
}
2018-05-13 17:07:54 +02:00
}
/**
* Bind `name` to SCALL in Forth.
*
* When an SCALL is done with `name` on the top of the stack, `fn` will be called (with the name popped off the stack).
* Use `stack` to pop parameters off the stack, and push results back on the stack.
*/
bind(name: string, fn: (stack: Stack) => void) {
this.fns[name] = fn;
}
}
2022-05-05 20:52:18 +02:00
export interface Stack {
push(n: number): void;
pop(): number;
popString(): string;
2018-05-12 21:09:19 +02:00
}
export default WAForth;