diff options
| author | 2026-10-12 08:46:23 +0330 | |
|---|---|---|
| committer | 2026-10-12 08:46:23 +0330 | |
| commit | ecc350ca5c2eb50be8309c5f14f8bf0d87db9a02 (patch) | |
| tree | 70e0af76a1b13affff7c6cc8aac7b3e6ef960478 /src/web | |
a
Diffstat (limited to 'src/web')
| -rwxr-xr-x | src/web/build.sh | 12 | ||||
| -rw-r--r-- | src/web/index.html | 60 | ||||
| -rw-r--r-- | src/web/port_wasm.c | 86 | ||||
| -rw-r--r-- | src/web/zc.js | 28 |
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 < 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); |
