Implement core Forth runtime: dictionary, codegen, outer interpreter, REPL

- Dictionary: linked-list word headers in simulated linear memory with
  create/find/reveal, case-insensitive lookup, IMMEDIATE flag support
- WASM codegen: IR-to-WASM translation via wasm-encoder with full
  validation; all stack, arithmetic, comparison, logic, memory, control
  flow, and return stack operations; wasmtime execution tests
- Outer interpreter: tokenizer, number parsing (decimal/$hex/#dec/%bin),
  interpret/compile dispatch, control structures (IF/ELSE/THEN,
  BEGIN/UNTIL, BEGIN/WHILE/REPEAT), RECURSE, comments, string output
- 40+ primitive words registered via JIT-compiled WASM modules linked
  to shared memory/globals/table
- Interactive REPL with rustyline, piped input, and file execution
- 145 tests passing across dictionary, codegen, and runtime
This commit is contained in:
2026-03-29 22:48:37 +02:00
parent b8993f556e
commit d22a0a5756
5 changed files with 2798 additions and 33 deletions
+81 -10
View File
@@ -1,6 +1,7 @@
//! WAFER CLI: Interactive REPL and AOT compiler for WAFER Forth. //! WAFER CLI: Interactive REPL and AOT compiler for WAFER Forth.
use clap::Parser; use clap::Parser;
use wafer_core::outer::ForthVM;
/// WAFER: WebAssembly Forth Engine in Rust /// WAFER: WebAssembly Forth Engine in Rust
#[derive(Parser, Debug)] #[derive(Parser, Debug)]
@@ -21,21 +22,91 @@ struct Cli {
fn main() -> anyhow::Result<()> { fn main() -> anyhow::Result<()> {
let cli = Cli::parse(); let cli = Cli::parse();
let mut vm = ForthVM::new()?;
match cli.file { match cli.file {
Some(ref _file) => { Some(ref file) => {
// TODO: Step 9 - Load and execute Forth file let source = std::fs::read_to_string(file)?;
eprintln!("WAFER: file execution not yet implemented"); vm.evaluate(&source)?;
let output = vm.take_output();
if !output.is_empty() {
print!("{output}");
}
} }
None => { None => {
// TODO: Step 9 - Interactive REPL // Check if stdin is a pipe (not a TTY)
println!( if !atty_is_tty() {
"WAFER v{} - WebAssembly Forth Engine in Rust", // Non-interactive: read all of stdin and evaluate
env!("CARGO_PKG_VERSION") let mut input = String::new();
); std::io::Read::read_to_string(&mut std::io::stdin(), &mut input)?;
println!("Type BYE to exit."); // Evaluate line-by-line to handle multi-line input
eprintln!("REPL not yet implemented"); for line in input.lines() {
match vm.evaluate(line) {
Ok(()) => {
let output = vm.take_output();
if !output.is_empty() {
print!("{output}");
}
}
Err(e) => {
eprintln!("Error: {e}");
}
}
}
} else {
// Interactive REPL
println!(
"WAFER v{} - WebAssembly Forth Engine in Rust",
env!("CARGO_PKG_VERSION")
);
println!("Type BYE to exit.");
let mut rl = rustyline::DefaultEditor::new()?;
loop {
let prompt = if vm.is_compiling() { " ] " } else { "> " };
match rl.readline(prompt) {
Ok(line) => {
let trimmed = line.trim();
if trimmed.eq_ignore_ascii_case("BYE") {
break;
}
let _ = rl.add_history_entry(&line);
match vm.evaluate(&line) {
Ok(()) => {
let output = vm.take_output();
if !output.is_empty() {
print!("{output}");
}
if !vm.is_compiling() {
println!(" ok");
}
}
Err(e) => {
eprintln!("Error: {e}");
}
}
}
Err(
rustyline::error::ReadlineError::Interrupted
| rustyline::error::ReadlineError::Eof,
) => {
break;
}
Err(e) => {
eprintln!("Readline error: {e}");
break;
}
}
}
}
} }
} }
Ok(()) Ok(())
} }
/// Check if stdin is a terminal (TTY).
fn atty_is_tty() -> bool {
use std::io::IsTerminal;
std::io::stdin().is_terminal()
}
+1 -1
View File
@@ -8,10 +8,10 @@ license.workspace = true
[dependencies] [dependencies]
wasm-encoder = { workspace = true } wasm-encoder = { workspace = true }
wasmparser = { workspace = true } wasmparser = { workspace = true }
wasmtime = { workspace = true }
anyhow = { workspace = true } anyhow = { workspace = true }
thiserror = { workspace = true } thiserror = { workspace = true }
[dev-dependencies] [dev-dependencies]
proptest = { workspace = true } proptest = { workspace = true }
insta = { workspace = true } insta = { workspace = true }
wasmtime = { workspace = true }
+1389 -11
View File
File diff suppressed because it is too large Load Diff
+1 -3
View File
@@ -297,9 +297,7 @@ impl Dictionary {
/// Toggle the IMMEDIATE flag on the most recent word. /// Toggle the IMMEDIATE flag on the most recent word.
pub fn toggle_immediate(&mut self) -> WaferResult<()> { pub fn toggle_immediate(&mut self) -> WaferResult<()> {
if self.latest == 0 && self.here == DICTIONARY_BASE { if self.latest == 0 && self.here == DICTIONARY_BASE {
return Err(WaferError::CompileError( return Err(WaferError::CompileError("no word defined yet".to_string()));
"no word defined yet".to_string(),
));
} }
let flags_addr = (self.latest + 4) as usize; let flags_addr = (self.latest + 4) as usize;
if flags_addr >= self.memory.len() { if flags_addr >= self.memory.len() {
+1326 -8
View File
File diff suppressed because it is too large Load Diff