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).
This commit is contained in:
2026-04-04 11:33:11 +02:00
parent 321903831d
commit 3a0f328f90
12 changed files with 1202 additions and 928 deletions
+78 -20
View File
@@ -10,9 +10,10 @@ use std::borrow::Cow;
use std::collections::HashMap;
use wasm_encoder::{
BlockType, CodeSection, ConstExpr, ElementSection, Elements, EntityType, ExportKind,
ExportSection, Function, FunctionSection, GlobalType, ImportSection, Instruction, MemArg,
MemoryType, Module, RefType, TableType, TypeSection, ValType,
BlockType, CodeSection, ConstExpr, CustomSection, DataCountSection, DataSection,
ElementSection, Elements, EntityType, ExportKind, ExportSection, Function, FunctionSection,
GlobalType, ImportSection, Instruction, MemArg, MemoryType, Module, RefType, TableType,
TypeSection, ValType,
};
use crate::dictionary::WordId;
@@ -2062,25 +2063,50 @@ fn emit_consolidated_do_loop(
f.instruction(&Instruction::Drop);
}
/// Compile all given words into a single consolidated WASM module.
/// Optional extras for exportable modules (data section, entry point, metadata).
pub struct ExportSections<'a> {
/// Memory snapshot to embed as a WASM data section.
pub memory_snapshot: &'a [u8],
/// If set, export this function index as `_start`.
pub entry_fn_index: Option<u32>,
/// JSON metadata to embed as a custom "wafer" section.
pub metadata_json: &'a [u8],
}
/// Compile multiple IR-based words into a single WASM module with direct calls.
///
/// Each word becomes a function in the module. Calls between words within the
/// module use direct `call` instructions instead of `call_indirect` through the
/// function table, enabling Cranelift to inline and optimize across word
/// boundaries.
///
/// # Arguments
///
/// * `words` - Words to consolidate, sorted by `WordId`. Each entry is
/// `(WordId, Vec<IrOp>)` containing the word's IR body.
/// * `local_fn_map` - Maps each `WordId` in the module to its WASM function
/// index (imported functions come first, so defined functions start at 1).
/// * `table_size` - Current function table size, used for table import minimum.
/// Used at runtime by `CONSOLIDATE` and during startup batch compilation.
pub fn compile_consolidated_module(
words: &[(WordId, Vec<IrOp>)],
local_fn_map: &HashMap<WordId, u32>,
table_size: u32,
) -> WaferResult<Vec<u8>> {
compile_multi_word_module(words, local_fn_map, table_size, None)
}
/// Compile an exportable WASM module with embedded memory and metadata.
///
/// Same as [`compile_consolidated_module`] but adds a WASM data section
/// (memory snapshot), an optional `_start` entry point export, and a
/// custom "wafer" section with JSON metadata.
pub fn compile_exportable_module(
words: &[(WordId, Vec<IrOp>)],
local_fn_map: &HashMap<WordId, u32>,
table_size: u32,
export: &ExportSections<'_>,
) -> WaferResult<Vec<u8>> {
compile_multi_word_module(words, local_fn_map, table_size, Some(export))
}
/// Internal: build a multi-word WASM module. When `export` is `Some`, adds
/// data section, entry-point export, and custom metadata section.
fn compile_multi_word_module(
words: &[(WordId, Vec<IrOp>)],
local_fn_map: &HashMap<WordId, u32>,
table_size: u32,
export: Option<&ExportSections<'_>>,
) -> WaferResult<Vec<u8>> {
let has_data = export.is_some_and(|e| !e.memory_snapshot.is_empty());
let mut module = Module::new();
// -- Type section --
@@ -2157,10 +2183,15 @@ pub fn compile_consolidated_module(
// +1 because emit is imported function index 0
exports.export(&name, ExportKind::Func, (i as u32) + 1);
}
// Optionally export an entry point as "_start"
if let Some(e) = export
&& let Some(fn_idx) = e.entry_fn_index
{
exports.export("_start", ExportKind::Func, fn_idx);
}
module.section(&exports);
// -- Element section: place each function in the table at its WordId slot --
// Use a single element section with one active segment per word.
let mut elements = ElementSection::new();
for (i, (word_id, _)) in words.iter().enumerate() {
let offset = ConstExpr::i32_const(word_id.0 as i32);
@@ -2174,6 +2205,11 @@ pub fn compile_consolidated_module(
}
module.section(&elements);
// -- DataCount section (required before Code when Data section is present) --
if has_data {
module.section(&DataCountSection { count: 1 });
}
// -- Code section: emit each function body --
let mut code = CodeSection::new();
for (_word_id, body) in words {
@@ -2206,12 +2242,34 @@ pub fn compile_consolidated_module(
}
module.section(&code);
// -- Data section (memory snapshot for exportable modules) --
if let Some(e) = export
&& !e.memory_snapshot.is_empty()
{
let mut data = DataSection::new();
data.active(
MEMORY_INDEX,
&ConstExpr::i32_const(0),
e.memory_snapshot.iter().copied(),
);
module.section(&data);
}
// -- Custom "wafer" section (metadata for exportable modules) --
if let Some(e) = export
&& !e.metadata_json.is_empty()
{
module.section(&CustomSection {
name: Cow::Borrowed("wafer"),
data: Cow::Borrowed(e.metadata_json),
});
}
let bytes = module.finish();
// Validate
wasmparser::validate(&bytes).map_err(|e| {
WaferError::ValidationError(format!("Consolidated WASM failed validation: {e}"))
})?;
wasmparser::validate(&bytes)
.map_err(|e| WaferError::ValidationError(format!("WASM module failed validation: {e}")))?;
Ok(bytes)
}