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.
use clap::Parser;
use wafer_core::outer::ForthVM;
/// WAFER: WebAssembly Forth Engine in Rust
#[derive(Parser, Debug)]
@@ -21,21 +22,91 @@ struct Cli {
fn main() -> anyhow::Result<()> {
let cli = Cli::parse();
let mut vm = ForthVM::new()?;
match cli.file {
Some(ref _file) => {
// TODO: Step 9 - Load and execute Forth file
eprintln!("WAFER: file execution not yet implemented");
Some(ref file) => {
let source = std::fs::read_to_string(file)?;
vm.evaluate(&source)?;
let output = vm.take_output();
if !output.is_empty() {
print!("{output}");
}
}
None => {
// TODO: Step 9 - Interactive REPL
println!(
"WAFER v{} - WebAssembly Forth Engine in Rust",
env!("CARGO_PKG_VERSION")
);
println!("Type BYE to exit.");
eprintln!("REPL not yet implemented");
// Check if stdin is a pipe (not a TTY)
if !atty_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(()) => {
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(())
}
/// 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]
wasm-encoder = { workspace = true }
wasmparser = { workspace = true }
wasmtime = { workspace = true }
anyhow = { workspace = true }
thiserror = { workspace = true }
[dev-dependencies]
proptest = { 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.
pub fn toggle_immediate(&mut self) -> WaferResult<()> {
if self.latest == 0 && self.here == DICTIONARY_BASE {
return Err(WaferError::CompileError(
"no word defined yet".to_string(),
));
return Err(WaferError::CompileError("no word defined yet".to_string()));
}
let flags_addr = (self.latest + 4) as usize;
if flags_addr >= self.memory.len() {
+1326 -8
View File
File diff suppressed because it is too large Load Diff