aboutsummaryrefslogtreecommitdiff
path: root/src/web
diff options
context:
space:
mode:
Diffstat (limited to 'src/web')
-rwxr-xr-xsrc/web/build.sh12
-rw-r--r--src/web/index.html60
-rw-r--r--src/web/port_wasm.c86
-rw-r--r--src/web/zc.js28
4 files changed, 186 insertions, 0 deletions
diff --git a/src/web/build.sh b/src/web/build.sh
new file mode 100755
index 0000000..48c784e
--- /dev/null
+++ b/src/web/build.sh
@@ -0,0 +1,12 @@
+#!/bin/bash
+# builds src/web/zc.wasm (or the path given as the first argument): the microcore, with the compiler genome inside, as
+# WebAssembly. needs clang with the wasm32 target and wasm-ld. the page
+# (src/web/index.html) loads it next to src/web/zc.js.
+set -e
+cd "$(dirname "$0")/../.."
+clang --target=wasm32 -O2 -w -fwrapv -nostdlib -fno-builtin -mbulk-memory -DJTYPE=int \
+ -Isrc/core -Isrc/micro src/micro/micro.c src/web/port_wasm.c \
+ -Wl,--no-entry -Wl,--export=wasm_input -Wl,--export=wasm_input_cap -Wl,--export=wasm_set_input -Wl,--export=wasm_run \
+ -Wl,--export=memory -Wl,--allow-undefined -Wl,--initial-memory=33554432 -Wl,-z,stack-size=65536 \
+ -o "${1:-src/web/zc.wasm}"
+ls -l "${1:-src/web/zc.wasm}"
diff --git a/src/web/index.html b/src/web/index.html
new file mode 100644
index 0000000..4cf7476
--- /dev/null
+++ b/src/web/index.html
@@ -0,0 +1,60 @@
+<!doctype html>
+<html lang="en">
+<meta charset="utf-8">
+<meta name="viewport" content="width=device-width, initial-scale=1">
+<title>zc in the browser</title>
+<style>
+ :root { --bg: #fafaf7; --fg: #1d1d1b; --mute: #6b6b66; --line: #d9d9d2; --accent: #2f5d50; --box: #fff; }
+ @media (prefers-color-scheme: dark) { :root { --bg: #161614; --fg: #e8e8e2; --mute: #9a9a92; --line: #33332f; --accent: #7fb5a3; --box: #1d1d1a; } }
+ * { box-sizing: border-box; }
+ body { margin: 0; background: var(--bg); color: var(--fg); font: 15px/1.5 system-ui, sans-serif; }
+ main { max-width: 960px; margin: 0 auto; padding: 24px 16px 48px; }
+ h1 { font-size: 1.3rem; margin: 0 0 4px; }
+ p { color: var(--mute); margin: 0 0 16px; }
+ .bar { display: flex; gap: 8px; align-items: center; flex-wrap: wrap; margin-bottom: 8px; }
+ select, button { font: inherit; padding: 6px 12px; border: 1px solid var(--line); border-radius: 6px; background: var(--box); color: var(--fg); }
+ button { background: var(--accent); color: var(--bg); border-color: var(--accent); cursor: pointer; }
+ textarea, pre { width: 100%; font: 13px/1.45 ui-monospace, Menlo, Consolas, monospace; border: 1px solid var(--line); border-radius: 6px; background: var(--box); color: var(--fg); padding: 12px; margin: 0; tab-size: 4; }
+ textarea { height: 340px; resize: vertical; }
+ pre { min-height: 120px; white-space: pre-wrap; overflow-wrap: anywhere; margin-top: 12px; }
+ small { color: var(--mute); }
+</style>
+<main>
+ <h1>zc in the browser</h1>
+ <p>the microcore compiled to webassembly, with the whole compiler inside. the program below is compiled and run by it, the same as <code>zy &lt; file.zc</code>.</p>
+ <div class="bar">
+ <select id="ex"></select>
+ <button id="run">run</button>
+ <small id="status">loading</small>
+ </div>
+ <textarea id="src" spellcheck="false">fn main() {
+ put_s("hello from the browser\n");
+ return 0;
+}
+</textarea>
+ <pre id="out" aria-live="polite"></pre>
+</main>
+<script src="zc.js"></script>
+<script>
+ const $ = id => document.getElementById(id);
+ const examples = ['hello', 'fact', 'primes', 'mandel', 'lists', 'typed', 'lang', 'struct_closure_opt', 'opt_test'];
+ let machine = null;
+ for (const n of examples) { const o = document.createElement('option'); o.value = n; o.textContent = n + '.zc'; $('ex').appendChild(o); }
+ $('ex').onchange = async () => {
+ try { $('src').value = await (await fetch('../examples/' + $('ex').value + '.zc')).text(); }
+ catch (e) { $('out').textContent = 'the examples folder is not served next to this page'; }
+ };
+ $('run').onclick = async () => {
+ if (!machine) return;
+ $('status').textContent = 'running';
+ const t = performance.now();
+ try {
+ const r = await machine.run($('src').value);
+ $('out').textContent = r.output + (r.messages ? '\n' + r.messages : '');
+ $('status').textContent = 'done in ' + Math.round(performance.now() - t) + ' ms';
+ } catch (e) { $('out').textContent = String(e); $('status').textContent = 'failed'; }
+ };
+ ZC.load('zc.wasm').then(m => { machine = m; $('status').textContent = 'ready'; })
+ .catch(e => { $('status').textContent = 'could not load zc.wasm; run src/web/build.sh and serve this folder over http'; });
+</script>
+</html>
diff --git a/src/web/port_wasm.c b/src/web/port_wasm.c
new file mode 100644
index 0000000..c1e8483
--- /dev/null
+++ b/src/web/port_wasm.c
@@ -0,0 +1,86 @@
+/* WebAssembly port of the microcore. the page hands the program over as
+ bytes, calls run(), and gets output back one byte at a time through the
+ one import. build with src/web/build.sh; nothing here needs libc. */
+#include "micro_port.h"
+
+extern void js_putc(int c);
+extern void js_msg(int c);
+
+static unsigned char inbuf[1 << 20];
+static int inn, inp;
+
+/* where the page writes the program, and how it says how long it is */
+unsigned char *wasm_input(void) { return inbuf; }
+int wasm_input_cap(void) { return (int)sizeof inbuf; }
+void wasm_set_input(int n) { inn = n; inp = 0; }
+
+/* the machine's memory: the linear memory above everything the module has in it.
+ the base is fixed when the first machine is made, and a grow only adds pages on
+ the top, so the base never moves. */
+static unsigned char *mbase;
+static UCell mcap; /* bytes of machine memory the module has grown for */
+
+Cell *micro_mem_alloc(UCell words)
+{
+ UCell need, extra, pages;
+ if (words > ((UCell)-1) / sizeof(Cell)) return 0;
+ if (!mbase) {
+ mbase = (unsigned char *)((UCell)__builtin_wasm_memory_size(0) * 65536u);
+ mcap = 0;
+ }
+ need = words * sizeof(Cell);
+ if (need <= mcap) return (Cell *)mbase;
+ extra = need - mcap;
+ pages = (extra + 65535u) / 65536u;
+ if (__builtin_wasm_memory_grow(0, (int)pages) < 0) return 0;
+ mcap += pages * 65536u; /* wasm zeroes the pages it grows by */
+ return (Cell *)mbase;
+}
+
+int micro_getc(void) { return inp < inn ? inbuf[inp++] : -1; }
+int micro_eof(void) { return inp >= inn; }
+void micro_putc(int c) { js_putc(c & 255); }
+void micro_msg(const char *s) { while (*s) js_msg(*s++); }
+
+Cell micro_syscall(Cell *mem, Cell op, Cell a, Cell b, Cell c)
+{
+ switch (op) {
+ case 9: /* memory: the size of the machine and its heap */
+ if (a == 0) return micro_mem_words();
+ if (a == 1) return HEAP_LO;
+ if (a == 2) return HEAP_HI;
+ if (a == 3) return (Cell)micro_mem_words() * 8;
+ return -1;
+ case 7: /* exit: the page sees run() return */
+ return -1;
+ case 21: /* argc: the page has no command line, so <eval> alone */
+ return 1;
+ case 22: /* argv: entry 0 is <eval>, as in zy(1) */
+ if (a != 0) return -1;
+ if (!b) return 6;
+ if (c < 7 || b + 6 >= micro_mem_words()) return -1;
+ {
+ static const char lead[] = "<eval>";
+ int i;
+ for (i = 0; i < 6; i++) mem[b + i] = (unsigned char)lead[i];
+ mem[b + 6] = 0;
+ }
+ return 6;
+ case 23: /* envc: a page has no environment */
+ return 0;
+ }
+ return -1;
+}
+
+/* the core multiplies 64 by 64 into 128 bits for the float helpers; wasm has no
+ such instruction and there is no compiler runtime to link, so this is it */
+void __multi3(unsigned long long *r, unsigned long long al, unsigned long long ah, unsigned long long bl, unsigned long long bh)
+{
+ unsigned long long a0 = al & 0xffffffffu, a1 = al >> 32, b0 = bl & 0xffffffffu, b1 = bl >> 32;
+ unsigned long long p00 = a0 * b0, p01 = a0 * b1, p10 = a1 * b0, p11 = a1 * b1;
+ unsigned long long mid = (p00 >> 32) + (p01 & 0xffffffffu) + (p10 & 0xffffffffu);
+ r[0] = (p00 & 0xffffffffu) | (mid << 32);
+ r[1] = p11 + (p01 >> 32) + (p10 >> 32) + (mid >> 32) + al * bh + ah * bl;
+}
+
+int wasm_run(void) { return micro_run(); }
diff --git a/src/web/zc.js b/src/web/zc.js
new file mode 100644
index 0000000..4f14d3c
--- /dev/null
+++ b/src/web/zc.js
@@ -0,0 +1,28 @@
+// runs zc in a browser or in node: the microcore compiled to webassembly,
+// with the compiler genome inside. ZC.load(bytes) gives a machine, and
+// machine.run(source) compiles and runs a program the way `zy < file.zc` does,
+// returning what it printed. every run starts from a fresh instance.
+(function (root) {
+ async function load(wasm) {
+ if (typeof wasm === 'string') wasm = await (await fetch(wasm)).arrayBuffer();
+ const mod = await WebAssembly.compile(wasm);
+ return {
+ async run(source) {
+ let out = [], msg = [], mem;
+ const inst = await WebAssembly.instantiate(mod, {
+ env: { js_putc: c => { out.push(c); }, js_msg: c => { msg.push(c); } }
+ });
+ const x = inst.exports;
+ const bytes = new TextEncoder().encode(source);
+ if (bytes.length > x.wasm_input_cap()) throw new Error('program too long for the input buffer');
+ new Uint8Array(x.memory.buffer).set(bytes, x.wasm_input());
+ x.wasm_set_input(bytes.length);
+ const status = x.wasm_run();
+ const dec = new TextDecoder();
+ return { output: dec.decode(new Uint8Array(out)), messages: dec.decode(new Uint8Array(msg)), status };
+ }
+ };
+ }
+ const ZC = { load };
+ if (typeof module !== 'undefined' && module.exports) module.exports = ZC; else root.ZC = ZC;
+})(typeof self !== 'undefined' ? self : this);