Files
WAFER/crates/core/src/js_loader.rs
T
ok 3a0f328f90 Implement WASM export and standalone execution
Add `wafer build` to compile Forth source files to standalone .wasm modules,
and `wafer run` to execute them. The same .wasm file works with both the
wafer runtime (via wasmtime) and in browsers (via generated JS loader).

New CLI subcommands:
- `wafer build file.fth -o file.wasm` — compile to standalone WASM
- `wafer build file.fth -o file.wasm --js` — also generate JS/HTML loader
- `wafer build file.fth --entry WORD` — custom entry point
- `wafer run file.wasm` — execute pre-compiled module

Entry point resolution: --entry flag > MAIN word > recorded top-level execution.
Memory snapshot embedded as WASM data section preserves VARIABLE/CONSTANT state.
Metadata in custom "wafer" section enables the runner to provide host functions.

New modules: export.rs (orchestration), runner.rs (wasmtime host), js_loader.rs
(browser support). Refactored codegen.rs to share logic between consolidation
and export via compile_multi_word_module(). Added ir_bodies tracking for
VARIABLE, CONSTANT, CREATE, VALUE, DEFER, BUFFER:, MARKER, 2CONSTANT,
2VARIABLE, 2VALUE, FVARIABLE defining words.

Removed dead code: dot_func field, unused wafer-web stub crate, wasmtime-wasi
dependency from CLI, orphaned --consolidate/--output CLI flags.

425 tests pass (414 original + 11 new including 7 round-trip integration tests).
2026-04-04 11:33:11 +02:00

164 lines
5.2 KiB
Rust

//! Generate JavaScript and HTML loaders for running exported WASM in the browser.
use crate::export::ExportMetadata;
/// Generate a JavaScript loader that instantiates a WAFER `.wasm` module.
///
/// The loader provides the six required imports (emit, memory, dsp, rsp, fsp,
/// table) and host-function stubs, then calls `_start`.
pub fn generate_js_loader(wasm_filename: &str, metadata: &ExportMetadata) -> String {
let (dsp, rsp, fsp) = (metadata.dsp_init, metadata.rsp_init, metadata.fsp_init);
let memory_pages = metadata.memory_size.div_ceil(65536).max(16);
// Build the host function registration code.
let mut host_registrations = String::new();
for (idx, name) in &metadata.host_functions {
let js_impl = js_host_function(name);
host_registrations.push_str(&format!(" table.set({idx}, {js_impl});\n"));
}
format!(
r#"// WAFER JS Loader - generated by wafer build --js
// Loads and runs {wasm_filename} in the browser.
const WAFER = (() => {{
const CELL_SIZE = 4;
const DATA_STACK_TOP = 0x1540;
const SYSVAR_BASE = 0x0004;
let outputCallback = (s) => {{
const el = document.getElementById('output');
if (el) el.textContent += s;
else console.log(s);
}};
async function run(opts) {{
if (opts && opts.output) outputCallback = opts.output;
const memory = new WebAssembly.Memory({{ initial: {memory_pages} }});
const dsp = new WebAssembly.Global({{ value: 'i32', mutable: true }}, {dsp});
const rsp = new WebAssembly.Global({{ value: 'i32', mutable: true }}, {rsp});
const fsp = new WebAssembly.Global({{ value: 'i32', mutable: true }}, {fsp});
const table = new WebAssembly.Table({{ element: 'anyfunc', initial: 256 }});
function emit(code) {{
outputCallback(String.fromCharCode(code));
}}
const importObject = {{
env: {{ emit, memory, dsp, rsp, fsp, table }}
}};
// Register host functions
const view = () => new DataView(memory.buffer);
const pop = () => {{
const sp = dsp.value;
const v = view().getInt32(sp, true);
dsp.value = sp + CELL_SIZE;
return v;
}};
const push = (v) => {{
const sp = dsp.value - CELL_SIZE;
view().setInt32(sp, v, true);
dsp.value = sp;
}};
{host_registrations}
const response = await fetch('{wasm_filename}');
const bytes = await response.arrayBuffer();
const {{ instance }} = await WebAssembly.instantiate(bytes, importObject);
if (instance.exports._start) {{
instance.exports._start();
}}
return instance;
}}
return {{ run }};
}})();
"#
)
}
/// Generate a minimal HTML page that loads the JS loader.
pub fn generate_html_page(wasm_filename: &str, js_filename: &str) -> String {
format!(
r#"<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>WAFER - {wasm_filename}</title>
<style>
body {{ font-family: monospace; background: #1a1a2e; color: #e0e0e0; padding: 2em; }}
#output {{ white-space: pre-wrap; font-size: 1.2em; padding: 1em; background: #16213e;
border: 1px solid #0f3460; border-radius: 4px; min-height: 4em; }}
</style>
</head>
<body>
<h2>WAFER Output</h2>
<div id="output"></div>
<script src="{js_filename}"></script>
<script>WAFER.run();</script>
</body>
</html>
"#
)
}
/// Return a JS expression that creates a `WebAssembly.Function` for a known
/// host word. Falls back to a stub that logs an error.
fn js_host_function(name: &str) -> &'static str {
match name {
"." => {
r#"new WebAssembly.Function({parameters:[], results:[]}, () => {
const n = pop();
const base = view().getUint32(SYSVAR_BASE, true);
outputCallback((base === 16 ? n.toString(16).toUpperCase() : n.toString()) + ' ');
})"#
}
"U." => {
r#"new WebAssembly.Function({parameters:[], results:[]}, () => {
const n = pop() >>> 0;
const base = view().getUint32(SYSVAR_BASE, true);
outputCallback((base === 16 ? n.toString(16).toUpperCase() : n.toString()) + ' ');
})"#
}
"TYPE" => {
r#"new WebAssembly.Function({parameters:[], results:[]}, () => {
const len = pop();
const addr = pop();
const bytes = new Uint8Array(memory.buffer, addr, len);
outputCallback(new TextDecoder().decode(bytes));
})"#
}
"SPACES" => {
r#"new WebAssembly.Function({parameters:[], results:[]}, () => {
const n = pop();
if (n > 0) outputCallback(' '.repeat(n));
})"#
}
".S" => {
r#"new WebAssembly.Function({parameters:[], results:[]}, () => {
const sp = dsp.value;
const depth = (DATA_STACK_TOP - sp) / CELL_SIZE;
let s = '<' + depth + '> ';
for (let a = DATA_STACK_TOP - CELL_SIZE; a >= sp; a -= CELL_SIZE) {
s += view().getInt32(a, true) + ' ';
}
outputCallback(s);
})"#
}
"DEPTH" => {
r#"new WebAssembly.Function({parameters:[], results:[]}, () => {
const depth = (DATA_STACK_TOP - dsp.value) / CELL_SIZE;
push(depth);
})"#
}
_ => {
r#"new WebAssembly.Function({parameters:[], results:[]}, () => {
console.error('Host function not available in standalone mode');
})"#
}
}
}