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:
+131
-15
@@ -1,31 +1,149 @@
|
||||
//! WAFER CLI: Interactive REPL and AOT compiler for WAFER Forth.
|
||||
//! WAFER CLI: Interactive REPL, AOT compiler, and WASM runner for WAFER Forth.
|
||||
|
||||
use clap::Parser;
|
||||
use std::path::Path;
|
||||
|
||||
use clap::{Parser, Subcommand};
|
||||
use wafer_core::export::{ExportConfig, export_module};
|
||||
use wafer_core::outer::ForthVM;
|
||||
use wafer_core::runner::run_wasm_file;
|
||||
|
||||
/// WAFER: WebAssembly Forth Engine in Rust
|
||||
#[derive(Parser, Debug)]
|
||||
#[command(name = "wafer", version, about)]
|
||||
struct Cli {
|
||||
/// Forth source file to execute
|
||||
#[command(subcommand)]
|
||||
command: Option<Commands>,
|
||||
|
||||
/// Forth source file to execute (when no subcommand is given)
|
||||
file: Option<String>,
|
||||
}
|
||||
|
||||
/// Compile all words into a single optimized WASM module
|
||||
#[arg(long)]
|
||||
consolidate: bool,
|
||||
#[derive(Subcommand, Debug)]
|
||||
enum Commands {
|
||||
/// Compile a Forth source file to a standalone WASM module
|
||||
Build {
|
||||
/// Input Forth source file
|
||||
file: String,
|
||||
|
||||
/// Output file for consolidated WASM (requires --consolidate)
|
||||
#[arg(short, long)]
|
||||
output: Option<String>,
|
||||
/// Output .wasm file (default: input with .wasm extension)
|
||||
#[arg(short, long)]
|
||||
output: Option<String>,
|
||||
|
||||
/// Entry-point word name (default: MAIN, or top-level execution)
|
||||
#[arg(long)]
|
||||
entry: Option<String>,
|
||||
|
||||
/// Also generate a JS loader and HTML page for browser execution
|
||||
#[arg(long)]
|
||||
js: bool,
|
||||
},
|
||||
|
||||
/// Run a pre-compiled WASM module
|
||||
Run {
|
||||
/// .wasm file to execute
|
||||
file: String,
|
||||
},
|
||||
}
|
||||
|
||||
fn main() -> anyhow::Result<()> {
|
||||
let cli = Cli::parse();
|
||||
|
||||
match cli.command {
|
||||
Some(Commands::Build {
|
||||
file,
|
||||
output,
|
||||
entry,
|
||||
js,
|
||||
}) => cmd_build(&file, output.as_deref(), entry, js),
|
||||
|
||||
Some(Commands::Run { file }) => cmd_run(&file),
|
||||
|
||||
None => cmd_eval_or_repl(cli.file.as_deref()),
|
||||
}
|
||||
}
|
||||
|
||||
/// `wafer build program.fth -o program.wasm`
|
||||
fn cmd_build(
|
||||
file: &str,
|
||||
output: Option<&str>,
|
||||
entry: Option<String>,
|
||||
js: bool,
|
||||
) -> anyhow::Result<()> {
|
||||
let source = std::fs::read_to_string(file)?;
|
||||
|
||||
let mut vm = ForthVM::new()?;
|
||||
vm.set_recording(true);
|
||||
vm.evaluate(&source)?;
|
||||
|
||||
// Print any side-effect output from evaluation.
|
||||
let eval_output = vm.take_output();
|
||||
if !eval_output.is_empty() {
|
||||
print!("{eval_output}");
|
||||
}
|
||||
|
||||
let config = ExportConfig { entry_word: entry };
|
||||
let (wasm_bytes, metadata) = export_module(&mut vm, &config)?;
|
||||
|
||||
// Determine output path.
|
||||
let out_path = match output {
|
||||
Some(p) => p.to_string(),
|
||||
None => {
|
||||
let stem = Path::new(file)
|
||||
.file_stem()
|
||||
.and_then(|s| s.to_str())
|
||||
.unwrap_or("out");
|
||||
format!("{stem}.wasm")
|
||||
}
|
||||
};
|
||||
|
||||
std::fs::write(&out_path, &wasm_bytes)?;
|
||||
|
||||
let word_count = vm.ir_words().len();
|
||||
let host_count = metadata.host_functions.len();
|
||||
eprintln!(
|
||||
"Wrote {out_path} ({} bytes, {word_count} words, {host_count} host functions)",
|
||||
wasm_bytes.len()
|
||||
);
|
||||
|
||||
if js {
|
||||
let out = Path::new(&out_path);
|
||||
let wasm_filename = out
|
||||
.file_name()
|
||||
.and_then(|s| s.to_str())
|
||||
.unwrap_or("out.wasm");
|
||||
let stem = out.file_stem().and_then(|s| s.to_str()).unwrap_or("out");
|
||||
let dir = out.parent().unwrap_or_else(|| Path::new("."));
|
||||
|
||||
let js_path = dir.join(format!("{stem}.js"));
|
||||
let html_path = dir.join(format!("{stem}.html"));
|
||||
let js_filename = format!("{stem}.js");
|
||||
|
||||
let js_code = wafer_core::js_loader::generate_js_loader(wasm_filename, &metadata);
|
||||
let html_code = wafer_core::js_loader::generate_html_page(wasm_filename, &js_filename);
|
||||
|
||||
std::fs::write(&js_path, &js_code)?;
|
||||
std::fs::write(&html_path, &html_code)?;
|
||||
eprintln!("Wrote {} and {}", js_path.display(), html_path.display());
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// `wafer run program.wasm`
|
||||
fn cmd_run(file: &str) -> anyhow::Result<()> {
|
||||
let output = run_wasm_file(file)?;
|
||||
if !output.is_empty() {
|
||||
print!("{output}");
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// `wafer` (REPL) or `wafer program.fth` (evaluate and exit)
|
||||
fn cmd_eval_or_repl(file: Option<&str>) -> anyhow::Result<()> {
|
||||
let mut vm = ForthVM::new()?;
|
||||
|
||||
match cli.file {
|
||||
Some(ref file) => {
|
||||
match file {
|
||||
Some(file) => {
|
||||
let source = std::fs::read_to_string(file)?;
|
||||
vm.evaluate(&source)?;
|
||||
let output = vm.take_output();
|
||||
@@ -34,12 +152,10 @@ fn main() -> anyhow::Result<()> {
|
||||
}
|
||||
}
|
||||
None => {
|
||||
// Check if stdin is a pipe (not a TTY)
|
||||
if !atty_is_tty() {
|
||||
if !stdin_is_tty() {
|
||||
// Non-interactive: read all of stdin and evaluate
|
||||
let mut input = String::new();
|
||||
std::io::Read::read_to_string(&mut std::io::stdin(), &mut input)?;
|
||||
// Evaluate line-by-line to handle multi-line input
|
||||
for line in input.lines() {
|
||||
match vm.evaluate(line) {
|
||||
Ok(()) => {
|
||||
@@ -106,7 +222,7 @@ fn main() -> anyhow::Result<()> {
|
||||
}
|
||||
|
||||
/// Check if stdin is a terminal (TTY).
|
||||
fn atty_is_tty() -> bool {
|
||||
fn stdin_is_tty() -> bool {
|
||||
use std::io::IsTerminal;
|
||||
std::io::stdin().is_terminal()
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user