Compare commits
4 Commits
v0.2.2
..
972c9544e4
| Author | SHA1 | Date | |
|---|---|---|---|
| 972c9544e4 | |||
| 5d40f32953 | |||
| 4ffa67e784 | |||
| 380250a641 |
@@ -3,6 +3,3 @@
|
|||||||
*.swp
|
*.swp
|
||||||
.DS_Store
|
.DS_Store
|
||||||
*.bk
|
*.bk
|
||||||
|
|
||||||
# Local planning notes — never tracked
|
|
||||||
/plans/
|
|
||||||
|
|||||||
-135
@@ -1,135 +0,0 @@
|
|||||||
# Changelog
|
|
||||||
|
|
||||||
All notable changes to WAFER are documented in this file.
|
|
||||||
|
|
||||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
||||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
||||||
|
|
||||||
## [0.2.2] - 2026-08-06
|
|
||||||
|
|
||||||
### Added
|
|
||||||
|
|
||||||
- **SwiftForth-style input number conversion.** Punctuation (`,` `.` `+`
|
|
||||||
`/` `:` and an embedded `-`) anywhere after the leftmost digit now forces
|
|
||||||
double-cell conversion, so `12.34`, `1,234`, `12:30:45` and `2026-08-06`
|
|
||||||
all convert as doubles without a custom parser. Previously only a
|
|
||||||
trailing `.` worked and `1.5` was an "unknown word" error. The
|
|
||||||
punctuation is a double-cell marker, not a fractional point: every
|
|
||||||
spelling of `1234` (`1234.`, `123.4`, `.1234`) yields the same value.
|
|
||||||
- **`DPL`** ( -- addr ): digits to the right of the rightmost punctuation
|
|
||||||
character in the last converted number, negative when the token carried
|
|
||||||
none. Seeded at -1024 and bumped once per digit, matching `sf64`.
|
|
||||||
Together with `<# #>` this is how fixed-point input is scaled.
|
|
||||||
- **`NH`** ( -- addr ): the high-order cell dropped by a single-cell
|
|
||||||
conversion, so a token that overflows a cell can be recovered as a
|
|
||||||
double (`4000000000 NH @ D.`).
|
|
||||||
|
|
||||||
Verified token-for-token against SwiftForth `sf64`: DPL values, double
|
|
||||||
promotion and sign handling agree on every probed form. One deliberate
|
|
||||||
divergence — WAFER also accepts a sign before a base prefix (`-$FF`), which
|
|
||||||
`sf64` rejects; the Forth 2012 spelling `$-FF` works in both. A leading `+`
|
|
||||||
is punctuation rather than a sign in both engines, so `+7` is the double 7
|
|
||||||
with `DPL` = 1.
|
|
||||||
|
|
||||||
## [0.2.1] - 2026-08-06
|
|
||||||
|
|
||||||
### Fixed
|
|
||||||
|
|
||||||
- **The search order is now authoritative** (Forth 2012 §16.3.3): a word
|
|
||||||
whose wordlist is not in the search order is no longer findable.
|
|
||||||
Previously lookup fell back to the newest entry across all wordlists,
|
|
||||||
making word hiding impossible. Verified against gforth and SwiftForth,
|
|
||||||
and guarded by a cross-engine corpus program.
|
|
||||||
- **Host words validate their stack arguments.** Around 40 host-implemented
|
|
||||||
words (`RND-SEED`, `ACCEPT`, `RESIZE`, `ALLOCATE`, `FREE`, `SEARCH`,
|
|
||||||
`SUBSTITUTE`, `ROLL`, `M*`, `UM/MOD`, `SF@ SF! DF@ DF!`, `F. FE. FS. F~`,
|
|
||||||
`2R@`, and friends) performed raw stack-pointer arithmetic with no
|
|
||||||
underflow check — calling them on an empty stack silently corrupted the
|
|
||||||
stack pointer (the compiled-code guards from 0.2.0 do not cover host
|
|
||||||
words). All argument-taking host words now fail with a clean, CATCHable
|
|
||||||
underflow error, enforced by a class-wide regression test.
|
|
||||||
|
|
||||||
## [0.2.0] - 2026-08-06
|
|
||||||
|
|
||||||
The usability release: introspection, source files, honest errors, and a
|
|
||||||
safety net under every compiled word.
|
|
||||||
|
|
||||||
### Added
|
|
||||||
|
|
||||||
- **Stack guards in compiled code**: under/overflow checks at the
|
|
||||||
stack-pointer choke points of generated WASM. Faults THROW standard codes
|
|
||||||
(`-3`..`-6`, `-44`, `-45`), are CATCHable, and print standard messages
|
|
||||||
instead of silently corrupting memory. Default on; `wafer build` output
|
|
||||||
stays unguarded; `WAFER_STACK_GUARDS=0|1` overrides.
|
|
||||||
- **`SEE`**: source-level decompiler. Colon words (including everything in
|
|
||||||
`boot.fth`) show their captured verbatim source; data words show
|
|
||||||
synthesized definitions with current values (`9 VALUE X`,
|
|
||||||
`DEFER D ( IS DUP )`); primitives fall back to a readable IR dump —
|
|
||||||
`SEE` never dead-ends on a defined word.
|
|
||||||
- **`SEE-IR`**: post-optimization IR view with resolved callee names and
|
|
||||||
indented control flow — shows what the optimizer actually did.
|
|
||||||
- **`HELP`**: stack effect + one-line description for **every** word in a
|
|
||||||
fresh VM (dictionary words and outer-interpreter tokens alike); coverage
|
|
||||||
is enforced by a unit test, so an undocumented new word fails the build.
|
|
||||||
User words echo their leading `( n -- n )` comment.
|
|
||||||
- **`INCLUDE` / `INCLUDED`**: nestable source-file loading with cycle
|
|
||||||
detection, depth bound, paths relative to the including file, and
|
|
||||||
per-level `SOURCE-ID`. The loader is injected (CLI: filesystem; web:
|
|
||||||
defined error), so the core stays IO-free. `wafer prog.fth` now runs
|
|
||||||
through the same machinery.
|
|
||||||
- **`MARKER` extensions**: `REMEMBER` (re-runnable marker), `EMPTY` and
|
|
||||||
`GILD` (boot-state rollback and re-baselining). Marker rollback now also
|
|
||||||
restores search order, wordlists, `REPLACES` substitutions, `ABORT"`
|
|
||||||
texts, and captured word sources — enabling the `REMEMBER` + `INCLUDE`
|
|
||||||
edit-reload loop.
|
|
||||||
- **`WORDS`**: optional substring filter (`WORDS FLOAT`), word count, and
|
|
||||||
`WORDS ALL` — a grouped full view by wordlist plus internal words.
|
|
||||||
- **Return-stack introspection**: `.RS`, `RDEPTH`, `RP@`.
|
|
||||||
- **Tools**: `.S` honors `BASE`, `F.S`, `?`, bounds-checked `DUMP`, real
|
|
||||||
`BYE`, named `ORDER` output.
|
|
||||||
- **CLI REPL**: persistent history (XDG state dir, `0600`), dictionary-backed
|
|
||||||
tab completion, prefix history search on Up/Down, Ctrl-C clears the line.
|
|
||||||
- **Web REPL**: history persisted to localStorage, User Words palette,
|
|
||||||
`BASE` indicator in the stack bar.
|
|
||||||
- **Error reporting**: uncaught `THROW` codes map to standard messages;
|
|
||||||
`ABORT"` text prints only when uncaught; errors inside included files
|
|
||||||
carry `file.fth:line:` context; uncaught throws are typed
|
|
||||||
(`WaferError::UncaughtThrow`) for embedding consumers; compiled words
|
|
||||||
carry WASM name sections, so genuine traps name the faulting word
|
|
||||||
(`in CRASHER: wasm trap: out of bounds memory access`).
|
|
||||||
- **SwiftForth correctness lane**: the cross-engine program corpus can run
|
|
||||||
against sf64 as an oracle (`just compare-correctness`), alongside the
|
|
||||||
existing gforth lane and the sf64 performance lane.
|
|
||||||
|
|
||||||
### Fixed
|
|
||||||
|
|
||||||
- Multi-line command output in the CLI REPL starts on its own line
|
|
||||||
(inline `ok` echo only for single-line output).
|
|
||||||
- `.S` printed in decimal regardless of `BASE`.
|
|
||||||
- A bare interpreted `R>` underflowed silently (exposed by the new stack
|
|
||||||
guards; compliance baseline updated).
|
|
||||||
- `SPACES` with a negative count now outputs nothing, per Forth 2012
|
|
||||||
6.1.2230.
|
|
||||||
|
|
||||||
### Changed
|
|
||||||
|
|
||||||
- `wafer prog.fth` reports errors with `file:line` context and resolves
|
|
||||||
nested `INCLUDE`s relative to the file.
|
|
||||||
- Internal words (`_`-prefixed) are flagged in the dictionary and hidden
|
|
||||||
from `WORDS` and completion (`WORDS ALL` shows them).
|
|
||||||
- Dependencies upgraded across the board: wasmtime 43 → 47,
|
|
||||||
wasm-encoder/wasmparser 0.246 → 0.255, plus all semver-compatible
|
|
||||||
updates.
|
|
||||||
|
|
||||||
## [0.1.0] - 2026-08-04
|
|
||||||
|
|
||||||
Initial development line (untagged): Forth 2012 core with IR optimizer and
|
|
||||||
WASM codegen via wasm-encoder/wasmtime, ~300 words across Core, Double,
|
|
||||||
Float, String, Search-Order, Exception, and Tools word sets, Forth 2012
|
|
||||||
compliance suite, `CONSOLIDATE` whole-program recompilation, `wafer build`
|
|
||||||
AOT export (WASM / native / JS loader), browser REPL, SHA-1/256/512 words,
|
|
||||||
and cross-engine benchmark lanes against gforth and SwiftForth.
|
|
||||||
|
|
||||||
[0.2.1]: https://github.com/ok2/wafer/compare/v0.2.0...v0.2.1
|
|
||||||
[0.2.0]: https://github.com/ok2/wafer/compare/v0.1.0...v0.2.0
|
|
||||||
[0.1.0]: https://github.com/ok2/wafer/releases/tag/v0.1.0
|
|
||||||
@@ -79,7 +79,7 @@ Handle in `interpret_token_immediate()` or `compile_token()` as a special case.
|
|||||||
|
|
||||||
## Testing
|
## Testing
|
||||||
|
|
||||||
- Run `cargo test --workspace` before committing (currently 562 unit + 1 benchmark + 11 compliance + 9 comparison + 5 crypto)
|
- Run `cargo test --workspace` before committing (currently 542 unit + 1 benchmark + 11 compliance + 9 comparison + 5 crypto)
|
||||||
- Forth 2012 compliance: `cargo test -p wafer-core --test compliance`
|
- Forth 2012 compliance: `cargo test -p wafer-core --test compliance`
|
||||||
- Cross-engine comparison (vs gforth): `cargo test -p wafer-core --test comparison`
|
- Cross-engine comparison (vs gforth): `cargo test -p wafer-core --test comparison`
|
||||||
- Performance benchmarks (release mode): `cargo test -p wafer-core --test comparison -- --nocapture --ignored`
|
- Performance benchmarks (release mode): `cargo test -p wafer-core --test comparison -- --nocapture --ignored`
|
||||||
|
|||||||
Generated
+634
-311
File diff suppressed because it is too large
Load Diff
+6
-6
@@ -3,7 +3,7 @@ members = ["crates/*"]
|
|||||||
resolver = "2"
|
resolver = "2"
|
||||||
|
|
||||||
[workspace.package]
|
[workspace.package]
|
||||||
version = "0.2.2"
|
version = "0.1.0"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
license = "MIT OR Apache-2.0"
|
license = "MIT OR Apache-2.0"
|
||||||
repository = "https://github.com/ok2/wafer"
|
repository = "https://github.com/ok2/wafer"
|
||||||
@@ -41,13 +41,13 @@ needless_collect = "warn"
|
|||||||
or_fun_call = "warn"
|
or_fun_call = "warn"
|
||||||
|
|
||||||
[workspace.dependencies]
|
[workspace.dependencies]
|
||||||
wasm-encoder = "0.255"
|
wasm-encoder = "0.246"
|
||||||
wasmparser = "0.255"
|
wasmparser = "0.246"
|
||||||
wasmtime = "47"
|
wasmtime = "43"
|
||||||
anyhow = "1"
|
anyhow = "1"
|
||||||
thiserror = "2"
|
thiserror = "2"
|
||||||
proptest = "1"
|
proptest = "1"
|
||||||
insta = "1"
|
insta = "1"
|
||||||
sha1 = "0.10"
|
sha1 = "0.11"
|
||||||
sha2 = "0.10"
|
sha2 = "0.11"
|
||||||
send_wrapper = "0.6"
|
send_wrapper = "0.6"
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ license.workspace = true
|
|||||||
workspace = true
|
workspace = true
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
wafer-core = { path = "../core", version = "0.2.1" }
|
wafer-core = { path = "../core", version = "0.1.0" }
|
||||||
wasmtime = { workspace = true }
|
wasmtime = { workspace = true }
|
||||||
anyhow = { workspace = true }
|
anyhow = { workspace = true }
|
||||||
clap = { version = "4", features = ["derive"] }
|
clap = { version = "4", features = ["derive"] }
|
||||||
|
|||||||
@@ -192,9 +192,10 @@ impl Dictionary {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// In no wordlist of the search order: not findable
|
// Fallback: return newest entry across all wordlists
|
||||||
// (Forth 2012 §16.3.3 — the order is authoritative).
|
if let Some(&(_wid, word_addr, fn_index, is_immediate)) = entries.last() {
|
||||||
return None;
|
return Some((word_addr, WordId(fn_index), is_immediate));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fallback: linked-list walk (for words not yet in the index)
|
// Fallback: linked-list walk (for words not yet in the index)
|
||||||
|
|||||||
@@ -108,23 +108,6 @@ pub const SYSVAR_HLD: u32 = SYSVAR_BASE + 28;
|
|||||||
pub const SYSVAR_LEAVE_FLAG: u32 = SYSVAR_BASE + 32;
|
pub const SYSVAR_LEAVE_FLAG: u32 = SYSVAR_BASE + 32;
|
||||||
/// Throw code left by a compiled stack-guard fault for `_STACK_FAULT_`.
|
/// Throw code left by a compiled stack-guard fault for `_STACK_FAULT_`.
|
||||||
pub const SYSVAR_FAULT_CODE: u32 = SYSVAR_BASE + 36;
|
pub const SYSVAR_FAULT_CODE: u32 = SYSVAR_BASE + 36;
|
||||||
/// DPL: digits right of the rightmost punctuation in the last converted
|
|
||||||
/// number; negative when the token carried no punctuation.
|
|
||||||
pub const SYSVAR_DPL: u32 = SYSVAR_BASE + 40;
|
|
||||||
/// NH: high-order cell of the last single-cell conversion, so an
|
|
||||||
/// out-of-range token can be recovered as a double.
|
|
||||||
pub const SYSVAR_NH: u32 = SYSVAR_BASE + 44;
|
|
||||||
|
|
||||||
/// Seed for [`SYSVAR_DPL`] before conversion starts.
|
|
||||||
///
|
|
||||||
/// `SwiftForth` seeds DPL with a negative value and bumps it once per digit,
|
|
||||||
/// so an unpunctuated token still ends up negative. Punctuation resets the
|
|
||||||
/// counter to zero, which makes the final value the digit count right of the
|
|
||||||
/// rightmost punctuation character.
|
|
||||||
///
|
|
||||||
/// The exact seed is observable: `sf64` reports DPL as -1020 after `1234`
|
|
||||||
/// and -1023 after `-1`, both of which pin it to -1024.
|
|
||||||
pub const DPL_INIT: i32 = -1024;
|
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
@@ -166,9 +149,6 @@ mod tests {
|
|||||||
SYSVAR_NUM_TIB,
|
SYSVAR_NUM_TIB,
|
||||||
SYSVAR_HLD,
|
SYSVAR_HLD,
|
||||||
SYSVAR_LEAVE_FLAG,
|
SYSVAR_LEAVE_FLAG,
|
||||||
SYSVAR_FAULT_CODE,
|
|
||||||
SYSVAR_DPL,
|
|
||||||
SYSVAR_NH,
|
|
||||||
];
|
];
|
||||||
for offset in all_offsets {
|
for offset in all_offsets {
|
||||||
assert!(offset + CELL_SIZE <= SYSVAR_BASE + SYSVAR_SIZE);
|
assert!(offset + CELL_SIZE <= SYSVAR_BASE + SYSVAR_SIZE);
|
||||||
|
|||||||
+178
-467
@@ -23,64 +23,12 @@ use crate::ir::IrOp;
|
|||||||
#[cfg(feature = "crypto")]
|
#[cfg(feature = "crypto")]
|
||||||
use crate::memory::HASH_SCRATCH_BASE;
|
use crate::memory::HASH_SCRATCH_BASE;
|
||||||
use crate::memory::{
|
use crate::memory::{
|
||||||
CELL_SIZE, DATA_STACK_TOP, DPL_INIT, FLOAT_SIZE, FLOAT_STACK_BASE, FLOAT_STACK_TOP,
|
CELL_SIZE, DATA_STACK_TOP, FLOAT_SIZE, FLOAT_STACK_BASE, FLOAT_STACK_TOP, INPUT_BUFFER_BASE,
|
||||||
INPUT_BUFFER_BASE, INPUT_BUFFER_SIZE, RETURN_STACK_TOP, SYSVAR_BASE_VAR, SYSVAR_DPL,
|
INPUT_BUFFER_SIZE, RETURN_STACK_TOP, SYSVAR_BASE_VAR, SYSVAR_FAULT_CODE, SYSVAR_HERE,
|
||||||
SYSVAR_FAULT_CODE, SYSVAR_HERE, SYSVAR_LEAVE_FLAG, SYSVAR_NH, SYSVAR_NUM_TIB, SYSVAR_STATE,
|
SYSVAR_LEAVE_FLAG, SYSVAR_NUM_TIB, SYSVAR_STATE, SYSVAR_TO_IN,
|
||||||
SYSVAR_TO_IN,
|
|
||||||
};
|
};
|
||||||
use crate::optimizer::optimize;
|
use crate::optimizer::optimize;
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// Number conversion
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
/// Characters that force double-cell conversion, following `SwiftForth`'s
|
|
||||||
/// input number conversion rules.
|
|
||||||
///
|
|
||||||
/// A leading `-` is the one exception: it binds as a sign, which keeps `-1`
|
|
||||||
/// a single-cell number while `1-2` converts as a double.
|
|
||||||
const DOUBLE_PUNCTUATION: [u8; 6] = *b",.+-/:";
|
|
||||||
|
|
||||||
/// Split a leading minus off a token, returning whether it was negative.
|
|
||||||
///
|
|
||||||
/// Only `-` is a sign. A leading `+` stays punctuation, matching `sf64`,
|
|
||||||
/// where `+7` converts as the double 7 with `DPL` = 1.
|
|
||||||
///
|
|
||||||
/// The sign may sit between a base-override prefix and the digits (`$-FF`,
|
|
||||||
/// the Forth 2012 spelling) or, as a WAFER extension, before the prefix
|
|
||||||
/// (`-$FF`), so this runs at both positions.
|
|
||||||
fn strip_sign(s: &str) -> (bool, &str) {
|
|
||||||
match s.as_bytes().first() {
|
|
||||||
Some(b'-') => (true, &s[1..]),
|
|
||||||
_ => (false, s),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A numeric token that converted successfully.
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
||||||
struct NumberLiteral {
|
|
||||||
/// The accumulated 64-bit value, sign applied.
|
|
||||||
value: i64,
|
|
||||||
/// Digits right of the rightmost punctuation character. Negative when the
|
|
||||||
/// token carried no punctuation, which is how `DPL` reports "single-cell".
|
|
||||||
dpl: i32,
|
|
||||||
/// Whether punctuation forced double-cell conversion.
|
|
||||||
is_double: bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl NumberLiteral {
|
|
||||||
/// Low-order cell — the value a single-cell conversion leaves on the stack.
|
|
||||||
fn lo(self) -> i32 {
|
|
||||||
self.value as i32
|
|
||||||
}
|
|
||||||
|
|
||||||
/// High-order cell. For a single-cell conversion this is what `NH` holds,
|
|
||||||
/// letting an out-of-range token be recovered as a double.
|
|
||||||
fn hi(self) -> i32 {
|
|
||||||
(self.value >> 32) as i32
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// Control-flow compilation state
|
// Control-flow compilation state
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
@@ -573,40 +521,6 @@ fn host_pop(ctx: &mut dyn HostAccess) -> anyhow::Result<i32> {
|
|||||||
Ok(v)
|
Ok(v)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Ensure the data stack holds at least `n` cells; returns the stack
|
|
||||||
/// pointer for the caller's reads. Host words must check before raw
|
|
||||||
/// pointer arithmetic — compiled-code guards do not cover them.
|
|
||||||
fn host_need(ctx: &mut dyn HostAccess, n: u32) -> anyhow::Result<u32> {
|
|
||||||
let sp = ctx.get_dsp();
|
|
||||||
match n.checked_mul(CELL_SIZE).and_then(|b| sp.checked_add(b)) {
|
|
||||||
Some(end) if end <= DATA_STACK_TOP => Ok(sp),
|
|
||||||
_ => anyhow::bail!("Stack underflow"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Ensure the float stack holds at least `n` floats; returns the pointer.
|
|
||||||
fn host_fneed(ctx: &mut dyn HostAccess, n: u32) -> anyhow::Result<u32> {
|
|
||||||
let sp = ctx.get_fsp();
|
|
||||||
match n.checked_mul(FLOAT_SIZE).and_then(|b| sp.checked_add(b)) {
|
|
||||||
Some(end) if end <= FLOAT_STACK_TOP => Ok(sp),
|
|
||||||
_ => anyhow::bail!("Float stack underflow"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Checked float-stack pop for host words.
|
|
||||||
fn host_fpop(ctx: &mut dyn HostAccess) -> anyhow::Result<f64> {
|
|
||||||
let sp = ctx.get_fsp();
|
|
||||||
if sp >= FLOAT_STACK_TOP {
|
|
||||||
anyhow::bail!("Float stack underflow");
|
|
||||||
}
|
|
||||||
let bytes: [u8; 8] = ctx
|
|
||||||
.mem_read_slice(sp, 8)
|
|
||||||
.try_into()
|
|
||||||
.map_err(|_| anyhow::anyhow!("float stack read failed"))?;
|
|
||||||
ctx.set_fsp(sp + FLOAT_SIZE);
|
|
||||||
Ok(f64::from_le_bytes(bytes))
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Advance past the next `\n` in `buf`, starting at `from`. Returns the
|
/// Advance past the next `\n` in `buf`, starting at `from`. Returns the
|
||||||
/// byte index of the first character on the next line (or `buf.len()` if
|
/// byte index of the first character on the next line (or `buf.len()` if
|
||||||
/// there's no more newline). Used by the `\` line-comment handler per
|
/// there's no more newline). Used by the `\` line-comment handler per
|
||||||
@@ -1246,18 +1160,22 @@ impl<R: Runtime> ForthVM<R> {
|
|||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
|
||||||
// Try to convert as a number; punctuation makes it double-cell
|
// Try to parse as double-number (trailing dot)
|
||||||
if let Some(lit) = self.parse_numeric_literal(token) {
|
if let Some((lo, hi)) = self.parse_double_number(token) {
|
||||||
self.record_number_conversion(lit);
|
self.push_data_stack(lo)?;
|
||||||
self.push_data_stack(lit.lo())?;
|
self.push_data_stack(hi)?;
|
||||||
if self.recording_toplevel && self.state == 0 {
|
if self.recording_toplevel && self.state == 0 {
|
||||||
self.toplevel_ir.push(IrOp::PushI32(lit.lo()));
|
self.toplevel_ir.push(IrOp::PushI32(lo));
|
||||||
|
self.toplevel_ir.push(IrOp::PushI32(hi));
|
||||||
}
|
}
|
||||||
if lit.is_double {
|
return Ok(());
|
||||||
self.push_data_stack(lit.hi())?;
|
}
|
||||||
if self.recording_toplevel && self.state == 0 {
|
|
||||||
self.toplevel_ir.push(IrOp::PushI32(lit.hi()));
|
// Try to parse as number
|
||||||
}
|
if let Some(n) = self.parse_number(token) {
|
||||||
|
self.push_data_stack(n)?;
|
||||||
|
if self.recording_toplevel && self.state == 0 {
|
||||||
|
self.toplevel_ir.push(IrOp::PushI32(n));
|
||||||
}
|
}
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
@@ -1619,13 +1537,16 @@ impl<R: Runtime> ForthVM<R> {
|
|||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
|
||||||
// Try to convert as a number; punctuation makes it double-cell
|
// Try to parse as double-number (trailing dot)
|
||||||
if let Some(lit) = self.parse_numeric_literal(token) {
|
if let Some((lo, hi)) = self.parse_double_number(token) {
|
||||||
self.record_number_conversion(lit);
|
self.push_ir(IrOp::PushI32(lo));
|
||||||
self.push_ir(IrOp::PushI32(lit.lo()));
|
self.push_ir(IrOp::PushI32(hi));
|
||||||
if lit.is_double {
|
return Ok(());
|
||||||
self.push_ir(IrOp::PushI32(lit.hi()));
|
}
|
||||||
}
|
|
||||||
|
// Try to parse as number
|
||||||
|
if let Some(n) = self.parse_number(token) {
|
||||||
|
self.push_ir(IrOp::PushI32(n));
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2776,109 +2697,83 @@ impl<R: Runtime> ForthVM<R> {
|
|||||||
// Number parsing
|
// Number parsing
|
||||||
// -----------------------------------------------------------------------
|
// -----------------------------------------------------------------------
|
||||||
|
|
||||||
/// Try to convert a token to a number, following `SwiftForth`'s input
|
/// Try to parse a token as a number.
|
||||||
/// number conversion rules.
|
fn parse_number(&self, token: &str) -> Option<i32> {
|
||||||
///
|
|
||||||
/// Punctuation (`,` `.` `+` `-` `/` `:`) forces double-cell conversion, so
|
|
||||||
/// `12.34`, `1,234`, `12:30:45` and `2026-08-06` all convert as doubles.
|
|
||||||
/// Only a leading `-` escapes this and binds as a sign, which keeps `-1`
|
|
||||||
/// single-cell; a leading `+` stays punctuation, so `+7` is the double 7.
|
|
||||||
///
|
|
||||||
/// `DPL` counts up once per digit from [`DPL_INIT`] and resets to zero at
|
|
||||||
/// every punctuation character, so it ends up holding the digit count right
|
|
||||||
/// of the rightmost punctuation, and stays negative for unpunctuated tokens.
|
|
||||||
fn parse_numeric_literal(&self, token: &str) -> Option<NumberLiteral> {
|
|
||||||
let token = token.trim();
|
let token = token.trim();
|
||||||
if token.is_empty() {
|
if token.is_empty() {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
|
|
||||||
// A leading sign binds to the number; it is not double punctuation.
|
// Check for negative prefix
|
||||||
let (neg_outer, rest) = strip_sign(token);
|
let (negative, rest) = if let Some(stripped) = token.strip_prefix('-') {
|
||||||
|
(true, stripped)
|
||||||
|
} else {
|
||||||
|
(false, token)
|
||||||
|
};
|
||||||
|
|
||||||
if rest.is_empty() {
|
if rest.is_empty() {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Character literal: 'x' → ASCII value of x. No digits, so DPL stays
|
// Parse based on prefix
|
||||||
// at its seed and the result is always single-cell.
|
let result = if let Some(hex) = rest.strip_prefix('$') {
|
||||||
if rest.len() == 3 && rest.as_bytes()[0] == b'\'' && rest.as_bytes()[2] == b'\'' {
|
i64::from_str_radix(hex, 16).ok()
|
||||||
let value = i64::from(rest.as_bytes()[1]);
|
} else if let Some(dec) = rest.strip_prefix('#') {
|
||||||
return Some(NumberLiteral {
|
dec.parse::<i64>().ok()
|
||||||
value: if neg_outer { -value } else { value },
|
} else if let Some(bin) = rest.strip_prefix('%') {
|
||||||
dpl: DPL_INIT,
|
i64::from_str_radix(bin, 2).ok()
|
||||||
is_double: false,
|
} else if rest.len() == 3 && rest.as_bytes()[0] == b'\'' && rest.as_bytes()[2] == b'\'' {
|
||||||
});
|
// Character literal: 'x' → ASCII value of x
|
||||||
}
|
Some(rest.as_bytes()[1] as i64)
|
||||||
|
|
||||||
// A base-override prefix sits before the leftmost digit.
|
|
||||||
let (radix, after_prefix) = match rest.as_bytes()[0] {
|
|
||||||
b'$' => (16, &rest[1..]),
|
|
||||||
b'#' => (10, &rest[1..]),
|
|
||||||
b'%' => (2, &rest[1..]),
|
|
||||||
_ => (self.base, rest),
|
|
||||||
};
|
|
||||||
|
|
||||||
// Forth 2012 spells a signed based number `#-1289`, so the sign can
|
|
||||||
// also follow the prefix. Either way it precedes the leftmost digit
|
|
||||||
// and so is a sign rather than double punctuation.
|
|
||||||
let (neg_inner, digits) = strip_sign(after_prefix);
|
|
||||||
let negative = neg_outer ^ neg_inner;
|
|
||||||
|
|
||||||
if digits.is_empty() {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Walk the digit string, stripping punctuation and tracking DPL.
|
|
||||||
let mut buf = String::with_capacity(digits.len());
|
|
||||||
let mut dpl = DPL_INIT;
|
|
||||||
let mut is_double = false;
|
|
||||||
for &b in digits.as_bytes() {
|
|
||||||
if DOUBLE_PUNCTUATION.contains(&b) {
|
|
||||||
is_double = true;
|
|
||||||
dpl = 0;
|
|
||||||
} else {
|
|
||||||
buf.push(char::from(b));
|
|
||||||
dpl += 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if buf.is_empty() {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
|
|
||||||
// i128 accumulation so the full u64 range survives conversion.
|
|
||||||
let magnitude = i128::from_str_radix(&buf, radix).ok()?;
|
|
||||||
let value = if negative {
|
|
||||||
-(magnitude as i64)
|
|
||||||
} else {
|
} else {
|
||||||
magnitude as i64
|
i64::from_str_radix(rest, self.base).ok()
|
||||||
};
|
};
|
||||||
|
|
||||||
Some(NumberLiteral {
|
result.map(|n| if negative { -(n as i32) } else { n as i32 })
|
||||||
value,
|
|
||||||
dpl,
|
|
||||||
is_double,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Publish the outcome of a conversion in `DPL` and `NH`.
|
/// Try to parse a token as a double-number (token ends with `.`).
|
||||||
///
|
/// Returns (lo, hi) where the double-cell value is (hi << 32) | lo.
|
||||||
/// `NH` only carries meaning after a single-cell conversion, where it holds
|
fn parse_double_number(&self, token: &str) -> Option<(i32, i32)> {
|
||||||
/// the high-order cell that the stack result dropped.
|
let token = token.trim();
|
||||||
fn record_number_conversion(&mut self, lit: NumberLiteral) {
|
if token.is_empty() {
|
||||||
self.rt.mem_write_i32(SYSVAR_DPL, lit.dpl);
|
return None;
|
||||||
if !lit.is_double {
|
|
||||||
self.rt.mem_write_i32(SYSVAR_NH, lit.hi());
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
/// Try to parse a token as a single-cell number, ignoring `DPL`/`NH`.
|
// Check for trailing dot (double-number indicator)
|
||||||
/// Used where only a plain cell value is meaningful.
|
let without_dot = token.strip_suffix('.')?;
|
||||||
fn parse_number(&self, token: &str) -> Option<i32> {
|
if without_dot.is_empty() {
|
||||||
self.parse_numeric_literal(token)
|
return None;
|
||||||
.filter(|lit| !lit.is_double)
|
}
|
||||||
.map(NumberLiteral::lo)
|
|
||||||
|
// Check for negative prefix
|
||||||
|
let (negative, rest) = if let Some(stripped) = without_dot.strip_prefix('-') {
|
||||||
|
(true, stripped)
|
||||||
|
} else {
|
||||||
|
(false, without_dot)
|
||||||
|
};
|
||||||
|
|
||||||
|
if rest.is_empty() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse based on prefix -- use i128 to handle the full u64 range
|
||||||
|
let result: Option<i128> = if let Some(hex) = rest.strip_prefix('$') {
|
||||||
|
i128::from_str_radix(hex, 16).ok()
|
||||||
|
} else if let Some(dec) = rest.strip_prefix('#') {
|
||||||
|
dec.parse::<i128>().ok()
|
||||||
|
} else if let Some(bin) = rest.strip_prefix('%') {
|
||||||
|
i128::from_str_radix(bin, 2).ok()
|
||||||
|
} else {
|
||||||
|
i128::from_str_radix(rest, self.base).ok()
|
||||||
|
};
|
||||||
|
|
||||||
|
result.map(|n| {
|
||||||
|
let val: i64 = if negative { -(n as i64) } else { n as i64 };
|
||||||
|
let lo = val as i32;
|
||||||
|
let hi = (val >> 32) as i32;
|
||||||
|
(lo, hi)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// -----------------------------------------------------------------------
|
// -----------------------------------------------------------------------
|
||||||
@@ -3163,7 +3058,6 @@ impl<R: Runtime> ForthVM<R> {
|
|||||||
self.register_to_in()?;
|
self.register_to_in()?;
|
||||||
self.register_state_var()?;
|
self.register_state_var()?;
|
||||||
self.register_base_var()?;
|
self.register_base_var()?;
|
||||||
self.register_number_conversion_vars()?;
|
|
||||||
|
|
||||||
// Double-cell arithmetic
|
// Double-cell arithmetic
|
||||||
self.register_m_star()?;
|
self.register_m_star()?;
|
||||||
@@ -3431,7 +3325,7 @@ impl<R: Runtime> ForthVM<R> {
|
|||||||
let digest_len = algo.digest_len as i32;
|
let digest_len = algo.digest_len as i32;
|
||||||
let func: HostFn = Box::new(move |ctx: &mut dyn HostAccess| {
|
let func: HostFn = Box::new(move |ctx: &mut dyn HostAccess| {
|
||||||
// Pop ( c-addr u )
|
// Pop ( c-addr u )
|
||||||
let dsp = host_need(ctx, 2)?;
|
let dsp = ctx.get_dsp();
|
||||||
let u = ctx.mem_read_i32(dsp) as u32;
|
let u = ctx.mem_read_i32(dsp) as u32;
|
||||||
let c_addr = ctx.mem_read_i32(dsp + CELL_SIZE) as u32;
|
let c_addr = ctx.mem_read_i32(dsp + CELL_SIZE) as u32;
|
||||||
|
|
||||||
@@ -4183,9 +4077,8 @@ impl<R: Runtime> ForthVM<R> {
|
|||||||
fn register_roll(&mut self) -> anyhow::Result<()> {
|
fn register_roll(&mut self) -> anyhow::Result<()> {
|
||||||
let func: HostFn = Box::new(move |ctx: &mut dyn HostAccess| {
|
let func: HostFn = Box::new(move |ctx: &mut dyn HostAccess| {
|
||||||
// Pop u from stack
|
// Pop u from stack
|
||||||
let sp = host_need(ctx, 1)?;
|
let sp = ctx.get_dsp();
|
||||||
let u = ctx.mem_read_i32(sp as u32) as u32;
|
let u = ctx.mem_read_i32(sp as u32) as u32;
|
||||||
host_need(ctx, u.saturating_add(2))?;
|
|
||||||
let sp = sp + CELL_SIZE; // pop u
|
let sp = sp + CELL_SIZE; // pop u
|
||||||
|
|
||||||
if u == 0 {
|
if u == 0 {
|
||||||
@@ -4370,7 +4263,7 @@ impl<R: Runtime> ForthVM<R> {
|
|||||||
|
|
||||||
let func: HostFn = Box::new(move |ctx: &mut dyn HostAccess| {
|
let func: HostFn = Box::new(move |ctx: &mut dyn HostAccess| {
|
||||||
// Pop xt from data stack
|
// Pop xt from data stack
|
||||||
let sp = host_need(ctx, 1)?;
|
let sp = ctx.get_dsp();
|
||||||
let xt = ctx.mem_read_i32(sp as u32) as u32;
|
let xt = ctx.mem_read_i32(sp as u32) as u32;
|
||||||
|
|
||||||
// Look up PFA for this xt
|
// Look up PFA for this xt
|
||||||
@@ -4390,7 +4283,7 @@ impl<R: Runtime> ForthVM<R> {
|
|||||||
/// ENVIRONMENT? -- ( c-addr u -- false | value true ) query system parameters.
|
/// ENVIRONMENT? -- ( c-addr u -- false | value true ) query system parameters.
|
||||||
fn register_environment_q(&mut self) -> anyhow::Result<()> {
|
fn register_environment_q(&mut self) -> anyhow::Result<()> {
|
||||||
let func: HostFn = Box::new(move |ctx: &mut dyn HostAccess| {
|
let func: HostFn = Box::new(move |ctx: &mut dyn HostAccess| {
|
||||||
let sp = host_need(ctx, 2)?;
|
let sp = ctx.get_dsp();
|
||||||
let u = ctx.mem_read_i32(sp as u32) as u32;
|
let u = ctx.mem_read_i32(sp as u32) as u32;
|
||||||
let b: [u8; 4] = ctx.mem_read_i32((sp + 4) as u32).to_le_bytes();
|
let b: [u8; 4] = ctx.mem_read_i32((sp + 4) as u32).to_le_bytes();
|
||||||
let addr = u32::from_le_bytes(b);
|
let addr = u32::from_le_bytes(b);
|
||||||
@@ -5223,26 +5116,10 @@ impl<R: Runtime> ForthVM<R> {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// DPL ( -- addr ) and NH ( -- addr ): input number conversion results.
|
|
||||||
///
|
|
||||||
/// `DPL` holds the digit count right of the rightmost punctuation
|
|
||||||
/// character in the last converted number, or a negative value when the
|
|
||||||
/// token carried none. `NH` holds the high-order cell dropped by a
|
|
||||||
/// single-cell conversion, so an out-of-range token can be recovered as a
|
|
||||||
/// double.
|
|
||||||
fn register_number_conversion_vars(&mut self) -> anyhow::Result<()> {
|
|
||||||
self.rt.mem_write_i32(SYSVAR_DPL, DPL_INIT);
|
|
||||||
self.rt.mem_write_i32(SYSVAR_NH, 0);
|
|
||||||
|
|
||||||
self.register_primitive("DPL", false, vec![IrOp::PushI32(SYSVAR_DPL as i32)])?;
|
|
||||||
self.register_primitive("NH", false, vec![IrOp::PushI32(SYSVAR_NH as i32)])?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// M* ( n1 n2 -- d ) signed multiply producing double-cell result.
|
/// M* ( n1 n2 -- d ) signed multiply producing double-cell result.
|
||||||
fn register_m_star(&mut self) -> anyhow::Result<()> {
|
fn register_m_star(&mut self) -> anyhow::Result<()> {
|
||||||
let func: HostFn = Box::new(move |ctx: &mut dyn HostAccess| {
|
let func: HostFn = Box::new(move |ctx: &mut dyn HostAccess| {
|
||||||
let sp = host_need(ctx, 2)?;
|
let sp = ctx.get_dsp();
|
||||||
let n2 = ctx.mem_read_i32(sp as u32) as i64;
|
let n2 = ctx.mem_read_i32(sp as u32) as i64;
|
||||||
let b: [u8; 4] = ctx.mem_read_i32((sp + 4) as u32).to_le_bytes();
|
let b: [u8; 4] = ctx.mem_read_i32((sp + 4) as u32).to_le_bytes();
|
||||||
let n1 = i32::from_le_bytes(b) as i64;
|
let n1 = i32::from_le_bytes(b) as i64;
|
||||||
@@ -5263,7 +5140,7 @@ impl<R: Runtime> ForthVM<R> {
|
|||||||
/// UM* ( u1 u2 -- ud ) unsigned multiply producing double-cell result.
|
/// UM* ( u1 u2 -- ud ) unsigned multiply producing double-cell result.
|
||||||
fn register_um_star(&mut self) -> anyhow::Result<()> {
|
fn register_um_star(&mut self) -> anyhow::Result<()> {
|
||||||
let func: HostFn = Box::new(move |ctx: &mut dyn HostAccess| {
|
let func: HostFn = Box::new(move |ctx: &mut dyn HostAccess| {
|
||||||
let sp = host_need(ctx, 2)?;
|
let sp = ctx.get_dsp();
|
||||||
let u2 = ctx.mem_read_i32(sp as u32) as u32 as u64;
|
let u2 = ctx.mem_read_i32(sp as u32) as u32 as u64;
|
||||||
let b: [u8; 4] = ctx.mem_read_i32((sp + 4) as u32).to_le_bytes();
|
let b: [u8; 4] = ctx.mem_read_i32((sp + 4) as u32).to_le_bytes();
|
||||||
let u1 = u32::from_le_bytes(b) as u64;
|
let u1 = u32::from_le_bytes(b) as u64;
|
||||||
@@ -5282,7 +5159,7 @@ impl<R: Runtime> ForthVM<R> {
|
|||||||
/// UM/MOD ( ud u -- rem quot ) unsigned double-cell divide.
|
/// UM/MOD ( ud u -- rem quot ) unsigned double-cell divide.
|
||||||
fn register_um_div_mod(&mut self) -> anyhow::Result<()> {
|
fn register_um_div_mod(&mut self) -> anyhow::Result<()> {
|
||||||
let func: HostFn = Box::new(move |ctx: &mut dyn HostAccess| {
|
let func: HostFn = Box::new(move |ctx: &mut dyn HostAccess| {
|
||||||
let sp = host_need(ctx, 3)?;
|
let sp = ctx.get_dsp();
|
||||||
// Pop u (divisor)
|
// Pop u (divisor)
|
||||||
let divisor = ctx.mem_read_i32(sp as u32) as u32 as u64;
|
let divisor = ctx.mem_read_i32(sp as u32) as u32 as u64;
|
||||||
// Pop ud (double-cell): high at sp+4, low at sp+8
|
// Pop ud (double-cell): high at sp+4, low at sp+8
|
||||||
@@ -5370,7 +5247,7 @@ impl<R: Runtime> ForthVM<R> {
|
|||||||
|
|
||||||
let func: HostFn = Box::new(move |ctx: &mut dyn HostAccess| {
|
let func: HostFn = Box::new(move |ctx: &mut dyn HostAccess| {
|
||||||
// Pop xt from data stack
|
// Pop xt from data stack
|
||||||
let sp = host_need(ctx, 1)?;
|
let sp = ctx.get_dsp();
|
||||||
let xt = ctx.mem_read_i32(sp as u32) as u32;
|
let xt = ctx.mem_read_i32(sp as u32) as u32;
|
||||||
// Drop top of stack
|
// Drop top of stack
|
||||||
let new_sp = sp + 4;
|
let new_sp = sp + 4;
|
||||||
@@ -5455,7 +5332,7 @@ impl<R: Runtime> ForthVM<R> {
|
|||||||
|
|
||||||
let func: HostFn = Box::new(move |ctx: &mut dyn HostAccess| {
|
let func: HostFn = Box::new(move |ctx: &mut dyn HostAccess| {
|
||||||
// ( c-addr u -- ) — pop both cells.
|
// ( c-addr u -- ) — pop both cells.
|
||||||
let sp = host_need(ctx, 2)?;
|
let sp = ctx.get_dsp();
|
||||||
let u = ctx.mem_read_i32(sp) as u32;
|
let u = ctx.mem_read_i32(sp) as u32;
|
||||||
let addr = ctx.mem_read_i32(sp + CELL_SIZE) as u32;
|
let addr = ctx.mem_read_i32(sp + CELL_SIZE) as u32;
|
||||||
ctx.set_dsp(sp + 2 * CELL_SIZE);
|
ctx.set_dsp(sp + 2 * CELL_SIZE);
|
||||||
@@ -5576,7 +5453,10 @@ impl<R: Runtime> ForthVM<R> {
|
|||||||
/// WORD ( char -- c-addr ) reads from the WASM input buffer and updates >IN.
|
/// WORD ( char -- c-addr ) reads from the WASM input buffer and updates >IN.
|
||||||
fn register_word_word(&mut self) -> anyhow::Result<()> {
|
fn register_word_word(&mut self) -> anyhow::Result<()> {
|
||||||
let func: HostFn = Box::new(move |ctx: &mut dyn HostAccess| {
|
let func: HostFn = Box::new(move |ctx: &mut dyn HostAccess| {
|
||||||
let delim = host_pop(ctx)? as u8;
|
// Pop delimiter from data stack
|
||||||
|
let sp = ctx.get_dsp();
|
||||||
|
let delim = ctx.mem_read_i32(sp as u32) as u8;
|
||||||
|
ctx.set_dsp(((sp + CELL_SIZE) as i32) as u32);
|
||||||
|
|
||||||
// Read >IN and #TIB from WASM memory
|
// Read >IN and #TIB from WASM memory
|
||||||
let b: [u8; 4] = ctx.mem_read_i32(SYSVAR_TO_IN as u32).to_le_bytes();
|
let b: [u8; 4] = ctx.mem_read_i32(SYSVAR_TO_IN as u32).to_le_bytes();
|
||||||
@@ -5622,8 +5502,8 @@ impl<R: Runtime> ForthVM<R> {
|
|||||||
ctx.mem_write_u8((dst_start + i) as u32, byte);
|
ctx.mem_write_u8((dst_start + i) as u32, byte);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Push c-addr onto data stack (reuse the popped delim's slot)
|
// Push c-addr onto data stack
|
||||||
let new_sp = ctx.get_dsp() - CELL_SIZE;
|
let new_sp = sp; // We already popped delim, now push c-addr
|
||||||
ctx.mem_write_i32(new_sp, buf_addr as i32);
|
ctx.mem_write_i32(new_sp, buf_addr as i32);
|
||||||
ctx.set_dsp(new_sp);
|
ctx.set_dsp(new_sp);
|
||||||
|
|
||||||
@@ -5976,9 +5856,6 @@ impl<R: Runtime> ForthVM<R> {
|
|||||||
fn register_2r_fetch(&mut self) -> anyhow::Result<()> {
|
fn register_2r_fetch(&mut self) -> anyhow::Result<()> {
|
||||||
let func: HostFn = Box::new(move |ctx: &mut dyn HostAccess| {
|
let func: HostFn = Box::new(move |ctx: &mut dyn HostAccess| {
|
||||||
let rsp_val = ctx.get_rsp();
|
let rsp_val = ctx.get_rsp();
|
||||||
if rsp_val + 2 * CELL_SIZE > RETURN_STACK_TOP {
|
|
||||||
anyhow::bail!("Return stack underflow");
|
|
||||||
}
|
|
||||||
let sp = ctx.get_dsp();
|
let sp = ctx.get_dsp();
|
||||||
// Return stack: x2 at rsp, x1 at rsp+4
|
// Return stack: x2 at rsp, x1 at rsp+4
|
||||||
let b: [u8; 4] = ctx.mem_read_i32(rsp_val as u32).to_le_bytes();
|
let b: [u8; 4] = ctx.mem_read_i32(rsp_val as u32).to_le_bytes();
|
||||||
@@ -6086,7 +5963,9 @@ impl<R: Runtime> ForthVM<R> {
|
|||||||
|
|
||||||
let state = Arc::clone(&self.rng_state);
|
let state = Arc::clone(&self.rng_state);
|
||||||
let func: HostFn = Box::new(move |ctx: &mut dyn HostAccess| {
|
let func: HostFn = Box::new(move |ctx: &mut dyn HostAccess| {
|
||||||
let seed = host_pop(ctx)? as u32 as u64;
|
let sp = ctx.get_dsp();
|
||||||
|
let seed = ctx.mem_read_i32(sp as u32) as u32 as u64;
|
||||||
|
ctx.set_dsp(sp + CELL_SIZE);
|
||||||
let mut s = state.lock().unwrap();
|
let mut s = state.lock().unwrap();
|
||||||
*s = if seed == 0 {
|
*s = if seed == 0 {
|
||||||
0xDEAD_BEEF_CAFE_BABE
|
0xDEAD_BEEF_CAFE_BABE
|
||||||
@@ -6103,7 +5982,7 @@ impl<R: Runtime> ForthVM<R> {
|
|||||||
fn register_parse_host(&mut self) -> anyhow::Result<()> {
|
fn register_parse_host(&mut self) -> anyhow::Result<()> {
|
||||||
let func: HostFn = Box::new(move |ctx: &mut dyn HostAccess| {
|
let func: HostFn = Box::new(move |ctx: &mut dyn HostAccess| {
|
||||||
// Pop delimiter from data stack
|
// Pop delimiter from data stack
|
||||||
let sp = host_need(ctx, 1)?;
|
let sp = ctx.get_dsp();
|
||||||
let delim = ctx.mem_read_i32(sp as u32) as u8;
|
let delim = ctx.mem_read_i32(sp as u32) as u8;
|
||||||
let sp = sp + CELL_SIZE; // pop delimiter
|
let sp = sp + CELL_SIZE; // pop delimiter
|
||||||
|
|
||||||
@@ -6221,7 +6100,7 @@ impl<R: Runtime> ForthVM<R> {
|
|||||||
// In non-interactive mode, return 0 (no input).
|
// In non-interactive mode, return 0 (no input).
|
||||||
let func: HostFn = Box::new(move |ctx: &mut dyn HostAccess| {
|
let func: HostFn = Box::new(move |ctx: &mut dyn HostAccess| {
|
||||||
// Pop +n1 (max count) and c-addr from stack
|
// Pop +n1 (max count) and c-addr from stack
|
||||||
let sp = host_need(ctx, 2)?;
|
let sp = ctx.get_dsp();
|
||||||
let new_sp = sp + CELL_SIZE; // pop +n1
|
let new_sp = sp + CELL_SIZE; // pop +n1
|
||||||
let new_sp = new_sp + CELL_SIZE; // pop c-addr
|
let new_sp = new_sp + CELL_SIZE; // pop c-addr
|
||||||
// Push 0 (no characters received)
|
// Push 0 (no characters received)
|
||||||
@@ -6246,7 +6125,7 @@ impl<R: Runtime> ForthVM<R> {
|
|||||||
fn register_memory_alloc(&mut self) -> anyhow::Result<()> {
|
fn register_memory_alloc(&mut self) -> anyhow::Result<()> {
|
||||||
// ALLOCATE ( u -- a-addr ior )
|
// ALLOCATE ( u -- a-addr ior )
|
||||||
let func: HostFn = Box::new(move |ctx: &mut dyn HostAccess| {
|
let func: HostFn = Box::new(move |ctx: &mut dyn HostAccess| {
|
||||||
let sp = host_need(ctx, 1)?;
|
let sp = ctx.get_dsp();
|
||||||
let size = ctx.mem_read_i32(sp as u32) as u32;
|
let size = ctx.mem_read_i32(sp as u32) as u32;
|
||||||
|
|
||||||
let mem_len = ctx.mem_len() as u32;
|
let mem_len = ctx.mem_len() as u32;
|
||||||
@@ -6305,7 +6184,7 @@ impl<R: Runtime> ForthVM<R> {
|
|||||||
// FREE ( a-addr -- ior )
|
// FREE ( a-addr -- ior )
|
||||||
let func: HostFn = Box::new(move |ctx: &mut dyn HostAccess| {
|
let func: HostFn = Box::new(move |ctx: &mut dyn HostAccess| {
|
||||||
// Simple allocator: FREE is a no-op (arena style), return ior=0
|
// Simple allocator: FREE is a no-op (arena style), return ior=0
|
||||||
let sp = host_need(ctx, 1)?;
|
let sp = ctx.get_dsp();
|
||||||
// Replace a-addr with ior=0
|
// Replace a-addr with ior=0
|
||||||
ctx.mem_write_i32(sp as u32, 0i32 as i32);
|
ctx.mem_write_i32(sp as u32, 0i32 as i32);
|
||||||
Ok(())
|
Ok(())
|
||||||
@@ -6314,7 +6193,7 @@ impl<R: Runtime> ForthVM<R> {
|
|||||||
|
|
||||||
// RESIZE ( a-addr u -- a-addr2 ior )
|
// RESIZE ( a-addr u -- a-addr2 ior )
|
||||||
let func: HostFn = Box::new(move |ctx: &mut dyn HostAccess| {
|
let func: HostFn = Box::new(move |ctx: &mut dyn HostAccess| {
|
||||||
let sp = host_need(ctx, 2)?;
|
let sp = ctx.get_dsp();
|
||||||
let new_size = ctx.mem_read_i32(sp as u32) as u32;
|
let new_size = ctx.mem_read_i32(sp as u32) as u32;
|
||||||
let b: [u8; 4] = ctx.mem_read_i32((sp + 4) as u32).to_le_bytes();
|
let b: [u8; 4] = ctx.mem_read_i32((sp + 4) as u32).to_le_bytes();
|
||||||
let old_addr = u32::from_le_bytes(b);
|
let old_addr = u32::from_le_bytes(b);
|
||||||
@@ -6735,14 +6614,8 @@ impl<R: Runtime> ForthVM<R> {
|
|||||||
{
|
{
|
||||||
let so = Arc::clone(&self.search_order);
|
let so = Arc::clone(&self.search_order);
|
||||||
let func: HostFn = Box::new(move |ctx: &mut dyn HostAccess| {
|
let func: HostFn = Box::new(move |ctx: &mut dyn HostAccess| {
|
||||||
let sp = host_need(ctx, 1)?;
|
let sp = ctx.get_dsp();
|
||||||
let n = ctx.mem_read_i32(sp as u32);
|
let n = ctx.mem_read_i32(sp as u32);
|
||||||
if !(-1..=64).contains(&n) {
|
|
||||||
anyhow::bail!("SET-ORDER: bad wordlist count: {n}");
|
|
||||||
}
|
|
||||||
if n != -1 {
|
|
||||||
host_need(ctx, 1 + n as u32)?;
|
|
||||||
}
|
|
||||||
|
|
||||||
if n == -1 {
|
if n == -1 {
|
||||||
*so.lock().unwrap() = vec![1];
|
*so.lock().unwrap() = vec![1];
|
||||||
@@ -6845,9 +6718,8 @@ impl<R: Runtime> ForthVM<R> {
|
|||||||
fn register_n_to_r(&mut self) -> anyhow::Result<()> {
|
fn register_n_to_r(&mut self) -> anyhow::Result<()> {
|
||||||
// N>R ( xn..x1 n -- ; R: -- x1..xn n )
|
// N>R ( xn..x1 n -- ; R: -- x1..xn n )
|
||||||
let func: HostFn = Box::new(move |ctx: &mut dyn HostAccess| {
|
let func: HostFn = Box::new(move |ctx: &mut dyn HostAccess| {
|
||||||
let sp = host_need(ctx, 1)?;
|
let sp = ctx.get_dsp();
|
||||||
let n = ctx.mem_read_i32(sp as u32) as u32;
|
let n = ctx.mem_read_i32(sp as u32) as u32;
|
||||||
host_need(ctx, n.saturating_add(1))?;
|
|
||||||
|
|
||||||
let mut rsp_val = ctx.get_rsp();
|
let mut rsp_val = ctx.get_rsp();
|
||||||
|
|
||||||
@@ -6931,7 +6803,7 @@ impl<R: Runtime> ForthVM<R> {
|
|||||||
// UNESCAPE ( c-addr1 u1 c-addr2 -- c-addr2 u2 )
|
// UNESCAPE ( c-addr1 u1 c-addr2 -- c-addr2 u2 )
|
||||||
// Copy string escaping each % as %%
|
// Copy string escaping each % as %%
|
||||||
let func: HostFn = Box::new(move |ctx: &mut dyn HostAccess| {
|
let func: HostFn = Box::new(move |ctx: &mut dyn HostAccess| {
|
||||||
let sp = host_need(ctx, 3)?;
|
let sp = ctx.get_dsp();
|
||||||
let dest = ctx.mem_read_i32(sp as u32) as u32;
|
let dest = ctx.mem_read_i32(sp as u32) as u32;
|
||||||
let b: [u8; 4] = ctx.mem_read_i32((sp + 4) as u32).to_le_bytes();
|
let b: [u8; 4] = ctx.mem_read_i32((sp + 4) as u32).to_le_bytes();
|
||||||
let u1 = u32::from_le_bytes(b);
|
let u1 = u32::from_le_bytes(b);
|
||||||
@@ -6969,7 +6841,7 @@ impl<R: Runtime> ForthVM<R> {
|
|||||||
// Define substitution: name (c-addr2 u2) → replacement (c-addr1 u1)
|
// Define substitution: name (c-addr2 u2) → replacement (c-addr1 u1)
|
||||||
let subs = Arc::clone(&self.substitutions);
|
let subs = Arc::clone(&self.substitutions);
|
||||||
let func: HostFn = Box::new(move |ctx: &mut dyn HostAccess| {
|
let func: HostFn = Box::new(move |ctx: &mut dyn HostAccess| {
|
||||||
let sp = host_need(ctx, 4)?;
|
let sp = ctx.get_dsp();
|
||||||
// Stack: u2(sp), c-addr2(sp+4), u1(sp+8), c-addr1(sp+12)
|
// Stack: u2(sp), c-addr2(sp+4), u1(sp+8), c-addr1(sp+12)
|
||||||
let u2 = ctx.mem_read_i32(sp as u32) as u32;
|
let u2 = ctx.mem_read_i32(sp as u32) as u32;
|
||||||
let b: [u8; 4] = ctx.mem_read_i32((sp + 4) as u32).to_le_bytes();
|
let b: [u8; 4] = ctx.mem_read_i32((sp + 4) as u32).to_le_bytes();
|
||||||
@@ -6997,7 +6869,7 @@ impl<R: Runtime> ForthVM<R> {
|
|||||||
// Replace %name% patterns, %% → %
|
// Replace %name% patterns, %% → %
|
||||||
let subs = Arc::clone(&self.substitutions);
|
let subs = Arc::clone(&self.substitutions);
|
||||||
let func: HostFn = Box::new(move |ctx: &mut dyn HostAccess| {
|
let func: HostFn = Box::new(move |ctx: &mut dyn HostAccess| {
|
||||||
let sp = host_need(ctx, 4)?;
|
let sp = ctx.get_dsp();
|
||||||
// Stack: u2/capacity(sp), c-addr2/dest(sp+4), u1(sp+8), c-addr1(sp+12)
|
// Stack: u2/capacity(sp), c-addr2/dest(sp+4), u1(sp+8), c-addr1(sp+12)
|
||||||
let capacity = ctx.mem_read_i32(sp as u32) as u32 as usize;
|
let capacity = ctx.mem_read_i32(sp as u32) as u32 as usize;
|
||||||
let b: [u8; 4] = ctx.mem_read_i32((sp + 4) as u32).to_le_bytes();
|
let b: [u8; 4] = ctx.mem_read_i32((sp + 4) as u32).to_le_bytes();
|
||||||
@@ -7085,7 +6957,7 @@ impl<R: Runtime> ForthVM<R> {
|
|||||||
/// M*/ ( d n1 n2 -- d ) multiply d by n1, divide by n2.
|
/// M*/ ( d n1 n2 -- d ) multiply d by n1, divide by n2.
|
||||||
fn register_m_star_slash(&mut self) -> anyhow::Result<()> {
|
fn register_m_star_slash(&mut self) -> anyhow::Result<()> {
|
||||||
let func: HostFn = Box::new(move |ctx: &mut dyn HostAccess| {
|
let func: HostFn = Box::new(move |ctx: &mut dyn HostAccess| {
|
||||||
let sp = host_need(ctx, 4)?;
|
let sp = ctx.get_dsp();
|
||||||
// Stack: n2(sp), n1(sp+4), d-hi(sp+8), d-lo(sp+12)
|
// Stack: n2(sp), n1(sp+4), d-hi(sp+8), d-lo(sp+12)
|
||||||
let n2 = ctx.mem_read_i32(sp as u32) as i128;
|
let n2 = ctx.mem_read_i32(sp as u32) as i128;
|
||||||
let b: [u8; 4] = ctx.mem_read_i32((sp + 4) as u32).to_le_bytes();
|
let b: [u8; 4] = ctx.mem_read_i32((sp + 4) as u32).to_le_bytes();
|
||||||
@@ -7221,7 +7093,7 @@ impl<R: Runtime> ForthVM<R> {
|
|||||||
/// SEARCH ( c-addr1 u1 c-addr2 u2 -- c-addr3 u3 flag ) search for substring.
|
/// SEARCH ( c-addr1 u1 c-addr2 u2 -- c-addr3 u3 flag ) search for substring.
|
||||||
fn register_search(&mut self) -> anyhow::Result<()> {
|
fn register_search(&mut self) -> anyhow::Result<()> {
|
||||||
let func: HostFn = Box::new(move |ctx: &mut dyn HostAccess| {
|
let func: HostFn = Box::new(move |ctx: &mut dyn HostAccess| {
|
||||||
let sp = host_need(ctx, 4)?;
|
let sp = ctx.get_dsp();
|
||||||
// Stack: u2(sp), c-addr2(sp+4), u1(sp+8), c-addr1(sp+12)
|
// Stack: u2(sp), c-addr2(sp+4), u1(sp+8), c-addr1(sp+12)
|
||||||
let u2 = ctx.mem_read_i32(sp as u32) as usize;
|
let u2 = ctx.mem_read_i32(sp as u32) as usize;
|
||||||
let b: [u8; 4] = ctx.mem_read_i32((sp + 4) as u32).to_le_bytes();
|
let b: [u8; 4] = ctx.mem_read_i32((sp + 4) as u32).to_le_bytes();
|
||||||
@@ -7372,7 +7244,7 @@ impl<R: Runtime> ForthVM<R> {
|
|||||||
// FROT ( F: r1 r2 r3 -- r2 r3 r1 )
|
// FROT ( F: r1 r2 r3 -- r2 r3 r1 )
|
||||||
{
|
{
|
||||||
let func: HostFn = Box::new(move |ctx: &mut dyn HostAccess| {
|
let func: HostFn = Box::new(move |ctx: &mut dyn HostAccess| {
|
||||||
let sp = host_fneed(ctx, 3)?;
|
let sp = ctx.get_fsp();
|
||||||
let c: [u8; 8] = ctx.mem_read_slice(sp, 8).try_into().unwrap();
|
let c: [u8; 8] = ctx.mem_read_slice(sp, 8).try_into().unwrap();
|
||||||
let b: [u8; 8] = ctx.mem_read_slice(sp + 8, 8).try_into().unwrap();
|
let b: [u8; 8] = ctx.mem_read_slice(sp + 8, 8).try_into().unwrap();
|
||||||
let a: [u8; 8] = ctx.mem_read_slice(sp + 16, 8).try_into().unwrap();
|
let a: [u8; 8] = ctx.mem_read_slice(sp + 16, 8).try_into().unwrap();
|
||||||
@@ -7439,9 +7311,14 @@ impl<R: Runtime> ForthVM<R> {
|
|||||||
// If r3 < 0: true if |r1-r2| < |r3|*(|r1|+|r2|)
|
// If r3 < 0: true if |r1-r2| < |r3|*(|r1|+|r2|)
|
||||||
{
|
{
|
||||||
let func: HostFn = Box::new(move |ctx: &mut dyn HostAccess| {
|
let func: HostFn = Box::new(move |ctx: &mut dyn HostAccess| {
|
||||||
let r3 = host_fpop(ctx)?;
|
let sp = ctx.get_fsp();
|
||||||
let r2 = host_fpop(ctx)?;
|
let r3_bytes: [u8; 8] = ctx.mem_read_slice(sp, 8).try_into().unwrap();
|
||||||
let r1 = host_fpop(ctx)?;
|
let r2_bytes: [u8; 8] = ctx.mem_read_slice(sp + 8, 8).try_into().unwrap();
|
||||||
|
let r1_bytes: [u8; 8] = ctx.mem_read_slice(sp + 16, 8).try_into().unwrap();
|
||||||
|
let r3 = f64::from_le_bytes(r3_bytes);
|
||||||
|
let r2 = f64::from_le_bytes(r2_bytes);
|
||||||
|
let r1 = f64::from_le_bytes(r1_bytes);
|
||||||
|
ctx.set_fsp(((sp + 24) as i32) as u32);
|
||||||
|
|
||||||
let result = if r3 > 0.0 {
|
let result = if r3 > 0.0 {
|
||||||
(r1 - r2).abs() < r3
|
(r1 - r2).abs() < r3
|
||||||
@@ -7489,7 +7366,7 @@ impl<R: Runtime> ForthVM<R> {
|
|||||||
// FALIGNED ( addr -- f-addr ) align to float boundary (8 bytes)
|
// FALIGNED ( addr -- f-addr ) align to float boundary (8 bytes)
|
||||||
{
|
{
|
||||||
let func: HostFn = Box::new(move |ctx: &mut dyn HostAccess| {
|
let func: HostFn = Box::new(move |ctx: &mut dyn HostAccess| {
|
||||||
let sp = host_need(ctx, 1)?;
|
let sp = ctx.get_dsp();
|
||||||
let addr = ctx.mem_read_i32(sp as u32) as u32;
|
let addr = ctx.mem_read_i32(sp as u32) as u32;
|
||||||
let aligned = (addr + 7) & !7;
|
let aligned = (addr + 7) & !7;
|
||||||
ctx.mem_write_i32(sp as u32, aligned as i32);
|
ctx.mem_write_i32(sp as u32, aligned as i32);
|
||||||
@@ -7534,7 +7411,7 @@ impl<R: Runtime> ForthVM<R> {
|
|||||||
// D>F ( d -- ) ( F: -- r ) convert double-cell integer to float
|
// D>F ( d -- ) ( F: -- r ) convert double-cell integer to float
|
||||||
{
|
{
|
||||||
let func: HostFn = Box::new(move |ctx: &mut dyn HostAccess| {
|
let func: HostFn = Box::new(move |ctx: &mut dyn HostAccess| {
|
||||||
let sp = host_need(ctx, 2)?;
|
let sp = ctx.get_dsp();
|
||||||
// Double-cell: hi on top, lo below
|
// Double-cell: hi on top, lo below
|
||||||
let hi_bytes: [u8; 4] = ctx.mem_read_slice(sp, 4).try_into().unwrap();
|
let hi_bytes: [u8; 4] = ctx.mem_read_slice(sp, 4).try_into().unwrap();
|
||||||
let lo_bytes: [u8; 4] = ctx.mem_read_slice(sp + 4, 4).try_into().unwrap();
|
let lo_bytes: [u8; 4] = ctx.mem_read_slice(sp + 4, 4).try_into().unwrap();
|
||||||
@@ -7557,7 +7434,11 @@ impl<R: Runtime> ForthVM<R> {
|
|||||||
// F>D ( -- d ) ( F: r -- ) convert float to double-cell integer
|
// F>D ( -- d ) ( F: r -- ) convert float to double-cell integer
|
||||||
{
|
{
|
||||||
let func: HostFn = Box::new(move |ctx: &mut dyn HostAccess| {
|
let func: HostFn = Box::new(move |ctx: &mut dyn HostAccess| {
|
||||||
let f = host_fpop(ctx)?;
|
// Pop from float stack
|
||||||
|
let fsp_val = ctx.get_fsp();
|
||||||
|
let bytes: [u8; 8] = ctx.mem_read_slice(fsp_val, 8).try_into().unwrap();
|
||||||
|
let f = f64::from_le_bytes(bytes);
|
||||||
|
ctx.set_fsp(fsp_val + FLOAT_SIZE);
|
||||||
// Convert to i64
|
// Convert to i64
|
||||||
let d = f as i64;
|
let d = f as i64;
|
||||||
let lo = d as i32;
|
let lo = d as i32;
|
||||||
@@ -7643,7 +7524,10 @@ impl<R: Runtime> ForthVM<R> {
|
|||||||
let output = Arc::clone(&self.output);
|
let output = Arc::clone(&self.output);
|
||||||
let precision = Arc::clone(&self.float_precision);
|
let precision = Arc::clone(&self.float_precision);
|
||||||
let func: HostFn = Box::new(move |ctx: &mut dyn HostAccess| {
|
let func: HostFn = Box::new(move |ctx: &mut dyn HostAccess| {
|
||||||
let val = host_fpop(ctx)?;
|
let sp = ctx.get_fsp();
|
||||||
|
let bytes: [u8; 8] = ctx.mem_read_slice(sp as u32, 8).try_into().unwrap();
|
||||||
|
let val = f64::from_le_bytes(bytes);
|
||||||
|
ctx.set_fsp(((sp + 8) as i32) as u32);
|
||||||
let prec = *precision.lock().unwrap();
|
let prec = *precision.lock().unwrap();
|
||||||
let s = format!("{val:.prec$} ");
|
let s = format!("{val:.prec$} ");
|
||||||
output.lock().unwrap().push_str(&s);
|
output.lock().unwrap().push_str(&s);
|
||||||
@@ -7657,7 +7541,10 @@ impl<R: Runtime> ForthVM<R> {
|
|||||||
let output = Arc::clone(&self.output);
|
let output = Arc::clone(&self.output);
|
||||||
let precision = Arc::clone(&self.float_precision);
|
let precision = Arc::clone(&self.float_precision);
|
||||||
let func: HostFn = Box::new(move |ctx: &mut dyn HostAccess| {
|
let func: HostFn = Box::new(move |ctx: &mut dyn HostAccess| {
|
||||||
let val = host_fpop(ctx)?;
|
let sp = ctx.get_fsp();
|
||||||
|
let bytes: [u8; 8] = ctx.mem_read_slice(sp as u32, 8).try_into().unwrap();
|
||||||
|
let val = f64::from_le_bytes(bytes);
|
||||||
|
ctx.set_fsp(((sp + 8) as i32) as u32);
|
||||||
let prec = *precision.lock().unwrap();
|
let prec = *precision.lock().unwrap();
|
||||||
let s = format_engineering(val, prec);
|
let s = format_engineering(val, prec);
|
||||||
output.lock().unwrap().push_str(&s);
|
output.lock().unwrap().push_str(&s);
|
||||||
@@ -7671,7 +7558,10 @@ impl<R: Runtime> ForthVM<R> {
|
|||||||
let output = Arc::clone(&self.output);
|
let output = Arc::clone(&self.output);
|
||||||
let precision = Arc::clone(&self.float_precision);
|
let precision = Arc::clone(&self.float_precision);
|
||||||
let func: HostFn = Box::new(move |ctx: &mut dyn HostAccess| {
|
let func: HostFn = Box::new(move |ctx: &mut dyn HostAccess| {
|
||||||
let val = host_fpop(ctx)?;
|
let sp = ctx.get_fsp();
|
||||||
|
let bytes: [u8; 8] = ctx.mem_read_slice(sp as u32, 8).try_into().unwrap();
|
||||||
|
let val = f64::from_le_bytes(bytes);
|
||||||
|
ctx.set_fsp(((sp + 8) as i32) as u32);
|
||||||
let prec = *precision.lock().unwrap();
|
let prec = *precision.lock().unwrap();
|
||||||
let s = format!("{val:.prec$E} ");
|
let s = format!("{val:.prec$E} ");
|
||||||
output.lock().unwrap().push_str(&s);
|
output.lock().unwrap().push_str(&s);
|
||||||
@@ -7698,7 +7588,9 @@ impl<R: Runtime> ForthVM<R> {
|
|||||||
{
|
{
|
||||||
let precision = Arc::clone(&self.float_precision);
|
let precision = Arc::clone(&self.float_precision);
|
||||||
let func: HostFn = Box::new(move |ctx: &mut dyn HostAccess| {
|
let func: HostFn = Box::new(move |ctx: &mut dyn HostAccess| {
|
||||||
let n = host_pop(ctx)? as usize;
|
let sp = ctx.get_dsp();
|
||||||
|
let n = ctx.mem_read_i32(sp as u32) as usize;
|
||||||
|
ctx.set_dsp(((sp + CELL_SIZE) as i32) as u32);
|
||||||
*precision.lock().unwrap() = n;
|
*precision.lock().unwrap() = n;
|
||||||
Ok(())
|
Ok(())
|
||||||
});
|
});
|
||||||
@@ -7708,12 +7600,17 @@ impl<R: Runtime> ForthVM<R> {
|
|||||||
// REPRESENT ( c-addr u -- n flag1 flag2 ) ( F: r -- )
|
// REPRESENT ( c-addr u -- n flag1 flag2 ) ( F: r -- )
|
||||||
{
|
{
|
||||||
let func: HostFn = Box::new(move |ctx: &mut dyn HostAccess| {
|
let func: HostFn = Box::new(move |ctx: &mut dyn HostAccess| {
|
||||||
let sp = host_need(ctx, 2)?;
|
// Read all values from memory first
|
||||||
|
let sp = ctx.get_dsp();
|
||||||
|
let fsp_val = ctx.get_fsp();
|
||||||
let u = ctx.mem_read_i32(sp) as usize;
|
let u = ctx.mem_read_i32(sp) as usize;
|
||||||
let c_addr = ctx.mem_read_i32(sp + 4) as u32;
|
let c_addr = ctx.mem_read_i32(sp + 4) as u32;
|
||||||
let val = host_fpop(ctx)?;
|
let f_bytes: [u8; 8] = ctx.mem_read_slice(fsp_val, 8).try_into().unwrap();
|
||||||
// Pop the 2 data cells
|
let val = f64::from_le_bytes(f_bytes);
|
||||||
|
|
||||||
|
// Update stack pointers: pop 2 data cells, pop 1 float
|
||||||
ctx.set_dsp(sp + 8);
|
ctx.set_dsp(sp + 8);
|
||||||
|
ctx.set_fsp(fsp_val + FLOAT_SIZE);
|
||||||
|
|
||||||
let (digits, exp, is_negative, is_valid) = represent_float(val, u);
|
let (digits, exp, is_negative, is_valid) = represent_float(val, u);
|
||||||
|
|
||||||
@@ -7741,7 +7638,7 @@ impl<R: Runtime> ForthVM<R> {
|
|||||||
// >FLOAT ( c-addr u -- flag ) ( F: -- r | ) parse string as float
|
// >FLOAT ( c-addr u -- flag ) ( F: -- r | ) parse string as float
|
||||||
{
|
{
|
||||||
let func: HostFn = Box::new(move |ctx: &mut dyn HostAccess| {
|
let func: HostFn = Box::new(move |ctx: &mut dyn HostAccess| {
|
||||||
let sp = host_need(ctx, 2)?;
|
let sp = ctx.get_dsp();
|
||||||
let u = ctx.mem_read_i32(sp) as usize;
|
let u = ctx.mem_read_i32(sp) as usize;
|
||||||
let c_addr = ctx.mem_read_i32(sp + 4) as u32;
|
let c_addr = ctx.mem_read_i32(sp + 4) as u32;
|
||||||
let s_bytes = ctx.mem_read_slice(c_addr, u);
|
let s_bytes = ctx.mem_read_slice(c_addr, u);
|
||||||
@@ -7782,9 +7679,14 @@ impl<R: Runtime> ForthVM<R> {
|
|||||||
// SF! ( sf-addr -- ) ( F: r -- ) store as single-precision float (f32)
|
// SF! ( sf-addr -- ) ( F: r -- ) store as single-precision float (f32)
|
||||||
{
|
{
|
||||||
let func: HostFn = Box::new(move |ctx: &mut dyn HostAccess| {
|
let func: HostFn = Box::new(move |ctx: &mut dyn HostAccess| {
|
||||||
let addr = host_pop(ctx)? as u32;
|
let sp = ctx.get_dsp();
|
||||||
let val = host_fpop(ctx)?;
|
let fsp_val = ctx.get_fsp();
|
||||||
|
let addr = ctx.mem_read_i32(sp) as u32;
|
||||||
|
let f_bytes: [u8; 8] = ctx.mem_read_slice(fsp_val, 8).try_into().unwrap();
|
||||||
|
let val = f64::from_le_bytes(f_bytes);
|
||||||
let f32_bytes = (val as f32).to_le_bytes();
|
let f32_bytes = (val as f32).to_le_bytes();
|
||||||
|
ctx.set_dsp(sp + CELL_SIZE);
|
||||||
|
ctx.set_fsp(fsp_val + FLOAT_SIZE);
|
||||||
ctx.mem_write_slice(addr, &f32_bytes);
|
ctx.mem_write_slice(addr, &f32_bytes);
|
||||||
Ok(())
|
Ok(())
|
||||||
});
|
});
|
||||||
@@ -7794,10 +7696,12 @@ impl<R: Runtime> ForthVM<R> {
|
|||||||
// SF@ ( sf-addr -- ) ( F: -- r ) fetch single-precision float (f32)
|
// SF@ ( sf-addr -- ) ( F: -- r ) fetch single-precision float (f32)
|
||||||
{
|
{
|
||||||
let func: HostFn = Box::new(move |ctx: &mut dyn HostAccess| {
|
let func: HostFn = Box::new(move |ctx: &mut dyn HostAccess| {
|
||||||
let addr = host_pop(ctx)? as u32;
|
let sp = ctx.get_dsp();
|
||||||
let fsp_val = ctx.get_fsp();
|
let fsp_val = ctx.get_fsp();
|
||||||
|
let addr = ctx.mem_read_i32(sp) as u32;
|
||||||
let f32_bytes: [u8; 4] = ctx.mem_read_slice(addr, 4).try_into().unwrap();
|
let f32_bytes: [u8; 4] = ctx.mem_read_slice(addr, 4).try_into().unwrap();
|
||||||
let val = f32::from_le_bytes(f32_bytes) as f64;
|
let val = f32::from_le_bytes(f32_bytes) as f64;
|
||||||
|
ctx.set_dsp(sp + CELL_SIZE);
|
||||||
let new_fsp = fsp_val - FLOAT_SIZE;
|
let new_fsp = fsp_val - FLOAT_SIZE;
|
||||||
ctx.set_fsp(new_fsp);
|
ctx.set_fsp(new_fsp);
|
||||||
ctx.mem_write_slice(new_fsp, &val.to_le_bytes());
|
ctx.mem_write_slice(new_fsp, &val.to_le_bytes());
|
||||||
@@ -7809,8 +7713,12 @@ impl<R: Runtime> ForthVM<R> {
|
|||||||
// DF! ( df-addr -- ) ( F: r -- ) same as F! (our floats are already f64)
|
// DF! ( df-addr -- ) ( F: r -- ) same as F! (our floats are already f64)
|
||||||
{
|
{
|
||||||
let func: HostFn = Box::new(move |ctx: &mut dyn HostAccess| {
|
let func: HostFn = Box::new(move |ctx: &mut dyn HostAccess| {
|
||||||
let addr = host_pop(ctx)? as u32;
|
let sp = ctx.get_dsp();
|
||||||
let float_bytes = host_fpop(ctx)?.to_le_bytes();
|
let fsp_val = ctx.get_fsp();
|
||||||
|
let addr = ctx.mem_read_i32(sp) as u32;
|
||||||
|
let float_bytes: [u8; 8] = ctx.mem_read_slice(fsp_val, 8).try_into().unwrap();
|
||||||
|
ctx.set_dsp(sp + CELL_SIZE);
|
||||||
|
ctx.set_fsp(fsp_val + FLOAT_SIZE);
|
||||||
ctx.mem_write_slice(addr, &float_bytes);
|
ctx.mem_write_slice(addr, &float_bytes);
|
||||||
Ok(())
|
Ok(())
|
||||||
});
|
});
|
||||||
@@ -7820,10 +7728,12 @@ impl<R: Runtime> ForthVM<R> {
|
|||||||
// DF@ ( df-addr -- ) ( F: -- r ) same as F@ (our floats are already f64)
|
// DF@ ( df-addr -- ) ( F: -- r ) same as F@ (our floats are already f64)
|
||||||
{
|
{
|
||||||
let func: HostFn = Box::new(move |ctx: &mut dyn HostAccess| {
|
let func: HostFn = Box::new(move |ctx: &mut dyn HostAccess| {
|
||||||
let addr = host_pop(ctx)? as u32;
|
let sp = ctx.get_dsp();
|
||||||
let fsp_val = ctx.get_fsp();
|
let fsp_val = ctx.get_fsp();
|
||||||
|
let addr = ctx.mem_read_i32(sp) as u32;
|
||||||
let float_bytes: [u8; 8] = ctx.mem_read_slice(addr, 8).try_into().unwrap();
|
let float_bytes: [u8; 8] = ctx.mem_read_slice(addr, 8).try_into().unwrap();
|
||||||
let val = f64::from_le_bytes(float_bytes);
|
let val = f64::from_le_bytes(float_bytes);
|
||||||
|
ctx.set_dsp(sp + CELL_SIZE);
|
||||||
let new_fsp = fsp_val - FLOAT_SIZE;
|
let new_fsp = fsp_val - FLOAT_SIZE;
|
||||||
ctx.set_fsp(new_fsp);
|
ctx.set_fsp(new_fsp);
|
||||||
ctx.mem_write_slice(new_fsp, &val.to_le_bytes());
|
ctx.mem_write_slice(new_fsp, &val.to_le_bytes());
|
||||||
@@ -7835,7 +7745,7 @@ impl<R: Runtime> ForthVM<R> {
|
|||||||
// SFALIGNED, DFALIGNED (alignment words for single/double floats)
|
// SFALIGNED, DFALIGNED (alignment words for single/double floats)
|
||||||
{
|
{
|
||||||
let func: HostFn = Box::new(move |ctx: &mut dyn HostAccess| {
|
let func: HostFn = Box::new(move |ctx: &mut dyn HostAccess| {
|
||||||
let sp = host_need(ctx, 1)?;
|
let sp = ctx.get_dsp();
|
||||||
let addr = ctx.mem_read_i32(sp as u32) as u32;
|
let addr = ctx.mem_read_i32(sp as u32) as u32;
|
||||||
let aligned = (addr + 3) & !3; // 4-byte alignment for single float
|
let aligned = (addr + 3) & !3; // 4-byte alignment for single float
|
||||||
ctx.mem_write_i32(sp as u32, aligned as i32);
|
ctx.mem_write_i32(sp as u32, aligned as i32);
|
||||||
@@ -7847,7 +7757,7 @@ impl<R: Runtime> ForthVM<R> {
|
|||||||
// DFALIGNED is the same as FALIGNED (8-byte alignment)
|
// DFALIGNED is the same as FALIGNED (8-byte alignment)
|
||||||
{
|
{
|
||||||
let func: HostFn = Box::new(move |ctx: &mut dyn HostAccess| {
|
let func: HostFn = Box::new(move |ctx: &mut dyn HostAccess| {
|
||||||
let sp = host_need(ctx, 1)?;
|
let sp = ctx.get_dsp();
|
||||||
let addr = ctx.mem_read_i32(sp as u32) as u32;
|
let addr = ctx.mem_read_i32(sp as u32) as u32;
|
||||||
let aligned = (addr + 7) & !7;
|
let aligned = (addr + 7) & !7;
|
||||||
ctx.mem_write_i32(sp as u32, aligned as i32);
|
ctx.mem_write_i32(sp as u32, aligned as i32);
|
||||||
@@ -9639,100 +9549,6 @@ mod tests {
|
|||||||
assert!(!output.contains("__CTRL__"));
|
assert!(!output.contains("__CTRL__"));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_rnd_seed_underflow_is_clean_error() {
|
|
||||||
let mut vm = ForthVM::<NativeRuntime>::new().unwrap();
|
|
||||||
let err = vm.evaluate("RND-SEED").unwrap_err();
|
|
||||||
assert!(err.to_string().contains("underflow"), "{err}");
|
|
||||||
// The stack pointer must not have drifted above the base.
|
|
||||||
vm.evaluate("RANDOM .S").unwrap();
|
|
||||||
assert!(vm.take_output().starts_with("<1>"), "dsp drifted");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_host_words_underflow_cleanly() {
|
|
||||||
// Every argument-taking host word must fail cleanly on an empty
|
|
||||||
// stack and leave both stack pointers at their bases (host words
|
|
||||||
// are outside the compiled-code guards).
|
|
||||||
let words = [
|
|
||||||
"RND-SEED",
|
|
||||||
"WORD",
|
|
||||||
"SET-ORDER",
|
|
||||||
"SET-PRECISION",
|
|
||||||
"REPRESENT",
|
|
||||||
">FLOAT",
|
|
||||||
"SF!",
|
|
||||||
"SF@",
|
|
||||||
"DF!",
|
|
||||||
"DF@",
|
|
||||||
"D>F",
|
|
||||||
"F.",
|
|
||||||
"FE.",
|
|
||||||
"FS.",
|
|
||||||
"F~",
|
|
||||||
"ROLL",
|
|
||||||
">BODY",
|
|
||||||
"ENVIRONMENT?",
|
|
||||||
"M*",
|
|
||||||
"UM*",
|
|
||||||
"UM/MOD",
|
|
||||||
"COMPILE,",
|
|
||||||
"ACCEPT",
|
|
||||||
"ALLOCATE",
|
|
||||||
"FREE",
|
|
||||||
"RESIZE",
|
|
||||||
"N>R",
|
|
||||||
"UNESCAPE",
|
|
||||||
"REPLACES",
|
|
||||||
"SUBSTITUTE",
|
|
||||||
"M*/",
|
|
||||||
"SEARCH",
|
|
||||||
"FALIGNED",
|
|
||||||
"SFALIGNED",
|
|
||||||
"DFALIGNED",
|
|
||||||
"FROT",
|
|
||||||
"F>D",
|
|
||||||
"2R@",
|
|
||||||
// WORD and PARSE are intercepted by the outer interpreter in
|
|
||||||
// interpret mode; exercise their host variants compiled.
|
|
||||||
": T_ WORD ; T_",
|
|
||||||
": T_ PARSE ; T_",
|
|
||||||
#[cfg(feature = "crypto")]
|
|
||||||
"SHA256",
|
|
||||||
];
|
|
||||||
for w in words {
|
|
||||||
let mut vm = ForthVM::<NativeRuntime>::new().unwrap();
|
|
||||||
let r = vm.evaluate(w);
|
|
||||||
assert!(r.is_err(), "{w}: silent underflow accepted");
|
|
||||||
vm.evaluate("DEPTH FDEPTH + .").unwrap();
|
|
||||||
assert_eq!(vm.take_output(), "0 ", "{w}: stack pointer drifted");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// -- Search order is authoritative (matches gforth + SwiftForth) --
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_search_order_hides_unlisted_wordlists() {
|
|
||||||
let mut vm = ForthVM::<NativeRuntime>::new().unwrap();
|
|
||||||
vm.evaluate(
|
|
||||||
"WORDLIST CONSTANT MY-WL MY-WL SET-CURRENT : SECRET 42 ; FORTH-WORDLIST SET-CURRENT",
|
|
||||||
)
|
|
||||||
.unwrap();
|
|
||||||
// MY-WL was never in the search order: SECRET must not resolve.
|
|
||||||
let err = vm.evaluate("SECRET").unwrap_err();
|
|
||||||
assert!(err.to_string().contains("unknown word"), "{err}");
|
|
||||||
// Push MY-WL onto the order: now it resolves.
|
|
||||||
vm.evaluate("GET-ORDER MY-WL SWAP 1+ SET-ORDER SECRET .")
|
|
||||||
.unwrap();
|
|
||||||
assert_eq!(vm.take_output(), "42 ");
|
|
||||||
// Back to the default order: hidden again.
|
|
||||||
vm.evaluate("-1 SET-ORDER").unwrap();
|
|
||||||
assert!(vm.evaluate("SECRET").is_err());
|
|
||||||
// FORTH words stay findable throughout.
|
|
||||||
vm.evaluate("1 2 + .").unwrap();
|
|
||||||
assert_eq!(vm.take_output(), "3 ");
|
|
||||||
}
|
|
||||||
|
|
||||||
// -- Error reporting (WS-008) --
|
// -- Error reporting (WS-008) --
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -10916,111 +10732,6 @@ mod tests {
|
|||||||
assert_eq!(eval_stack("1E 2.5E 1E F~"), vec![0]); // |1-2.5| = 1.5 >= 1
|
assert_eq!(eval_stack("1E 2.5E 1E F~"), vec![0]); // |1-2.5| = 1.5 >= 1
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn punctuation_anywhere_converts_as_double() {
|
|
||||||
// The punctuation is a double-cell marker, not a fractional point:
|
|
||||||
// every form below carries the same digits, so the value is the same.
|
|
||||||
// eval_stack reports top-first, so a double reads as [hi, lo].
|
|
||||||
for token in ["1234.", "123.4", "12.34", "1.234", ".1234"] {
|
|
||||||
assert_eq!(eval_stack(token), vec![0, 1234], "token {token}");
|
|
||||||
}
|
|
||||||
// SwiftForth accepts comma, colon, slash, plus and dash too, which is
|
|
||||||
// what makes dates and times convert without a custom parser.
|
|
||||||
assert_eq!(eval_stack("1,234"), vec![0, 1234]);
|
|
||||||
assert_eq!(eval_stack("12:30:45"), vec![0, 123045]);
|
|
||||||
assert_eq!(eval_stack("2026-08-06"), vec![0, 20260806]);
|
|
||||||
assert_eq!(eval_stack("12/34"), vec![0, 1234]);
|
|
||||||
assert_eq!(eval_stack("1+234"), vec![0, 1234]);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn dpl_counts_digits_right_of_last_punctuation() {
|
|
||||||
for (token, dpl) in [("1234.", 0), ("123.4", 1), ("12.34", 2), (".1234", 4)] {
|
|
||||||
assert_eq!(eval_stack(&format!("{token} 2DROP DPL @")), vec![dpl]);
|
|
||||||
}
|
|
||||||
// Only the rightmost punctuation counts.
|
|
||||||
assert_eq!(eval_stack("12:30:45 2DROP DPL @"), vec![2]);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn dpl_stays_negative_for_unpunctuated_numbers() {
|
|
||||||
// A leading minus is a sign, not punctuation, so these stay single-cell.
|
|
||||||
for token in ["1234", "-1", "$FF"] {
|
|
||||||
let dpl = eval_stack(&format!("{token} DROP DPL @"))[0];
|
|
||||||
assert!(dpl < 0, "token {token} left DPL = {dpl}");
|
|
||||||
}
|
|
||||||
assert_eq!(eval_stack("-1"), vec![-1]);
|
|
||||||
// DPL counts up from its seed once per digit.
|
|
||||||
assert_eq!(eval_stack("1234 DROP DPL @"), vec![DPL_INIT + 4]);
|
|
||||||
assert_eq!(eval_stack("-1 DROP DPL @"), vec![DPL_INIT + 1]);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn leading_plus_is_punctuation_not_a_sign() {
|
|
||||||
// sf64 converts `+7` as the double 7 with DPL = 1: unlike `-`, a
|
|
||||||
// leading `+` does not bind to the number.
|
|
||||||
assert_eq!(eval_stack("+7"), vec![0, 7]);
|
|
||||||
assert_eq!(eval_stack("+7 2DROP DPL @"), vec![1]);
|
|
||||||
// Same after a base prefix.
|
|
||||||
assert_eq!(eval_stack("#+7"), vec![0, 7]);
|
|
||||||
assert_eq!(eval_stack("$+F"), vec![0, 15]);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn repeated_punctuation_only_counts_from_the_last_one() {
|
|
||||||
// sf64: `12..34` is 1234 with DPL 2, `1-2-3` is 123 with DPL 1.
|
|
||||||
assert_eq!(eval_stack("12..34"), vec![0, 1234]);
|
|
||||||
assert_eq!(eval_stack("12..34 2DROP DPL @"), vec![2]);
|
|
||||||
assert_eq!(eval_stack("1-2-3"), vec![0, 123]);
|
|
||||||
assert_eq!(eval_stack("1-2-3 2DROP DPL @"), vec![1]);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn nh_recovers_an_out_of_range_single_number() {
|
|
||||||
// 4000000000 overflows a signed cell, so the stack value is truncated.
|
|
||||||
assert_eq!(eval_stack("4000000000"), vec![-294967296]);
|
|
||||||
// NH carries the high cell, making the true value recoverable.
|
|
||||||
assert_eq!(eval_stack("4000000000 NH @"), vec![0, -294967296]);
|
|
||||||
assert_eq!(eval_output("4000000000 NH @ D."), "4000000000 ");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn float_literals_still_win_over_double_punctuation() {
|
|
||||||
// `1.5E0` has an embedded dot, but "15E0" is not a decimal number,
|
|
||||||
// so conversion falls through to the float parser.
|
|
||||||
assert_eq!(eval_output("1.5E0 F."), "1.500000 ");
|
|
||||||
assert_eq!(eval_output("-3.25E0 F."), "-3.250000 ");
|
|
||||||
assert_eq!(eval_output("1E-3 F."), "0.001000 ");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn double_punctuation_respects_base_prefixes() {
|
|
||||||
assert_eq!(eval_stack("$FF."), vec![0, 255]);
|
|
||||||
assert_eq!(eval_stack("$F.F"), vec![0, 255]);
|
|
||||||
assert_eq!(eval_stack("%1010."), vec![0, 10]);
|
|
||||||
assert_eq!(eval_stack("#12.34"), vec![0, 1234]);
|
|
||||||
assert_eq!(eval_stack("-$FF."), vec![-1, -255]);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn sign_after_a_base_prefix_is_a_sign_not_punctuation() {
|
|
||||||
// Forth 2012 spells signed based numbers with the sign after the
|
|
||||||
// prefix. The dash precedes the leftmost digit, so it must not
|
|
||||||
// trigger double-cell conversion.
|
|
||||||
assert_eq!(eval_stack("#-1289"), vec![-1289]);
|
|
||||||
assert_eq!(eval_stack("$-12eF"), vec![-4847]);
|
|
||||||
assert_eq!(eval_stack("%-10010110"), vec![-150]);
|
|
||||||
// The sign may also precede the prefix, and both spellings cancel.
|
|
||||||
assert_eq!(eval_stack("-$FF"), vec![-255]);
|
|
||||||
assert_eq!(eval_stack("-$-FF"), vec![255]);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn punctuated_numbers_compile_into_definitions() {
|
|
||||||
assert_eq!(eval_stack(": D1 12.34 ; D1"), vec![0, 1234]);
|
|
||||||
assert_eq!(eval_output(": STAMP 2026-08-06 D. ; STAMP"), "20260806 ");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn optimizer_doesnt_break_basic_arithmetic() {
|
fn optimizer_doesnt_break_basic_arithmetic() {
|
||||||
assert_eq!(eval_stack("5 3 +"), vec![8]);
|
assert_eq!(eval_stack("5 3 +"), vec![8]);
|
||||||
|
|||||||
@@ -350,16 +350,6 @@ pub const WORD_DOCS: &[(&str, &str, &str)] = &[
|
|||||||
"Convert digits, accumulating into ud.",
|
"Convert digits, accumulating into ud.",
|
||||||
),
|
),
|
||||||
("BASE", "( -- addr )", "Variable holding the number base."),
|
("BASE", "( -- addr )", "Variable holding the number base."),
|
||||||
(
|
|
||||||
"DPL",
|
|
||||||
"( -- addr )",
|
|
||||||
"Variable: digits right of the last punctuation; negative if none.",
|
|
||||||
),
|
|
||||||
(
|
|
||||||
"NH",
|
|
||||||
"( -- addr )",
|
|
||||||
"Variable: high cell dropped by the last single-cell conversion.",
|
|
||||||
),
|
|
||||||
("HEX", "( -- )", "Set BASE to sixteen."),
|
("HEX", "( -- )", "Set BASE to sixteen."),
|
||||||
("DECIMAL", "( -- )", "Set BASE to ten."),
|
("DECIMAL", "( -- )", "Set BASE to ten."),
|
||||||
// -- Core: strings --
|
// -- Core: strings --
|
||||||
|
|||||||
@@ -453,21 +453,6 @@ fn programs() -> Vec<Program> {
|
|||||||
expected: "99 \n",
|
expected: "99 \n",
|
||||||
category: Category::Definitions,
|
category: Category::Definitions,
|
||||||
},
|
},
|
||||||
Program {
|
|
||||||
name: "search-order-hides",
|
|
||||||
code: "WORDLIST CONSTANT MY-WL\n\
|
|
||||||
MY-WL SET-CURRENT\n\
|
|
||||||
: SECRET 42 ;\n\
|
|
||||||
FORTH-WORDLIST SET-CURRENT\n\
|
|
||||||
[UNDEFINED] SECRET . CR\n\
|
|
||||||
GET-ORDER MY-WL SWAP 1+ SET-ORDER\n\
|
|
||||||
[DEFINED] SECRET . CR\n\
|
|
||||||
SECRET . CR\n\
|
|
||||||
-1 SET-ORDER\n\
|
|
||||||
[UNDEFINED] SECRET . CR",
|
|
||||||
expected: "-1 \n-1 \n42 \n-1 \n",
|
|
||||||
category: Category::Definitions,
|
|
||||||
},
|
|
||||||
// -- Strings --
|
// -- Strings --
|
||||||
Program {
|
Program {
|
||||||
name: "s-quote-type",
|
name: "s-quote-type",
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ workspace = true
|
|||||||
crate-type = ["cdylib", "rlib"]
|
crate-type = ["cdylib", "rlib"]
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
wafer-core = { path = "../core", version = "0.2.1", default-features = false, features = ["crypto"] }
|
wafer-core = { path = "../core", version = "0.1.0", default-features = false, features = ["crypto"] }
|
||||||
wasm-bindgen = "0.2"
|
wasm-bindgen = "0.2"
|
||||||
js-sys = "0.3"
|
js-sys = "0.3"
|
||||||
send_wrapper = { workspace = true }
|
send_wrapper = { workspace = true }
|
||||||
|
|||||||
@@ -18,11 +18,11 @@ confidence-threshold = 0.8
|
|||||||
[bans]
|
[bans]
|
||||||
multiple-versions = "deny"
|
multiple-versions = "deny"
|
||||||
wildcards = "deny"
|
wildcards = "deny"
|
||||||
# Transitive duplicates from wasmtime v47 dependencies
|
# Transitive duplicates from wasmtime v31 -- will resolve when upgrading
|
||||||
skip = [
|
skip = [
|
||||||
"getrandom",
|
"getrandom",
|
||||||
"syn",
|
|
||||||
"hashbrown",
|
"hashbrown",
|
||||||
|
"r-efi",
|
||||||
"thiserror",
|
"thiserror",
|
||||||
"thiserror-impl",
|
"thiserror-impl",
|
||||||
"wasm-encoder",
|
"wasm-encoder",
|
||||||
|
|||||||
@@ -0,0 +1,230 @@
|
|||||||
|
# Plan: SEE / SEE-IR / HELP — Introspection Trio
|
||||||
|
|
||||||
|
Status: implemented 2026-08-05 (all phases; HELP covers every word in a fresh VM, enforced by test)
|
||||||
|
Scope: `SEE` (source-level decompile), `SEE-IR` (optimized-IR dump), `HELP` (per-word docs), shared lookup infrastructure.
|
||||||
|
Each phase is self-contained and executable in a fresh context. Execute in order; every phase leaves the tree green (`cargo test --workspace` passes).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Phase 0 — Consolidated Findings (read this first, do not re-derive)
|
||||||
|
|
||||||
|
All references verified at commit `e31407a` (branch `usability`).
|
||||||
|
|
||||||
|
### The template to copy: WORDS
|
||||||
|
|
||||||
|
`WORDS` is a **host primitive whose body runs Rust-side via the `pending_define` mechanism**. This is the exact pattern for SEE/SEE-IR/HELP because it gives a real dictionary entry (→ findable by `'`, listed by `WORDS`, tab-completable in the CLI via `crates/cli/src/main.rs:452-455`, exposed to web palette via `crates/web/src/lib.rs:61`) while the implementation can still call `next_token()` and write `self.output`.
|
||||||
|
|
||||||
|
- Registration: `register_words()` at `crates/core/src/outer.rs:6301-6310` — host fn pushes code `40` into `pending_define`, called from `register_primitives()` at `outer.rs:2991` under the `// -- Programming-Tools word set --` header.
|
||||||
|
- Dispatch: `handle_pending_define()` arm at `outer.rs:5328`: `40 => self.do_words(),`.
|
||||||
|
- Body: `do_words()` at `outer.rs:6045-6074`. Note `outer.rs:6050-6052`: it reads an optional same-line argument with `self.next_token()` **gated on `self.state == 0`** — SEE must copy this gate.
|
||||||
|
- **Used `pending_define` codes: 1–12, 20, 21, 25, 33, 40.** Free: **41 (SEE), 42 (SEE-IR), 43 (HELP)**. Legend comment at `outer.rs:232-233` must be extended.
|
||||||
|
|
||||||
|
### Allowed APIs (verified signatures)
|
||||||
|
|
||||||
|
| API | Location | Notes |
|
||||||
|
|---|---|---|
|
||||||
|
| `Dictionary::find(&self, name: &str) -> Option<(u32, WordId, bool)>` | `dictionary.rs:182` | `(word_addr, WordId, is_immediate)`, case-insensitive |
|
||||||
|
| `Dictionary::word_name(word_addr)` / `code_field(word_addr)` / `read_link` / `latest()` | `dictionary.rs:375/392/287/282` | manual entry walk |
|
||||||
|
| `flags::IMMEDIATE = 0x80`, `HIDDEN = 0x40`, `INTERNAL = 0x20` | `dictionary.rs:16-27` | raw flags byte = `dict.memory()[(word_addr+4) as usize]` — no getter exists |
|
||||||
|
| `ir_bodies: HashMap<WordId, Vec<IrOp>>` | `outer.rs:256` | **post-optimization** IR; populated for colon words AND all defining-word products AND IR primitives (see kind table below) |
|
||||||
|
| `host_word_names: HashMap<WordId, String>` | `outer.rs:214` | only populated by `register_host_primitive` (`outer.rs:2702-2703`) |
|
||||||
|
| `does_definitions: HashMap<WordId, DoesDefinition>` | `outer.rs:223` | DOES>-words |
|
||||||
|
| `output: Arc<Mutex<String>>` | `outer.rs:208` | ALL text output goes here; `HostAccess` has **no** emit method (`runtime.rs:17-60`) |
|
||||||
|
| `next_token()` | `outer.rs:690-704` | whitespace-delimited, advances `input_pos` |
|
||||||
|
| `register_host_primitive(name, immediate, func) -> anyhow::Result<WordId>` | `outer.rs:2686-2691` | public |
|
||||||
|
| `IrOp` enum, `#[derive(Debug, Clone, PartialEq)]` | `ir.rs:9-218` | **no `Display` impl exists anywhere in core** — formatter is net-new |
|
||||||
|
| `eval_output(input) -> String` test helper | `outer.rs:7566` | fresh VM per call; multi-eval tests build VM inline like `outer.rs:9077-9080` |
|
||||||
|
|
||||||
|
### Word-kind classification (SEE must distinguish these)
|
||||||
|
|
||||||
|
| Kind | Detectable via | SEE output strategy |
|
||||||
|
|---|---|---|
|
||||||
|
| Colon word / `:NONAME` | in `ir_bodies`, has captured source (Phase 3) | source (Phase 3) or IR (Phase 2) |
|
||||||
|
| IR primitive (`DUP`…) | in `ir_bodies`, no source | IR body + "primitive" tag |
|
||||||
|
| Host primitive (`.S`, `WORDS`…) | in `host_word_names` | `<built-in host word>` stub |
|
||||||
|
| CONSTANT / VARIABLE / VALUE / CREATE / DEFER / SYNONYM / BUFFER: / 2\*/F\* | in `ir_bodies` with recognizable shape (e.g. CONSTANT = `[PushI32(v)]`, insert sites: `outer.rs:3194/3226/3263/3309/3351/3388/3440/6517/6547/6590/7344`) | synthesized definition, e.g. `42 CONSTANT ANSWER` (Phase 3) |
|
||||||
|
| DOES>-defined word | key in `does_definitions` | show CREATE part + DOES> body IR |
|
||||||
|
| Interpreter special token (`:`, `;`, `VARIABLE`, `'`, `CHAR`…) | hardcoded matches `outer.rs:755-820`, `927-992`; several have **no dictionary entry at all** | `<compiler word, handled by the outer interpreter>` stub |
|
||||||
|
|
||||||
|
### Hard constraints
|
||||||
|
|
||||||
|
1. **Feature-free.** `outer.rs`, `ir.rs`, `dictionary.rs` compile without the `native` feature (`lib.rs:17-42`); web consumes core with `default-features = false` (`crates/web/Cargo.toml:15`). No `#[cfg(feature = "native")]` in any SEE code. Unit tests live in the existing `#[cfg(all(test, feature = "native"))]` module (`outer.rs:7553`) — that is fine and matches practice.
|
||||||
|
2. **`ir_bodies` stores post-optimization IR** (`finish_colon_def`: optimize at `outer.rs:2297`, insert at `outer.rs:2298`; inlining threshold 8 at `optimizer.rs:56`). `: FOO SQ SQ ;` shows `SQ`'s body inlined. This is a *feature* for SEE-IR (shows what the optimizer did) and the *reason* SEE needs separate source capture (Phase 3).
|
||||||
|
3. **Multi-line definitions**: compile state persists across `evaluate()` calls (`outer.rs:512-514` resets only `input_buffer`/`input_pos`); the driver (CLI `main.rs:411-416`) feeds lines. Source capture must accumulate across calls. On error, `evaluate()` wipes compile state (`outer.rs:523-535`) — capture state must be wiped there too.
|
||||||
|
4. **Error house style** (`outer.rs:1294/3400/4057` precedents): `anyhow::bail!("SEE: unknown word: {name}")`, `anyhow::bail!("SEE: expected word name")`.
|
||||||
|
5. **Compliance suite gives SEE zero coverage** — `toolstest.fth:38-39` explicitly excludes it. All coverage is hand-written unit tests. Adding SEE cannot break `compliance_tools`.
|
||||||
|
6. **MARKER correctness**: any new per-word map (source text, docs) must be snapshotted/restored in `MarkerState` (`outer.rs:166-176`, snapshot `outer.rs:3462-3480`, restore `outer.rs:3484-3506`), mirroring how `ir_bodies` is handled there.
|
||||||
|
|
||||||
|
### Anti-patterns (verified NOT to exist — do not invent)
|
||||||
|
|
||||||
|
- `HostAccess::emit(...)` / any output method on `HostAccess` — does not exist; capture `Arc::clone(&self.output)` instead.
|
||||||
|
- `Display for IrOp` — does not exist; write the formatter.
|
||||||
|
- A dictionary "entry struct" or kind tag — does not exist; classify via the VM-side maps above.
|
||||||
|
- `Dictionary::flags(addr)` getter — does not exist; read the raw byte.
|
||||||
|
- Refill-on-demand for a missing SEE argument — `REFILL`/`ACCEPT` are hardcoded to fail (`outer.rs:5824-5852`); `SEE` at end of line is an error, same as `'` (`outer.rs:4051-4053`).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Phase 1 — IR pretty-printer (pure function, no VM changes)
|
||||||
|
|
||||||
|
**Goal:** a feature-free formatter turning `&[IrOp]` into readable, indented text. Foundation for SEE-IR and the SEE fallback path.
|
||||||
|
|
||||||
|
**What to implement:**
|
||||||
|
|
||||||
|
1. New module `crates/core/src/see.rs`, registered unconditionally in `lib.rs` next to `pub mod outer;` (`lib.rs:30`). Public API:
|
||||||
|
```rust
|
||||||
|
/// Format an IR body as indented, one-op-per-line text.
|
||||||
|
pub fn format_ir(ops: &[IrOp]) -> String
|
||||||
|
```
|
||||||
|
2. Exhaustive `match` over every `IrOp` variant (full list at `ir.rs:10-218`) — **no wildcard arm**, so adding a variant later forces a formatter update at compile time.
|
||||||
|
3. Simple ops print as their Forth-ish name plus payload: `PushI32(7)` → `push 7`, `Call(WordId(12))` → `call #12`, `TailCall` → `tail-call #12`. Resolve `#12` to a word name at a higher level (Phase 2) — `format_ir` itself stays name-agnostic, but takes an optional resolver to keep it pure:
|
||||||
|
```rust
|
||||||
|
pub fn format_ir_with(ops: &[IrOp], resolve: &dyn Fn(WordId) -> Option<String>) -> String
|
||||||
|
```
|
||||||
|
(`format_ir` delegates with a `|_| None` resolver.)
|
||||||
|
4. The six nested variants (`If`, `DoLoop`, `BeginUntil`, `BeginAgain`, `BeginWhileRepeat` at `ir.rs:78-99`, `BeginDoubleWhileRepeat` at `ir.rs:105-111`) print as Forth control words with 2-space indented bodies:
|
||||||
|
```
|
||||||
|
if
|
||||||
|
dup
|
||||||
|
mul
|
||||||
|
else
|
||||||
|
drop
|
||||||
|
then
|
||||||
|
```
|
||||||
|
5. Flat branch ops (`Block`/`BranchIfFalse`/`EndBlock`, `ir.rs:118-124`) print literally (`block L3` etc.) — they have no clean Forth surface syntax; do not attempt reconstruction.
|
||||||
|
|
||||||
|
**Verification checklist:**
|
||||||
|
- [ ] Unit tests in `see.rs` (plain `#[cfg(test)]`, NOT feature-gated — the module has no runtime dependency): nested `If` inside `DoLoop` indents correctly; every-variant smoke test via a `Vec` containing one of each simple op.
|
||||||
|
- [ ] `cargo check -p wafer-core --no-default-features` passes (proves feature-freedom).
|
||||||
|
- [ ] `cargo test --workspace` green; `cargo fmt --all` + `cargo clippy --workspace` clean.
|
||||||
|
|
||||||
|
**Anti-pattern guards:** no `impl Display for IrOp` (keep the formatter in `see.rs`, IrOp is data); no wildcard match arm; no `#[cfg(feature = "native")]`.
|
||||||
|
|
||||||
|
---## Phase 2 — SEE-IR word
|
||||||
|
|
||||||
|
**Goal:** `SEE-IR name` prints the stored post-optimization IR for any word — the optimizer-debugging view. Ship this before source-SEE: it is nearly free and immediately useful.
|
||||||
|
|
||||||
|
**What to implement:**
|
||||||
|
|
||||||
|
1. Copy the WORDS registration pattern verbatim (`outer.rs:6301-6310`): `register_see_ir()` pushes pending code **42**; register in `register_primitives()` next to `self.register_words()?` (`outer.rs:2991`). Extend the legend comment at `outer.rs:232-233`.
|
||||||
|
2. Dispatch arm in `handle_pending_define()` next to `outer.rs:5328`: `42 => self.do_see_ir(),`.
|
||||||
|
3. `do_see_ir()` (place near `do_words()`, `outer.rs:6045`):
|
||||||
|
- Parse name: `let Some(name) = self.next_token() else { bail!("SEE-IR: expected word name") }` — **no** interpret-mode gate here (unlike WORDS' optional filter, the argument is mandatory; compile-mode `SEE-IR` may simply also parse — matches `'`).
|
||||||
|
- Lookup: `self.dictionary.find(&name)` → else `bail!("SEE-IR: unknown word: {name}")`.
|
||||||
|
- Classify per the Phase 0 kind table, in this order: `ir_bodies` hit → header line + `see::format_ir_with(...)` with a resolver that maps `WordId` → name (build once from a dictionary walk: `latest()`/`read_link`/`word_name`/`code_field`, `dictionary.rs:282/287/375/392`); `host_word_names` hit → `SEE-IR: <name> is a built-in host word`; neither → `SEE-IR: <name> has no IR body`.
|
||||||
|
- Header line format: `\ <NAME> — <n> ops (optimized IR)`, plus ` immediate` when the find() flag is set, plus `does>` info when `does_definitions` has the id.
|
||||||
|
- Write everything into `self.output.lock().unwrap()`; end with `\n` (multi-line output convention from commit `2910884`).
|
||||||
|
4. Special-token names (`:`, `VARIABLE`, `'`, …): after dictionary miss, check a small const list of known interpreter tokens (source: match arms at `outer.rs:755-820`, `927-992`) and print `SEE-IR: <name> is handled directly by the outer interpreter` instead of erroring.
|
||||||
|
|
||||||
|
**Documentation references:** WORDS pattern `outer.rs:6301-6310`, `5328`, `6045-6074`; error style `outer.rs:4057`; output convention `outer.rs:6056-6073`.
|
||||||
|
|
||||||
|
**Verification checklist:**
|
||||||
|
- [ ] Tests (in `outer.rs` test module, `eval_output` style, cf. `outer.rs:9035-9041`):
|
||||||
|
- `: SQ DUP * ; SEE-IR SQ` output contains `dup` and `mul`;
|
||||||
|
- `: FOO SQ SQ ; SEE-IR FOO` shows the **inlined** body (contains two `mul`, no `call`) — locks in the "optimized view" semantics;
|
||||||
|
- `SEE-IR DUP` works (IR primitive); `SEE-IR WORDS` prints host-word stub; `SEE-IR NOSUCHWORD` errors with `SEE-IR: unknown word: NOSUCHWORD`; bare `SEE-IR` errors with `expected word name`;
|
||||||
|
- `SEE-IR :` prints the interpreter-token message.
|
||||||
|
- [ ] `IF`/`ELSE`/`THEN` and `DO LOOP` bodies render indented (one structured-word test).
|
||||||
|
- [ ] `cargo test --workspace` green; fmt + clippy clean; `cargo check -p wafer-core --no-default-features` passes.
|
||||||
|
|
||||||
|
**Anti-pattern guards:** do not print via a nonexistent `HostAccess` emit; do not gate name parsing on `state == 0` (mandatory arg, not optional filter); do not `THROW -13` (plain `bail!` matches TO/SYNONYM precedent).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Phase 3 — Source capture + SEE
|
||||||
|
|
||||||
|
**Goal:** `SEE name` prints the original source text `: name … ;` for colon words, synthesized definitions for data words, graceful stubs otherwise. This is the user-facing SEE.
|
||||||
|
|
||||||
|
**What to implement:**
|
||||||
|
|
||||||
|
1. **Capture fields** on `ForthVM` (near `compiling_ir`, `outer.rs:204`):
|
||||||
|
```rust
|
||||||
|
compiling_source: String, // accumulated raw text of the definition in progress
|
||||||
|
source_capture_from: Option<usize>, // input_pos where capture started in the CURRENT buffer
|
||||||
|
word_sources: HashMap<WordId, String>,
|
||||||
|
```
|
||||||
|
2. **Capture protocol** (verbatim source, including comments and string literals — token-level reassembly would lose them):
|
||||||
|
- `start_colon_def()` (`outer.rs:2097`): set `source_capture_from` to the position where `:` began. `interpret_token()` receives the token already consumed, so record the position **before** dispatch: in the `evaluate()` loop (`outer.rs:518-521`), remember `pos_before = self.input_pos` minus token — simplest correct form: capture `token_start` inside `next_token()` (`outer.rs:690-704`) into a new field `last_token_start: usize` as it skips whitespace; `start_colon_def` then does `self.source_capture_from = Some(self.last_token_start)`.
|
||||||
|
- End of `evaluate()` (after the loop, `outer.rs:~536`): if still compiling and capture active, flush `input_buffer[from..]` + `'\n'` into `compiling_source`, reset `source_capture_from = Some(0)` so the next buffer continues capture from its start.
|
||||||
|
- `finish_colon_def()` (`outer.rs:2266`): flush `input_buffer[from..=pos of ';']`, store `word_sources.insert(word_id, normalized)`, clear capture state. Normalize only trailing whitespace; keep interior verbatim.
|
||||||
|
- Error path `outer.rs:523-535`: clear both capture fields alongside the existing compile-state wipe.
|
||||||
|
- `:NONAME` and quotations (`outer.rs:759-761, 773-778`): skip capture (no name to SEE) — guard on `compiling_name.is_some()`.
|
||||||
|
3. **MARKER integration**: add `word_sources` to `MarkerState` (`outer.rs:166-176`), snapshot (`outer.rs:3462-3480`) and restore (`outer.rs:3484-3506`) exactly as `ir_bodies` is handled there.
|
||||||
|
4. **SEE word**: pending code **41**, same registration/dispatch shape as Phase 2. `do_see()` resolution order:
|
||||||
|
1. `word_sources` hit → print stored source verbatim, append ` immediate` on its own line if flagged (cf. `set_immediate`, `dictionary.rs:404`).
|
||||||
|
2. Recognizable data-word IR shape (Phase 0 kind table) → synthesized one-liner. CONSTANT `[PushI32(v)]` → `<v> CONSTANT <NAME>`; VARIABLE → `VARIABLE <NAME> ( addr=<v> )`; VALUE `[PushI32(a), Fetch]` → `<cur> VALUE <NAME>` reading current value via `self.rt` memory read if cheap, else `VALUE <NAME>`; SYNONYM `[Call(id)]` → `SYNONYM <NAME> <OLD>`; DEFER → `DEFER <NAME>` plus current target name via `does`/pfa lookup when resolvable.
|
||||||
|
3. `ir_bodies` hit (primitive or pre-capture colon word) → `\ <NAME> is a primitive; IR:` + `format_ir_with` output (reuse Phase 1/2 machinery — SEE never dead-ends).
|
||||||
|
4. `host_word_names` hit → `<NAME> is a built-in host word`.
|
||||||
|
5. Interpreter-token list → `<NAME> is handled by the outer interpreter (compiler word)`.
|
||||||
|
6. Else → `bail!("SEE: unknown word: {name}")`.
|
||||||
|
5. **Boot words get sources for free**: `boot.fth` definitions flow through the same `evaluate()`/`finish_colon_def` path, so `SEE NIP` etc. shows real boot source. Verify, don't assume — one test below.
|
||||||
|
|
||||||
|
**Documentation references:** compile-state lifecycle `outer.rs:512-535`, `2097-2121`, `2266-2329`; multi-line REPL driver `main.rs:411-416`; MarkerState `outer.rs:166-176, 3462-3506`.
|
||||||
|
|
||||||
|
**Verification checklist:**
|
||||||
|
- [ ] `: SQ DUP * ; SEE SQ` prints `: SQ DUP * ;` (verbatim, one line).
|
||||||
|
- [ ] Multi-line: inline-VM test (pattern `outer.rs:9077-9080`): `evaluate(": TRI\")` then `evaluate(\" DUP DUP ;")`, then `SEE TRI` shows both lines.
|
||||||
|
- [ ] Comment survives: `: C ( n -- n ) 1+ ; SEE C` output contains `( n -- n )`.
|
||||||
|
- [ ] `42 CONSTANT A SEE A` → `42 CONSTANT A`; `VARIABLE V SEE V` → contains `VARIABLE V`.
|
||||||
|
- [ ] `SEE NIP` (boot word) prints a colon definition, not an IR dump.
|
||||||
|
- [ ] `SEE DUP` prints the primitive-IR fallback; `SEE WORDS` prints host stub; `SEE '` prints interpreter-token message; unknown word errors in house style.
|
||||||
|
- [ ] MARKER round-trip: define word, set marker, redefine, execute marker, `SEE` shows the original — plus existing marker tests still green.
|
||||||
|
- [ ] Immediate flag: `: I2 ; IMMEDIATE SEE I2` output contains `immediate`.
|
||||||
|
- [ ] Error path: force `unknown word` mid-definition, then define a fresh word — its captured source must not contain debris from the aborted definition.
|
||||||
|
- [ ] Full suite + fmt + clippy + `--no-default-features` check.
|
||||||
|
|
||||||
|
**Anti-pattern guards:** do not reconstruct source from tokens (loses comments/strings/spacing); do not capture into `word_sources` for `:NONAME`; do not forget the error-path wipe (`outer.rs:523-535`) — stale capture corrupts the next definition's source; `evaluate()` resets `input_pos` per call (`outer.rs:512-514`) so `source_capture_from` is per-buffer, never carried across calls uncleared.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Phase 4 — HELP word + doc table
|
||||||
|
|
||||||
|
**Goal:** `HELP name` prints stack effect + one-line description; `HELP` alone prints usage. Shares lookup/classification with SEE.
|
||||||
|
|
||||||
|
**What to implement:**
|
||||||
|
|
||||||
|
1. New feature-free module `crates/core/src/wordhelp.rs`: a static table
|
||||||
|
```rust
|
||||||
|
/// (NAME, stack effect, one-line description)
|
||||||
|
pub const WORD_DOCS: &[(&str, &str, &str)] = &[
|
||||||
|
("DUP", "( x -- x x )", "Duplicate the top of the data stack."),
|
||||||
|
...
|
||||||
|
];
|
||||||
|
pub fn lookup(name: &str) -> Option<(&'static str, &'static str)> // case-insensitive
|
||||||
|
```
|
||||||
|
Seed from the Forth 2012 glossary (stack effects are standardized). Cover, in priority order: core + core-ext words WAFER implements, then tools/double/float sets. Incomplete coverage is acceptable and expected — `HELP` says `no help for <name> (word exists)` when the word is defined but undocumented, which doubles as the TODO list.
|
||||||
|
2. `HELP` word: pending code **43**, same registration/dispatch shape as Phase 2. Resolution: parse optional name (bare `HELP` → usage line `HELP <word> — also try: WORDS, SEE <word>, SEE-IR <word>`); table hit → print `NAME ( stack effect ) description`; miss but dictionary hit → `no help for <name>` + hint `try SEE <name>`; miss both → house-style unknown-word error.
|
||||||
|
3. Cross-wiring (the "as useful as possible" part):
|
||||||
|
- `SEE`/`SEE-IR` prepend the HELP line as a `\ ...` comment when the table has one.
|
||||||
|
- `HELP` appends ` immediate` / `built-in` / `defined in boot.fth or user code` classification reusing the Phase 2/3 classifier — factor that classifier into a shared `fn classify_word(&self, name) -> WordClass` when Phase 4 lands (do NOT pre-build it in Phase 2; extract once there are two users, per smallest-change rule).
|
||||||
|
4. User-defined words: optional docstring convention — if the captured source's first parenthesized comment looks like a stack effect (`( ... -- ... )`), `HELP` echoes it for user words. No new syntax, zero cost, rewards idiomatic Forth style.
|
||||||
|
|
||||||
|
**Verification checklist:**
|
||||||
|
- [ ] `HELP DUP` prints stack effect + description; `HELP dup` (lowercase) same.
|
||||||
|
- [ ] `HELP` alone prints usage; `HELP NOSUCH` errors house-style; `HELP MYWORD` for undocumented-but-defined word prints the `no help` + `SEE` hint.
|
||||||
|
- [ ] `: SQ ( n -- n^2 ) DUP * ; HELP SQ` echoes `( n -- n^2 )`.
|
||||||
|
- [ ] Table lint test: iterate `WORD_DOCS`, assert every documented name resolves in a booted VM's dictionary (catches typos/renames mechanically).
|
||||||
|
- [ ] Full suite + fmt + clippy + `--no-default-features`.
|
||||||
|
|
||||||
|
**Anti-pattern guards:** no doc strings threaded through `register_primitive` call sites (200+ call-site churn, bloats outer.rs — the side table is deliberate); no partial-coverage panic — missing docs degrade gracefully.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Phase 5 — Final verification + docs
|
||||||
|
|
||||||
|
1. **Full gate:** `cargo fmt --all` && `cargo clippy --workspace` (zero warnings) && `cargo test --workspace` (expect baseline 431 unit + new SEE/SEE-IR/HELP tests, 1 benchmark, 11 compliance, 9 comparison — all green).
|
||||||
|
2. **Feature-freedom proof:** `cargo check -p wafer-core --no-default-features` and web build `cd crates/web && wasm-pack build --target web --dev --out-dir www/pkg`.
|
||||||
|
3. **Manual REPL pass** (CLI): `SEE SQ`, `SEE-IR FOO` with inlining, `HELP DUP`, multi-line definition then SEE, tab-complete `SE<tab>` — confirm multi-line output renders per commit `2910884` conventions (block output, ` ok` on own line).
|
||||||
|
4. **Web REPL smoke:** serve `crates/web/www`, run the same commands — output flows through `take_output()` (`web/src/lib.rs:38-43`), no web-side changes expected.
|
||||||
|
5. **Anti-pattern grep:** `grep -n "cfg(feature" crates/core/src/see.rs crates/core/src/wordhelp.rs` → empty; `grep -n "impl Display for IrOp" -r crates/core` → empty; `grep -rn "emit" crates/core/src/see.rs` → empty.
|
||||||
|
6. **Docs:** `docs/FORTH.md:95` already lists SEE under Programming-Tools — verify claim now true; add SEE/SEE-IR/HELP to README feature list if words are enumerated there; extend CLAUDE.md test-count line.
|
||||||
|
7. **Compliance untouched:** `cargo test -p wafer-core --test compliance` — must stay 11/11 (suite excludes SEE by design, `toolstest.fth:38-39`).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Deliberate scope cuts (revisit later, not now)
|
||||||
|
|
||||||
|
- **`SEE-WASM`** (disassemble compiled module via `wasmprinter`): compiled bytes are likely dropped after instantiation; `codegen.rs` unexamined. Separate plan if wanted.
|
||||||
|
- **IR→Forth source reconstruction** for optimized bodies: lossy and misleading post-inlining; the source-capture path makes it unnecessary.
|
||||||
|
- **`LOCATE` / editor integration**: needs file/line provenance in the dictionary; out of scope.
|
||||||
|
- **Forth-side doc syntax (`:doc`)**: revisit after self-hosting work starts; the `( n -- n^2 )` echo in Phase 4 covers the 80% case with zero syntax.
|
||||||
Reference in New Issue
Block a user