diff --git a/README.md b/README.md index 85df5b5..291b35d 100644 --- a/README.md +++ b/README.md @@ -11,8 +11,9 @@ TinyAPL (read like *tiny apple*) is a tiny APL dialect and interpreter in Haskel * Part 5: [Array Notation and Reductions](https://blog.rubenverg.com/tinyapl_5_array_notation_reductions) * Part 6: [Tests, Docs, Each](https://blog.rubenverg.com/tinyapl_6_tests_docs_each) * Part 7: [Quads, Key, Index](https://blog.rubenverg.com/tinyapl_7_quads_key_index) +* Part 8: [All About Rank, and a Web Interface](https://blog.rubenverg.com/tinyapl_8_rank_web) -Documentation is available [here](https://tinyapl.rubenverg.com) +Documentation is available [here](https://tinyapl.rubenverg.com) and you can run the latest interpreter [here](https://tinyapl.rubenverg.com/run/latest) ## Features diff --git a/docs/interpreters/0.5.0/ghc_wasm_jsffi.js b/docs/interpreters/0.5.0/ghc_wasm_jsffi.js new file mode 100644 index 0000000..7c0dfa1 --- /dev/null +++ b/docs/interpreters/0.5.0/ghc_wasm_jsffi.js @@ -0,0 +1,122 @@ +// This file implements the JavaScript runtime logic for Haskell +// modules that use JSFFI. It is not an ESM module, but the template +// of one; the post-linker script will copy all contents into a new +// ESM module. + +// Manage a mapping from unique 32-bit ids to actual JavaScript +// values. +class JSValManager { + #lastk = 0; + #kv = new Map(); + + constructor() {} + + // Maybe just bump this.#lastk? For 64-bit ids that's sufficient, + // but better safe than sorry in the 32-bit case. + #allocKey() { + let k = this.#lastk; + while (true) { + if (!this.#kv.has(k)) { + this.#lastk = k; + return k; + } + k = (k + 1) | 0; + } + } + + newJSVal(v) { + const k = this.#allocKey(); + this.#kv.set(k, v); + return k; + } + + // A separate has() call to ensure we can store undefined as a value + // too. Also, unconditionally check this since the check is cheap + // anyway, if the check fails then there's a use-after-free to be + // fixed. + getJSVal(k) { + if (!this.#kv.has(k)) { + throw new WebAssembly.RuntimeError(`getJSVal(${k})`); + } + return this.#kv.get(k); + } + + // Check for double free as well. + freeJSVal(k) { + if (!this.#kv.delete(k)) { + throw new WebAssembly.RuntimeError(`freeJSVal(${k})`); + } + } +} + +// A simple & fast setImmediate() implementation for browsers. It's +// not a drop-in replacement for node.js setImmediate() because: +// 1. There's no clearImmediate(), and setImmediate() doesn't return +// anything +// 2. There's no guarantee that callbacks scheduled by setImmediate() +// are executed in the same order (in fact it's the opposite lol), +// but you are never supposed to rely on this assumption anyway +class SetImmediate { + #fs = []; + #mc = new MessageChannel(); + + constructor() { + this.#mc.port1.addEventListener("message", () => { + this.#fs.pop()(); + }); + this.#mc.port1.start(); + } + + setImmediate(cb, ...args) { + this.#fs.push(() => cb(...args)); + this.#mc.port2.postMessage(undefined); + } +} + +// The actual setImmediate() to be used. This is a ESM module top +// level binding and doesn't pollute the globalThis namespace. +let setImmediate; +if (globalThis.setImmediate) { + // node.js, bun + setImmediate = globalThis.setImmediate; +} else { + try { + // deno + setImmediate = (await import("node:timers")).setImmediate; + } catch { + // browsers + const sm = new SetImmediate(); + setImmediate = (cb, ...args) => sm.setImmediate(cb, ...args); + } +} + +export default (__exports) => { +const __ghc_wasm_jsffi_jsval_manager = new JSValManager(); +const __ghc_wasm_jsffi_finalization_registry = new FinalizationRegistry(sp => __exports.rts_freeStablePtr(sp)); +return { +newJSVal: (v) => __ghc_wasm_jsffi_jsval_manager.newJSVal(v), +getJSVal: (k) => __ghc_wasm_jsffi_jsval_manager.getJSVal(k), +freeJSVal: (k) => __ghc_wasm_jsffi_jsval_manager.freeJSVal(k), +scheduleWork: () => setImmediate(__exports.rts_schedulerLoop), +ZC25ZCtinyaplzm0zi5zi0zi0zminplacezmtinyaplzmjsZCMainZC: ($1,$2) => {return [$1, $2];}, +ZC26ZCtinyaplzm0zi5zi0zi0zminplacezmtinyaplzmjsZCMainZC: async ($1,$2) => {await $1($2);}, +ZC27ZCtinyaplzm0zi5zi0zi0zminplacezmtinyaplzmjsZCMainZC: async ($1) => {return await $1();}, +ZC28ZCtinyaplzm0zi5zi0zi0zminplacezmtinyaplzmjsZCMainZC: ($1) => {return $1;}, +ZC29ZCtinyaplzm0zi5zi0zi0zminplacezmtinyaplzmjsZCMainZC: ($1) => {return $1;}, +ZC30ZCtinyaplzm0zi5zi0zi0zminplacezmtinyaplzmjsZCMainZC: ($1,$2) => {const a = $2.slice(); a.unshift($1); return a;}, +ZC31ZCtinyaplzm0zi5zi0zi0zminplacezmtinyaplzmjsZCMainZC: () => {return [];}, +ZC0ZCghczminternalZCGHCziInternalziWasmziPrimziExportsZC: ($1,$2) => ($1.reject(new WebAssembly.RuntimeError($2))), +ZC16ZCghczminternalZCGHCziInternalziWasmziPrimziExportsZC: ($1,$2) => ($1.resolve($2)), +ZC18ZCghczminternalZCGHCziInternalziWasmziPrimziExportsZC: ($1,$2) => ($1.resolve($2)), +ZC19ZCghczminternalZCGHCziInternalziWasmziPrimziExportsZC: ($1) => ($1.resolve()), +ZC20ZCghczminternalZCGHCziInternalziWasmziPrimziExportsZC: () => {let res, rej; const p = new Promise((resolve, reject) => { res = resolve; rej = reject; }); p.resolve = res; p.reject = rej; return p;}, +ZC17ZCghczminternalZCGHCziInternalziWasmziPrimziImportsZC: ($1,$2) => ($1.then(res => __exports.rts_promiseResolveJSVal($2, res), err => __exports.rts_promiseReject($2, err))), +ZC18ZCghczminternalZCGHCziInternalziWasmziPrimziImportsZC: ($1,$2) => ($1.then(() => __exports.rts_promiseResolveUnit($2), err => __exports.rts_promiseReject($2, err))), +ZC0ZCghczminternalZCGHCziInternalziWasmziPrimziTypesZC: ($1) => (`${$1.stack ? $1.stack : $1}`), +ZC1ZCghczminternalZCGHCziInternalziWasmziPrimziTypesZC: ($1,$2) => ((new TextDecoder('utf-8', {fatal: true})).decode(new Uint8Array(__exports.memory.buffer, $1, $2))), +ZC2ZCghczminternalZCGHCziInternalziWasmziPrimziTypesZC: ($1,$2,$3) => ((new TextEncoder()).encodeInto($1, new Uint8Array(__exports.memory.buffer, $2, $3)).written), +ZC3ZCghczminternalZCGHCziInternalziWasmziPrimziTypesZC: ($1) => ($1.length), +ZC4ZCghczminternalZCGHCziInternalziWasmziPrimziTypesZC: ($1) => {if (!__ghc_wasm_jsffi_finalization_registry.unregister($1)) { throw new WebAssembly.RuntimeError('js_callback_unregister'); }}, +ZC0ZCghczminternalZCGHCziInternalziWasmziPrimziConcziInternalZC: async ($1) => (new Promise(res => setTimeout(res, $1 / 1000))), +}; +}; diff --git a/docs/interpreters/0.5.0/index.html b/docs/interpreters/0.5.0/index.html new file mode 100644 index 0000000..77fe67b --- /dev/null +++ b/docs/interpreters/0.5.0/index.html @@ -0,0 +1,81 @@ + + + + TinyAPL Interpreter + + + + + +
+

+	
+ + +
+ +

+	
+
diff --git a/docs/interpreters/0.5.0/index.js b/docs/interpreters/0.5.0/index.js
new file mode 100644
index 0000000..c4afb8d
--- /dev/null
+++ b/docs/interpreters/0.5.0/index.js
@@ -0,0 +1,199 @@
+import * as tinyapl from './tinyapl.js';
+
+const buttons = document.querySelector('#buttons');
+const output = document.querySelector('#output');
+/** @type {HTMLInputElement} */
+const input = document.querySelector('#input');
+const highlighted = document.querySelector('#highlighted');
+const button = document.querySelector('#button');
+
+function zip(as, bs) {
+	return [...as, ...bs].slice(0, Math.min(as.length, bs.length)).map((_, idx) => [as[idx], bs[idx]]);
+}
+
+const prefix = { code: 'Backquote', sym: '`' };
+
+const keyboard = [
+	['Backquote', '`', '~', undefined, '⍨', '⋄', '⌺'],
+	['Digit1', '1', '!', '¨', undefined, undefined, undefined],
+	['Digit2', '2', '@', '¯', undefined, undefined, undefined],
+	['Digit3', '3', '#', undefined, '⍒', undefined, undefined],
+	['Digit4', '4', '$', '≤', '⍋', '⊴', undefined],
+	['Digit5', '5', '%', undefined, undefined, undefined, undefined],
+	['Digit6', '6', '^', '≥', '⍉', '⊵', undefined],
+	['Digit7', '7', '&', undefined, '⊖', undefined, undefined],
+	['Digit8', '8', '*', '≠', '⍣', '⍟', '∞'],
+	['Digit9', '9', '(', '∨', '⍱', undefined, undefined],
+	['Digit0', '0', ')', '∧', '⍲', '⍬', undefined],
+	['Minus', '-', '_', '×', '⊗', '⸚', undefined],
+	['Equal', '=', '+', '÷', '⊕', '⌹', undefined],
+	['KeyQ', 'q', 'Q', undefined, undefined, undefined, undefined],
+	['KeyW', 'w', 'W', '⍵', '⍹', undefined, undefined],
+	['KeyE', 'e', 'E', '∊', '⍷', '⏨', '⋷'],
+	['KeyR', 'r', 'R', '⍴', '√', 'ϼ', 'ℜ'],
+	['KeyT', 't', 'T', '⊞', '⍨', undefined, undefined],
+	['KeyY', 'y', 'Y', '↑', '↟', undefined, undefined],
+	['KeyU', 'u', 'U', '↓', '↡', undefined, undefined],
+	['KeyI', 'i', 'I', '⍳', '⍸', '…', 'ℑ'],
+	['KeyO', 'o', 'O', '○', '⍥', undefined, undefined],
+	['KeyP', 'p', 'P', undefined, undefined, undefined, undefined],
+	['BracketLeft', '[', '{', '←', '⟨', undefined, undefined],
+	['BracketRight', ']', '}', undefined, '⟩', undefined, undefined],
+	['KeyA', 'a', 'A', '⍺', '⍶', undefined, undefined],
+	['KeyS', 's', 'S', '⌈', '§', '↾', undefined],
+	['KeyD', 'd', 'D', '⌊', '⸠', '⇂', undefined],
+	['KeyF', 'f', 'F', '⍛', '∡', '∠', undefined],
+	['KeyG', 'g', 'G', '∇', '⍢', undefined, undefined],
+	['KeyH', 'h', 'H', '∆', '⍙', '⊸', '⟜'],
+	['KeyJ', 'j', 'J', '∘', '⍤', 'ᴊ', undefined],
+	['KeyK', 'k', 'K', '⍆', '⌸', '⍅', undefined],
+	['KeyL', 'l', 'L', '⎕', '⌷', undefined, undefined],
+	['Semicolon', ';', ':', '⍎', '≡', '⍮', '■'],
+	['Quote', '\'', '"', '⍕', '≢', '⍘', '⍞'],
+	['Backslash', '\\', '|', '⊢', '⊣', undefined, undefined],
+	['KeyZ', 'z', 'Z', '⊂', '⊆', undefined, undefined],
+	['KeyX', 'x', 'X', '⊃', '⊇', '↗', undefined],
+	['KeyC', 'c', 'C', '∩', '⍝', '⟃', '⟄'],
+	['KeyV', 'v', 'V', '∪', '⁖', undefined, undefined],
+	['KeyB', 'b', 'B', '⊥', undefined, undefined, undefined],
+	['KeyN', 'n', 'N', '⊤', undefined, undefined, undefined],
+	['KeyM', 'm', 'M', '«', '»', undefined, undefined],
+	['Comma', ',', '<', '⍪', 'ᑈ', '⊲', undefined],
+	['Period', '.', '>', '∙', 'ᐵ', '⊳', undefined],
+	['Slash', '/', '?', '⌿', undefined, undefined, undefined],
+].map(([code, sym, symS, symP, symPS, symPP, symPPS]) => ({ code, sym, symS, symP, symPS, symPP, symPPS }));
+
+const colors = {
+	other: 'unset',
+	syntax: 'unset',
+	number: '#ea0027',
+	string: '#0079d3',
+	stringEscape: '#0266b3',
+	arrayName: '#ff4500',
+	primArray: '#cc3600',
+	functionName: '#46d160',
+	primFunction: '#349e48',
+	adverbName: '#ff66ac',
+	primAdverb: '#cc5289',
+	conjunctionName: '#ffd635',
+	primConjunction: '#ccac2b',
+	comment: '#014980',
+};
+
+async function highlight() {
+	const code = input.value;
+	const pairs = zip(await tinyapl.splitString(code), await tinyapl.highlight(code));
+	highlighted.innerHTML = '';
+	for (const [t, c] of pairs) {
+		const span = document.createElement('span');
+		span.style.color = colors[tinyapl.colorsInv[c]];
+		span.innerText = t;
+		highlighted.appendChild(span);
+	}
+	highlighted.scrollLeft = input.scrollLeft;
+}
+
+function insertText(str) {
+	input.setRangeText(str, input.selectionStart, input.selectionEnd, "end");
+	input.focus();
+	highlight();
+}
+
+const io = new class IO {
+	#input = [];
+	#output = [];
+	#error = [];
+	rInput(l) { this.#input.push(l); }
+	rOutput(l) { this.#output.push(l); }
+	rError(l) { this.#error.push(l); }
+	done() { this.#input = []; this.#output = []; this.#error = []; }
+	async input() { const i = window.prompt('Input'); for (const l of this.#input) await l(i); return i; }
+	async output(what) { for (const l of this.#output) await l(what); }
+	async error(what) { for (const l of this.#error) await l(what); }
+};
+
+for (const k of ['syntax', 'identifiers', 'arrays', 'functions', 'adverbs', 'conjunctions']) {
+	for (const i of tinyapl.glyphs[k]) {
+		let v, p;
+		if (v = keyboard.find(k => k.symP === i)) p = `${prefix.sym}${v.sym}`;
+		else if (v = keyboard.find(k => k.symPS === i)) p = `${prefix.sym}${v.symS}`;
+		else if (v = keyboard.find(k => k.symPP === i)) p = `${prefix.sym}${prefix.sym}${v.sym}`;
+		else if (v = keyboard.find(k => k.symPPS === i)) p = `${prefix.sym}${prefix.sym}${v.symS}`;
+		const b = document.createElement('button');
+		b.textContent = i;
+		if (p !== undefined) b.title = `Input: ${p}`;
+		b.addEventListener('click', () => { insertText(i); });
+		buttons.appendChild(b);
+	}
+	buttons.appendChild(document.createElement('br'));
+}
+
+const context = await tinyapl.newContext(io.input.bind(io), io.output.bind(io), io.error.bind(io));
+
+function div(cls, contents) {
+	const div = document.createElement('div');
+	div.className = cls;
+	div.textContent = contents;
+	return div;
+}
+
+function clickableDiv(cls, contents, clickedContents = contents) {
+	const d = div(cls, contents);
+	d.addEventListener('click', () => {
+		if (input.value.trim() == '') {
+			input.value = clickedContents;
+			input.focus();
+			highlight();
+		}
+	});
+	return d;
+}
+
+async function runCode(code) {
+	output.appendChild(clickableDiv('code', ' '.repeat(6) + code, code));
+	const d = div('quad', '');
+	output.appendChild(d);
+	io.rInput(what => { d.innerText += what + '\n'; });
+	io.rOutput(what => { d.innerText += what; });
+	io.rError(what => { d.innerText += what; });
+	const [result, success] = await tinyapl.runCode(context, code);
+	io.done();
+	if (d.textContent.trim() === '') output.removeChild(d);
+	if (d.textContent.at(-1) === '\n') d.textContent = d.textContent.slice(0, -1);
+	if (success) output.appendChild(clickableDiv('result', result));
+	else output.appendChild(div('error', result));
+}
+
+async function run() {
+	await runCode(input.value);
+	input.value = '';
+	highlight();
+}
+
+let keyboardState = 0;
+
+button.addEventListener('click', () => run());
+input.addEventListener('keydown', evt => {
+	if (keyboardState < 2 && evt.code == prefix.code && !evt.shiftKey) {
+		keyboardState++
+		evt.preventDefault();
+	} else if (keyboardState !== 0 && !evt.altKey && !evt.ctrlKey && !evt.metaKey) {
+		const v = keyboard.find(k => k.code == evt.code);
+		if (v) {
+			const t = keyboardState == 2 ? (evt.shiftKey ? v.symPPS : v.symPP) : (evt.shiftKey ? v.symPS : v.symP);
+			insertText(t ?? evt.key);
+			keyboardState = 0;
+			evt.preventDefault();
+		}
+	} else if (evt.key == 'Enter') {
+		evt.preventDefault();
+		return run();
+	}
+});
+input.addEventListener('input', () => highlight());
+input.addEventListener('scroll', () => highlight());
+
+const search = new URLSearchParams(window.location.search);
+
+for (const line of search.getAll('run')) await runCode(decodeURIComponent(line));
+
diff --git a/docs/interpreters/0.5.0/tinyapl-js.wasm b/docs/interpreters/0.5.0/tinyapl-js.wasm
new file mode 100755
index 0000000..b4dbb3d
Binary files /dev/null and b/docs/interpreters/0.5.0/tinyapl-js.wasm differ
diff --git a/docs/interpreters/0.5.0/tinyapl.js b/docs/interpreters/0.5.0/tinyapl.js
new file mode 100644
index 0000000..f29fd6d
--- /dev/null
+++ b/docs/interpreters/0.5.0/tinyapl.js
@@ -0,0 +1,86 @@
+import { WASI, OpenFile, File, ConsoleStdout } from 'https://esm.run/@bjorn3/browser_wasi_shim@0.3.0';
+import ghc_wasm_jsffi from './ghc_wasm_jsffi.js';
+
+const args = [];
+const env = [];
+const files = [
+	new OpenFile(new File([])), // stdin
+	ConsoleStdout.lineBuffered(msg => console.log(`[WASI] ${msg}`)), // stdout
+	ConsoleStdout.lineBuffered(msg => console.warn(`[WASI] ${msg}`)), // stderr
+];
+const options = {};
+const wasi = new WASI(args, env, files, options);
+
+const instanceExports = {};
+const { instance } = await WebAssembly.instantiateStreaming(fetch("./tinyapl-js.wasm"), {
+	wasi_snapshot_preview1: wasi.wasiImport,
+	ghc_wasm_jsffi: ghc_wasm_jsffi(instanceExports),
+});
+Object.assign(instanceExports, instance.exports);
+
+wasi.initialize(instance);
+
+await instance.exports.hs_start();
+
+/**
+ * Create a new context for TinyAPL code
+ * @param {() => string | Promise} input Function providing standard input
+ * @param {(what: string) => void | Promise} output Function providing standard output
+ * @param {(what: string) => void | Promise} error Function providing standard error
+ * @returns {Promise} Scope ID
+ */
+export async function newContext(input, output, error) {
+	return await instance.exports.tinyapl_newContext(input, output, error);
+}
+
+/**
+ * Run code in a context
+ * @param {number} context Context ID
+ * @param {string} code
+ * @returns {Promise<[string, boolean]>} A pair containing the result of the code or the error and whether running succeeded
+ */
+export async function runCode(context, code) {
+	const [result, success] = await instance.exports.tinyapl_runCode(context, code);
+	return [result, Boolean(success)];
+}
+
+/**
+ * Higlight a piece of code
+ * @param {string} code
+ * @returns {Promise} Colors of each character
+ */
+export async function highlight(code) {
+	return await instance.exports.tinyapl_highlight(code);
+}
+
+/**
+ * Split a string into UTF32 codepoints
+ * @param {string} str
+ * @returns {Promise}
+ */
+export async function splitString(str) {
+	return await instance.exports.tinyapl_splitString(str);
+}
+
+/**
+ * @type {Record}
+ */
+export const glyphs = {
+	syntax: await instance.exports.tinyapl_glyphsSyntax(),
+	identifiers: await instance.exports.tinyapl_glyphsIdentifiers(),
+	arrays: await instance.exports.tinyapl_glyphsArrays(),
+	functions: await instance.exports.tinyapl_glyphsFunctions(),
+	adverbs: await instance.exports.tinyapl_glyphsAdverbs(),
+	conjunctions: await instance.exports.tinyapl_glyphsConjunctions(),
+};
+
+/**
+ * @type {Record}
+ */
+export const colors = Object.fromEntries(await Promise.all(Object.entries(instance.exports).filter(([k]) => k.startsWith('tinyapl_hl')).map(async ([k, v]) => [k['tinyapl_hl'.length].toLowerCase() + k.slice('tinyapl_hl'.length + 1), await v()])));
+
+/**
+ * @type {Record}
+ */
+export const colorsInv = Object.fromEntries(Object.entries(colors).map(([k, v]) => [v, k]));
+
diff --git a/docs/interpreters/latest/ghc_wasm_jsffi.js b/docs/interpreters/latest/ghc_wasm_jsffi.js
index 3e50da8..7c0dfa1 100644
--- a/docs/interpreters/latest/ghc_wasm_jsffi.js
+++ b/docs/interpreters/latest/ghc_wasm_jsffi.js
@@ -98,13 +98,13 @@ newJSVal: (v) => __ghc_wasm_jsffi_jsval_manager.newJSVal(v),
 getJSVal: (k) => __ghc_wasm_jsffi_jsval_manager.getJSVal(k),
 freeJSVal: (k) => __ghc_wasm_jsffi_jsval_manager.freeJSVal(k),
 scheduleWork: () => setImmediate(__exports.rts_schedulerLoop),
-ZC25ZCtinyaplzm0zi4zi2zi0zminplacezmtinyaplzmjsZCMainZC: ($1,$2) => {return [$1, $2];},
-ZC26ZCtinyaplzm0zi4zi2zi0zminplacezmtinyaplzmjsZCMainZC: async ($1,$2) => {await $1($2);},
-ZC27ZCtinyaplzm0zi4zi2zi0zminplacezmtinyaplzmjsZCMainZC: async ($1) => {return await $1();},
-ZC28ZCtinyaplzm0zi4zi2zi0zminplacezmtinyaplzmjsZCMainZC: ($1) => {return $1;},
-ZC29ZCtinyaplzm0zi4zi2zi0zminplacezmtinyaplzmjsZCMainZC: ($1) => {return $1;},
-ZC30ZCtinyaplzm0zi4zi2zi0zminplacezmtinyaplzmjsZCMainZC: ($1,$2) => {const a = $2.slice(); a.unshift($1); return a;},
-ZC31ZCtinyaplzm0zi4zi2zi0zminplacezmtinyaplzmjsZCMainZC: () => {return [];},
+ZC25ZCtinyaplzm0zi5zi0zi0zminplacezmtinyaplzmjsZCMainZC: ($1,$2) => {return [$1, $2];},
+ZC26ZCtinyaplzm0zi5zi0zi0zminplacezmtinyaplzmjsZCMainZC: async ($1,$2) => {await $1($2);},
+ZC27ZCtinyaplzm0zi5zi0zi0zminplacezmtinyaplzmjsZCMainZC: async ($1) => {return await $1();},
+ZC28ZCtinyaplzm0zi5zi0zi0zminplacezmtinyaplzmjsZCMainZC: ($1) => {return $1;},
+ZC29ZCtinyaplzm0zi5zi0zi0zminplacezmtinyaplzmjsZCMainZC: ($1) => {return $1;},
+ZC30ZCtinyaplzm0zi5zi0zi0zminplacezmtinyaplzmjsZCMainZC: ($1,$2) => {const a = $2.slice(); a.unshift($1); return a;},
+ZC31ZCtinyaplzm0zi5zi0zi0zminplacezmtinyaplzmjsZCMainZC: () => {return [];},
 ZC0ZCghczminternalZCGHCziInternalziWasmziPrimziExportsZC: ($1,$2) => ($1.reject(new WebAssembly.RuntimeError($2))),
 ZC16ZCghczminternalZCGHCziInternalziWasmziPrimziExportsZC: ($1,$2) => ($1.resolve($2)),
 ZC18ZCghczminternalZCGHCziInternalziWasmziPrimziExportsZC: ($1,$2) => ($1.resolve($2)),
diff --git a/docs/interpreters/latest/tinyapl-js.wasm b/docs/interpreters/latest/tinyapl-js.wasm
index faca08f..b4dbb3d 100755
Binary files a/docs/interpreters/latest/tinyapl-js.wasm and b/docs/interpreters/latest/tinyapl-js.wasm differ
diff --git a/tinyapl.cabal b/tinyapl.cabal
index ebea075..a3742e1 100644
--- a/tinyapl.cabal
+++ b/tinyapl.cabal
@@ -1,6 +1,6 @@
 cabal-version:      3.0
 name:               tinyapl
-version:            0.4.2.0
+version:            0.5.0.0
 synopsis:           TinyAPL is a tiny APL dialect
 
 -- A longer description of the package.