3a0f328f90
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).
164 lines
5.2 KiB
Rust
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');
|
|
})"#
|
|
}
|
|
}
|
|
}
|