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:
Oleksandr Kozachuk
2026-08-05 14:57:12 +02:00
parent 4980648982
commit f584066a0a
6 changed files with 539 additions and 111 deletions
+14 -3
View File
@@ -18,6 +18,8 @@ pub mod flags {
pub const IMMEDIATE: u8 = 0x80;
/// Word is hidden (being compiled, not yet findable).
pub const HIDDEN: u8 = 0x40;
/// Word is an implementation detail: findable, but skipped by WORDS.
pub const INTERNAL: u8 = 0x20;
/// Mask for the name length (lower 5 bits).
pub const LENGTH_MASK: u8 = 0x1F;
/// Maximum word name length.
@@ -95,11 +97,17 @@ impl Dictionary {
// Write link field (points to previous LATEST)
self.write_u32_unchecked(entry_start, self.latest);
// Write flags byte: HIDDEN | length, optionally IMMEDIATE
// Write flags byte: HIDDEN | length, optionally IMMEDIATE.
// Underscore-prefixed names are implementation details by repo
// convention (see tools/editor-support): flag them INTERNAL so
// WORDS and completion skip them while FIND still works.
let mut flag_byte = flags::HIDDEN | (name_len as u8 & flags::LENGTH_MASK);
if immediate {
flag_byte |= flags::IMMEDIATE;
}
if name_bytes.first() == Some(&b'_') {
flag_byte |= flags::INTERNAL;
}
self.memory[(entry_start + 4) as usize] = flag_byte;
// Write name bytes
@@ -410,12 +418,15 @@ impl Dictionary {
}
/// Return names of all visible (non-hidden) words, newest first.
pub fn visible_words(&self) -> Vec<String> {
/// With `include_internal` false, words flagged INTERNAL are skipped.
pub fn visible_words(&self, include_internal: bool) -> Vec<String> {
let mut names = Vec::new();
let mut addr = self.latest;
while addr != 0 {
let flags_byte = self.memory[(addr + 4) as usize];
if flags_byte & flags::HIDDEN == 0 {
let skip = flags_byte & flags::HIDDEN != 0
|| (!include_internal && flags_byte & flags::INTERNAL != 0);
if !skip {
let name_len = (flags_byte & flags::LENGTH_MASK) as usize;
let name_start = (addr + 5) as usize;
let name = String::from_utf8_lossy(&self.memory[name_start..name_start + name_len])