feat(repl): usability batch - errors, WORDS, .S, BYE, DUMP, history
Core:
- Uncaught THROW prints its standard message ("Stack underflow
(throw -4)"; unknown codes as "Catch = <n>") instead of the
"forth-throw" sentinel. ABORT" text is carried as a structured
payload and shown only when the -2 throw goes uncaught -- CATCH
stays silent and the payload cannot go stale. ABORT throws -1
through the same path.
- WORDS: optional same-line substring filter (WORDS FDEPTH), skips
internal words (new INTERNAL header flag, set at create for
underscore-prefixed names), wraps at 78 columns, prints a count.
ORDER names wids (FORTH / wid#N) instead of Rust debug output.
- .S honors BASE. New: F.S (float stack), DUMP (hex+ASCII,
bounds-checked, 4K cap), ? (fetch-and-print, boot.fth). BYE is a
real word now: sets a VM flag the driver honors (exits REPL,
stops rest of line/file).
CLI:
- Persistent history (~/.local/state/wafer/history, 0600 perms,
$WAFER_HISTORY override), Tab completion over the live dictionary
(snapshot refreshed after each line), Up/Down do prefix history
search, Ctrl-C clears the line instead of exiting.
Web:
- History survives reloads (localStorage, cap 200, dedup, init-code
runs excluded), User Words palette populated via new words()
export, stack bar annotates non-decimal BASE, base() reads the
real BASE sysvar instead of returning a hardcoded 10.
This commit is contained in:
+153
-53
@@ -285,6 +285,9 @@ fn cmd_eval_or_repl(file: Option<&str>) -> anyhow::Result<()> {
|
||||
if !output.is_empty() {
|
||||
print!("{output}");
|
||||
}
|
||||
if vm.bye_requested() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!("Error: {e}");
|
||||
@@ -292,59 +295,7 @@ fn cmd_eval_or_repl(file: Option<&str>) -> anyhow::Result<()> {
|
||||
}
|
||||
}
|
||||
} 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();
|
||||
// PAGE (form feed) clears the terminal
|
||||
if output.contains('\x0C') {
|
||||
print!("\x1b[2J\x1b[H");
|
||||
}
|
||||
let output = output.replace('\x0C', "");
|
||||
if !vm.is_compiling() {
|
||||
// Move cursor back up to end of input line so
|
||||
// output appears inline, like traditional Forth:
|
||||
// > 2 2 + . 4 ok
|
||||
let col = prompt.len() + line.len() + 1;
|
||||
print!("\x1b[A\x1b[{col}G {output} ok");
|
||||
println!();
|
||||
} else if !output.is_empty() {
|
||||
print!("{output}");
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!("Error: {e}");
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(
|
||||
rustyline::error::ReadlineError::Interrupted
|
||||
| rustyline::error::ReadlineError::Eof,
|
||||
) => {
|
||||
break;
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!("Readline error: {e}");
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
run_repl(&mut vm)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -357,3 +308,152 @@ fn stdin_is_tty() -> bool {
|
||||
use std::io::IsTerminal;
|
||||
std::io::stdin().is_terminal()
|
||||
}
|
||||
|
||||
/// Completes the token under the cursor against the live dictionary.
|
||||
struct WaferHelper {
|
||||
words: Vec<String>,
|
||||
}
|
||||
|
||||
impl rustyline::completion::Completer for WaferHelper {
|
||||
type Candidate = String;
|
||||
|
||||
fn complete(
|
||||
&self,
|
||||
line: &str,
|
||||
pos: usize,
|
||||
_ctx: &rustyline::Context<'_>,
|
||||
) -> rustyline::Result<(usize, Vec<String>)> {
|
||||
let start = line[..pos]
|
||||
.rfind(|c: char| c.is_whitespace())
|
||||
.map_or(0, |i| i + 1);
|
||||
let prefix = line[start..pos].to_ascii_uppercase();
|
||||
let mut matches: Vec<String> = self
|
||||
.words
|
||||
.iter()
|
||||
.filter(|w| w.to_ascii_uppercase().starts_with(&prefix))
|
||||
.cloned()
|
||||
.collect();
|
||||
matches.sort();
|
||||
matches.dedup();
|
||||
Ok((start, matches))
|
||||
}
|
||||
}
|
||||
|
||||
impl rustyline::hint::Hinter for WaferHelper {
|
||||
type Hint = String;
|
||||
}
|
||||
impl rustyline::highlight::Highlighter for WaferHelper {}
|
||||
impl rustyline::validate::Validator for WaferHelper {}
|
||||
impl rustyline::Helper for WaferHelper {}
|
||||
|
||||
/// History file: `$WAFER_HISTORY`, else `$XDG_STATE_HOME/wafer/history`,
|
||||
/// else `~/.local/state/wafer/history`.
|
||||
fn history_path() -> Option<std::path::PathBuf> {
|
||||
if let Some(p) = std::env::var_os("WAFER_HISTORY") {
|
||||
return Some(p.into());
|
||||
}
|
||||
let base = std::env::var_os("XDG_STATE_HOME")
|
||||
.map(std::path::PathBuf::from)
|
||||
.or_else(|| {
|
||||
std::env::var_os("HOME").map(|h| std::path::PathBuf::from(h).join(".local/state"))
|
||||
})?;
|
||||
Some(base.join("wafer/history"))
|
||||
}
|
||||
|
||||
/// Interactive REPL: line editing, persistent history with prefix search
|
||||
/// on Up/Down, and Tab completion over the live dictionary.
|
||||
fn run_repl(vm: &mut ForthVM<NativeRuntime>) -> anyhow::Result<()> {
|
||||
use rustyline::{Cmd, Editor, EventHandler, KeyCode, KeyEvent, Modifiers};
|
||||
|
||||
println!(
|
||||
"WAFER v{} - WebAssembly Forth Engine in Rust",
|
||||
env!("CARGO_PKG_VERSION")
|
||||
);
|
||||
println!("Type BYE to exit.");
|
||||
|
||||
let config = rustyline::Config::builder()
|
||||
.completion_type(rustyline::CompletionType::List)
|
||||
.history_ignore_dups(true)?
|
||||
.build();
|
||||
let mut rl: Editor<WaferHelper, rustyline::history::DefaultHistory> =
|
||||
Editor::with_config(config)?;
|
||||
rl.set_helper(Some(WaferHelper {
|
||||
words: vm.word_names(),
|
||||
}));
|
||||
// Up/Down recall only entries starting with the typed prefix
|
||||
rl.bind_sequence(
|
||||
KeyEvent(KeyCode::Up, Modifiers::NONE),
|
||||
EventHandler::Simple(Cmd::HistorySearchBackward),
|
||||
);
|
||||
rl.bind_sequence(
|
||||
KeyEvent(KeyCode::Down, Modifiers::NONE),
|
||||
EventHandler::Simple(Cmd::HistorySearchForward),
|
||||
);
|
||||
|
||||
let history = history_path();
|
||||
if let Some(path) = &history {
|
||||
if let Some(dir) = path.parent() {
|
||||
let _ = std::fs::create_dir_all(dir);
|
||||
}
|
||||
let _ = rl.load_history(path);
|
||||
}
|
||||
let save_history = |rl: &mut Editor<WaferHelper, rustyline::history::DefaultHistory>| {
|
||||
if let Some(path) = &history {
|
||||
let _ = rl.save_history(path);
|
||||
#[cfg(unix)]
|
||||
{
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
let _ = std::fs::set_permissions(path, std::fs::Permissions::from_mode(0o600));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
loop {
|
||||
let prompt = if vm.is_compiling() { " ] " } else { "> " };
|
||||
match rl.readline(prompt) {
|
||||
Ok(line) => {
|
||||
let _ = rl.add_history_entry(&line);
|
||||
match vm.evaluate(&line) {
|
||||
Ok(()) => {
|
||||
let output = vm.take_output();
|
||||
if vm.bye_requested() {
|
||||
break;
|
||||
}
|
||||
// PAGE (form feed) clears the terminal
|
||||
if output.contains('\x0C') {
|
||||
print!("\x1b[2J\x1b[H");
|
||||
}
|
||||
let output = output.replace('\x0C', "");
|
||||
if !vm.is_compiling() {
|
||||
// Move cursor back up to end of input line so
|
||||
// output appears inline, like traditional Forth:
|
||||
// > 2 2 + . 4 ok
|
||||
let col = prompt.len() + line.len() + 1;
|
||||
print!("\x1b[A\x1b[{col}G {output} ok");
|
||||
println!();
|
||||
} else if !output.is_empty() {
|
||||
print!("{output}");
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!("Error: {e}");
|
||||
}
|
||||
}
|
||||
// New definitions may have appeared: refresh completion
|
||||
if let Some(h) = rl.helper_mut() {
|
||||
h.words = vm.word_names();
|
||||
}
|
||||
save_history(&mut rl);
|
||||
}
|
||||
// Ctrl-C abandons the current line, Ctrl-D exits
|
||||
Err(rustyline::error::ReadlineError::Interrupted) => {}
|
||||
Err(rustyline::error::ReadlineError::Eof) => break,
|
||||
Err(e) => {
|
||||
eprintln!("Readline error: {e}");
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
save_history(&mut rl);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user