Compare commits
8 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 15f8005b6d | |||
| d55a27873e | |||
| 645b00d6e8 | |||
| 9efb92ddc8 | |||
| 706c73ce2a | |||
| a89d7ca704 | |||
| 20b8754e27 | |||
| 17852ed459 |
+64
-1
@@ -5,6 +5,68 @@ 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.3] - 2026-08-06
|
||||
|
||||
### Fixed
|
||||
|
||||
- **Release builds of `wafer-web` no longer fail on proc-macro loading.**
|
||||
Cargo strips debuginfo from release artifacts by default, and on macOS
|
||||
that also strips the metadata proc-macro dylibs need to be loadable, so
|
||||
`wasm-pack build --release` died with `can't find crate` for
|
||||
`rustversion`, `thiserror_impl` and every other proc-macro. Build
|
||||
scripts and proc-macros gain nothing from stripping, so
|
||||
`[profile.release.build-override]` now exempts them; release binaries
|
||||
stay stripped. Debug builds were never affected, which is why the test
|
||||
suite stayed green while the browser REPL could not be built for
|
||||
production.
|
||||
- `wafer-web` and `wafer-cli` requested `wafer-core` version `0.2.1`
|
||||
while the workspace had moved to `0.2.2`. The caret requirement still
|
||||
resolved, so nothing broke, but the pin is now kept in step.
|
||||
|
||||
## [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
|
||||
@@ -60,7 +122,7 @@ safety net under every compiled word.
|
||||
### Fixed
|
||||
|
||||
- Multi-line command output in the CLI REPL starts on its own line
|
||||
(inline ` ok` echo only for single-line output).
|
||||
(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).
|
||||
@@ -86,5 +148,6 @@ 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
|
||||
|
||||
- Run `cargo test --workspace` before committing (currently 542 unit + 1 benchmark + 11 compliance + 9 comparison + 5 crypto)
|
||||
- Run `cargo test --workspace` before committing (currently 562 unit + 1 benchmark + 11 compliance + 9 comparison + 5 crypto)
|
||||
- Forth 2012 compliance: `cargo test -p wafer-core --test compliance`
|
||||
- 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`
|
||||
|
||||
Generated
+23
-87
@@ -132,15 +132,6 @@ dependencies = [
|
||||
"generic-array",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "block-buffer"
|
||||
version = "0.12.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d2f6c7dbe95a6ed67ad9f18e57daf93a2f034c524b99fd2b76d18fdfeb6660aa"
|
||||
dependencies = [
|
||||
"hybrid-array",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bumpalo"
|
||||
version = "3.20.3"
|
||||
@@ -182,9 +173,9 @@ checksum = "f079e83a288787bcd14a6aea84cee5c87a67c5a3e660c30f557a3d24761b3527"
|
||||
|
||||
[[package]]
|
||||
name = "clap"
|
||||
version = "4.6.5"
|
||||
version = "4.6.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "301b56658598e48f3648647ac6fc887be7e7108eddfa4e9b63fcf3ec58c0cadf"
|
||||
checksum = "473c7e07f409a8d772161724aa8db6a765a2532a70f9667eeb7b49d3d02fbdca"
|
||||
dependencies = [
|
||||
"clap_builder",
|
||||
"clap_derive",
|
||||
@@ -192,9 +183,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "clap_builder"
|
||||
version = "4.6.5"
|
||||
version = "4.6.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "94a65403d1a1bd28f7dc68eb8506e8874808ee5eecb59298de588e2e1407a078"
|
||||
checksum = "7b48fea5a88e9ae728a2dcbedbfc0e730f7d60da42e1cb049a83c9fb8b789889"
|
||||
dependencies = [
|
||||
"anstream",
|
||||
"anstyle",
|
||||
@@ -255,12 +246,6 @@ dependencies = [
|
||||
"windows-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "const-oid"
|
||||
version = "0.10.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a6ef517f0926dd24a1582492c791b6a4818a4d94e789a334894aa15b0d12f55c"
|
||||
|
||||
[[package]]
|
||||
name = "cpp_demangle"
|
||||
version = "0.5.1"
|
||||
@@ -279,15 +264,6 @@ dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cpufeatures"
|
||||
version = "0.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8b2a41393f66f16b0823bb79094d54ac5fbd34ab292ddafb9a0456ac9f87d201"
|
||||
dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cranelift-assembler-x64"
|
||||
version = "0.134.3"
|
||||
@@ -352,7 +328,7 @@ dependencies = [
|
||||
"rustc-hash",
|
||||
"serde",
|
||||
"serde_derive",
|
||||
"sha2 0.10.9",
|
||||
"sha2",
|
||||
"smallvec",
|
||||
"target-lexicon",
|
||||
"wasmtime-internal-core",
|
||||
@@ -478,15 +454,6 @@ dependencies = [
|
||||
"typenum",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crypto-common"
|
||||
version = "0.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ce6e4c961d6cd6c9a86db418387425e8bdeaf05b3c8bc1411e6dca4c252f1453"
|
||||
dependencies = [
|
||||
"hybrid-array",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "debugid"
|
||||
version = "0.8.0"
|
||||
@@ -502,19 +469,8 @@ version = "0.10.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292"
|
||||
dependencies = [
|
||||
"block-buffer 0.10.4",
|
||||
"crypto-common 0.1.7",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "digest"
|
||||
version = "0.11.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f1dd6dbb5841937940781866fa1281a1ff7bd3bf827091440879f9994983d5c2"
|
||||
dependencies = [
|
||||
"block-buffer 0.12.1",
|
||||
"const-oid",
|
||||
"crypto-common 0.2.2",
|
||||
"block-buffer",
|
||||
"crypto-common",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -795,15 +751,6 @@ dependencies = [
|
||||
"windows-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hybrid-array"
|
||||
version = "0.4.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "707114b52a152fa7bdb290cd7cd5912d9467273b6d74e21b8d81aca1f8533f6b"
|
||||
dependencies = [
|
||||
"typenum",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "id-arena"
|
||||
version = "2.3.0"
|
||||
@@ -1400,13 +1347,13 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "sha1"
|
||||
version = "0.11.0"
|
||||
version = "0.10.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "aacc4cc499359472b4abe1bf11d0b12e688af9a805fa5e3016f9a386dc2d0214"
|
||||
checksum = "a978451301f4db1d02937a4ab3ccce137717b81826e79b7d49ffe3244a13c3b8"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"cpufeatures 0.3.0",
|
||||
"digest 0.11.3",
|
||||
"cpufeatures",
|
||||
"digest",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1416,19 +1363,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"cpufeatures 0.2.17",
|
||||
"digest 0.10.7",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sha2"
|
||||
version = "0.11.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "446ba717509524cb3f22f17ecc096f10f4822d76ab5c0b9822c5f9c284e825f4"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"cpufeatures 0.3.0",
|
||||
"digest 0.11.3",
|
||||
"cpufeatures",
|
||||
"digest",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1653,7 +1589,7 @@ checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
|
||||
|
||||
[[package]]
|
||||
name = "wafer"
|
||||
version = "0.2.0"
|
||||
version = "0.2.3"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"clap",
|
||||
@@ -1664,13 +1600,13 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "wafer-core"
|
||||
version = "0.2.0"
|
||||
version = "0.2.3"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"insta",
|
||||
"proptest",
|
||||
"sha1",
|
||||
"sha2 0.11.0",
|
||||
"sha2",
|
||||
"thiserror 2.0.19",
|
||||
"wasm-encoder 0.255.0",
|
||||
"wasmparser 0.255.0",
|
||||
@@ -1679,7 +1615,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "wafer-web"
|
||||
version = "0.2.0"
|
||||
version = "0.2.3"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"js-sys",
|
||||
@@ -1966,7 +1902,7 @@ dependencies = [
|
||||
"semver",
|
||||
"serde",
|
||||
"serde_derive",
|
||||
"sha2 0.10.9",
|
||||
"sha2",
|
||||
"smallvec",
|
||||
"target-lexicon",
|
||||
"wasm-encoder 0.252.0",
|
||||
@@ -1989,7 +1925,7 @@ dependencies = [
|
||||
"rustix",
|
||||
"serde",
|
||||
"serde_derive",
|
||||
"sha2 0.10.9",
|
||||
"sha2",
|
||||
"toml",
|
||||
"wasmtime-environ",
|
||||
"windows-sys",
|
||||
@@ -2239,18 +2175,18 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "zerocopy"
|
||||
version = "0.8.55"
|
||||
version = "0.8.56"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b5a105cd7b140f6eeec8acff2ea38135d3cab283ada58540f629fe51e46696eb"
|
||||
checksum = "556764e583adb45a9f8d413c2a147fa7e8d821e48e12b14fd560b607998b75eb"
|
||||
dependencies = [
|
||||
"zerocopy-derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zerocopy-derive"
|
||||
version = "0.8.55"
|
||||
version = "0.8.56"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0fe976fb70c78cd64cccfe3a6fc142244e8a77b70959b30faf9d0ac37ee228eb"
|
||||
checksum = "f2ab42fc20575779bd240faa45f94a74256f755c0fa9e89f0ede20d91d0cdfc1"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
|
||||
+11
-3
@@ -3,7 +3,7 @@ members = ["crates/*"]
|
||||
resolver = "2"
|
||||
|
||||
[workspace.package]
|
||||
version = "0.2.0"
|
||||
version = "0.2.3"
|
||||
edition = "2024"
|
||||
license = "MIT OR Apache-2.0"
|
||||
repository = "https://github.com/ok2/wafer"
|
||||
@@ -48,6 +48,14 @@ anyhow = "1"
|
||||
thiserror = "2"
|
||||
proptest = "1"
|
||||
insta = "1"
|
||||
sha1 = "0.11"
|
||||
sha2 = "0.11"
|
||||
sha1 = "0.10"
|
||||
sha2 = "0.10"
|
||||
send_wrapper = "0.6"
|
||||
|
||||
# Cargo strips debuginfo from release artifacts by default, and on macOS that
|
||||
# also strips the metadata proc-macro dylibs need to be loadable — release
|
||||
# builds then fail with "can't find crate" for every proc-macro (rustversion,
|
||||
# thiserror_impl, ...). Build scripts and proc-macros gain nothing from
|
||||
# stripping, so exempt them; the release binaries stay stripped.
|
||||
[profile.release.build-override]
|
||||
strip = false
|
||||
|
||||
@@ -9,7 +9,7 @@ license.workspace = true
|
||||
workspace = true
|
||||
|
||||
[dependencies]
|
||||
wafer-core = { path = "../core", version = "0.2.0" }
|
||||
wafer-core = { path = "../core", version = "0.2.3" }
|
||||
wasmtime = { workspace = true }
|
||||
anyhow = { workspace = true }
|
||||
clap = { version = "4", features = ["derive"] }
|
||||
|
||||
@@ -192,10 +192,9 @@ impl Dictionary {
|
||||
}
|
||||
}
|
||||
}
|
||||
// Fallback: return newest entry across all wordlists
|
||||
if let Some(&(_wid, word_addr, fn_index, is_immediate)) = entries.last() {
|
||||
return Some((word_addr, WordId(fn_index), is_immediate));
|
||||
}
|
||||
// In no wordlist of the search order: not findable
|
||||
// (Forth 2012 §16.3.3 — the order is authoritative).
|
||||
return None;
|
||||
}
|
||||
|
||||
// Fallback: linked-list walk (for words not yet in the index)
|
||||
|
||||
@@ -108,6 +108,23 @@ pub const SYSVAR_HLD: u32 = SYSVAR_BASE + 28;
|
||||
pub const SYSVAR_LEAVE_FLAG: u32 = SYSVAR_BASE + 32;
|
||||
/// Throw code left by a compiled stack-guard fault for `_STACK_FAULT_`.
|
||||
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)]
|
||||
mod tests {
|
||||
@@ -149,6 +166,9 @@ mod tests {
|
||||
SYSVAR_NUM_TIB,
|
||||
SYSVAR_HLD,
|
||||
SYSVAR_LEAVE_FLAG,
|
||||
SYSVAR_FAULT_CODE,
|
||||
SYSVAR_DPL,
|
||||
SYSVAR_NH,
|
||||
];
|
||||
for offset in all_offsets {
|
||||
assert!(offset + CELL_SIZE <= SYSVAR_BASE + SYSVAR_SIZE);
|
||||
|
||||
+460
-171
@@ -23,12 +23,64 @@ use crate::ir::IrOp;
|
||||
#[cfg(feature = "crypto")]
|
||||
use crate::memory::HASH_SCRATCH_BASE;
|
||||
use crate::memory::{
|
||||
CELL_SIZE, DATA_STACK_TOP, FLOAT_SIZE, FLOAT_STACK_BASE, FLOAT_STACK_TOP, INPUT_BUFFER_BASE,
|
||||
INPUT_BUFFER_SIZE, RETURN_STACK_TOP, SYSVAR_BASE_VAR, SYSVAR_FAULT_CODE, SYSVAR_HERE,
|
||||
SYSVAR_LEAVE_FLAG, SYSVAR_NUM_TIB, SYSVAR_STATE, SYSVAR_TO_IN,
|
||||
CELL_SIZE, DATA_STACK_TOP, DPL_INIT, FLOAT_SIZE, FLOAT_STACK_BASE, FLOAT_STACK_TOP,
|
||||
INPUT_BUFFER_BASE, INPUT_BUFFER_SIZE, RETURN_STACK_TOP, SYSVAR_BASE_VAR, SYSVAR_DPL,
|
||||
SYSVAR_FAULT_CODE, SYSVAR_HERE, SYSVAR_LEAVE_FLAG, SYSVAR_NH, SYSVAR_NUM_TIB, SYSVAR_STATE,
|
||||
SYSVAR_TO_IN,
|
||||
};
|
||||
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
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -521,6 +573,40 @@ fn host_pop(ctx: &mut dyn HostAccess) -> anyhow::Result<i32> {
|
||||
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
|
||||
/// 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
|
||||
@@ -1160,22 +1246,18 @@ impl<R: Runtime> ForthVM<R> {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// Try to parse as double-number (trailing dot)
|
||||
if let Some((lo, hi)) = self.parse_double_number(token) {
|
||||
self.push_data_stack(lo)?;
|
||||
self.push_data_stack(hi)?;
|
||||
// Try to convert as a number; punctuation makes it double-cell
|
||||
if let Some(lit) = self.parse_numeric_literal(token) {
|
||||
self.record_number_conversion(lit);
|
||||
self.push_data_stack(lit.lo())?;
|
||||
if self.recording_toplevel && self.state == 0 {
|
||||
self.toplevel_ir.push(IrOp::PushI32(lo));
|
||||
self.toplevel_ir.push(IrOp::PushI32(hi));
|
||||
self.toplevel_ir.push(IrOp::PushI32(lit.lo()));
|
||||
}
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// Try to parse as number
|
||||
if let Some(n) = self.parse_number(token) {
|
||||
self.push_data_stack(n)?;
|
||||
if lit.is_double {
|
||||
self.push_data_stack(lit.hi())?;
|
||||
if self.recording_toplevel && self.state == 0 {
|
||||
self.toplevel_ir.push(IrOp::PushI32(n));
|
||||
self.toplevel_ir.push(IrOp::PushI32(lit.hi()));
|
||||
}
|
||||
}
|
||||
return Ok(());
|
||||
}
|
||||
@@ -1537,16 +1619,13 @@ impl<R: Runtime> ForthVM<R> {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// Try to parse as double-number (trailing dot)
|
||||
if let Some((lo, hi)) = self.parse_double_number(token) {
|
||||
self.push_ir(IrOp::PushI32(lo));
|
||||
self.push_ir(IrOp::PushI32(hi));
|
||||
return Ok(());
|
||||
// Try to convert as a number; punctuation makes it double-cell
|
||||
if let Some(lit) = self.parse_numeric_literal(token) {
|
||||
self.record_number_conversion(lit);
|
||||
self.push_ir(IrOp::PushI32(lit.lo()));
|
||||
if lit.is_double {
|
||||
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(());
|
||||
}
|
||||
|
||||
@@ -2697,85 +2776,111 @@ impl<R: Runtime> ForthVM<R> {
|
||||
// Number parsing
|
||||
// -----------------------------------------------------------------------
|
||||
|
||||
/// Try to parse a token as a number.
|
||||
fn parse_number(&self, token: &str) -> Option<i32> {
|
||||
/// Try to convert a token to a number, following `SwiftForth`'s input
|
||||
/// number conversion rules.
|
||||
///
|
||||
/// 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();
|
||||
if token.is_empty() {
|
||||
return None;
|
||||
}
|
||||
|
||||
// Check for negative prefix
|
||||
let (negative, rest) = if let Some(stripped) = token.strip_prefix('-') {
|
||||
(true, stripped)
|
||||
} else {
|
||||
(false, token)
|
||||
};
|
||||
// A leading sign binds to the number; it is not double punctuation.
|
||||
let (neg_outer, rest) = strip_sign(token);
|
||||
|
||||
if rest.is_empty() {
|
||||
return None;
|
||||
}
|
||||
|
||||
// Parse based on prefix
|
||||
let result = if let Some(hex) = rest.strip_prefix('$') {
|
||||
i64::from_str_radix(hex, 16).ok()
|
||||
} else if let Some(dec) = rest.strip_prefix('#') {
|
||||
dec.parse::<i64>().ok()
|
||||
} else if let Some(bin) = rest.strip_prefix('%') {
|
||||
i64::from_str_radix(bin, 2).ok()
|
||||
} 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)
|
||||
} else {
|
||||
i64::from_str_radix(rest, self.base).ok()
|
||||
};
|
||||
|
||||
result.map(|n| if negative { -(n as i32) } else { n as i32 })
|
||||
// Character literal: 'x' → ASCII value of x. No digits, so DPL stays
|
||||
// at its seed and the result is always single-cell.
|
||||
if rest.len() == 3 && rest.as_bytes()[0] == b'\'' && rest.as_bytes()[2] == b'\'' {
|
||||
let value = i64::from(rest.as_bytes()[1]);
|
||||
return Some(NumberLiteral {
|
||||
value: if neg_outer { -value } else { value },
|
||||
dpl: DPL_INIT,
|
||||
is_double: false,
|
||||
});
|
||||
}
|
||||
|
||||
/// Try to parse a token as a double-number (token ends with `.`).
|
||||
/// Returns (lo, hi) where the double-cell value is (hi << 32) | lo.
|
||||
fn parse_double_number(&self, token: &str) -> Option<(i32, i32)> {
|
||||
let token = token.trim();
|
||||
if token.is_empty() {
|
||||
// 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;
|
||||
}
|
||||
|
||||
// Check for trailing dot (double-number indicator)
|
||||
let without_dot = token.strip_suffix('.')?;
|
||||
if without_dot.is_empty() {
|
||||
// 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;
|
||||
}
|
||||
|
||||
// Check for negative prefix
|
||||
let (negative, rest) = if let Some(stripped) = without_dot.strip_prefix('-') {
|
||||
(true, stripped)
|
||||
// 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 {
|
||||
(false, without_dot)
|
||||
magnitude as i64
|
||||
};
|
||||
|
||||
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)
|
||||
Some(NumberLiteral {
|
||||
value,
|
||||
dpl,
|
||||
is_double,
|
||||
})
|
||||
}
|
||||
|
||||
/// Publish the outcome of a conversion in `DPL` and `NH`.
|
||||
///
|
||||
/// `NH` only carries meaning after a single-cell conversion, where it holds
|
||||
/// the high-order cell that the stack result dropped.
|
||||
fn record_number_conversion(&mut self, lit: NumberLiteral) {
|
||||
self.rt.mem_write_i32(SYSVAR_DPL, lit.dpl);
|
||||
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`.
|
||||
/// Used where only a plain cell value is meaningful.
|
||||
fn parse_number(&self, token: &str) -> Option<i32> {
|
||||
self.parse_numeric_literal(token)
|
||||
.filter(|lit| !lit.is_double)
|
||||
.map(NumberLiteral::lo)
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// Float literal parsing
|
||||
// -----------------------------------------------------------------------
|
||||
@@ -3058,6 +3163,7 @@ impl<R: Runtime> ForthVM<R> {
|
||||
self.register_to_in()?;
|
||||
self.register_state_var()?;
|
||||
self.register_base_var()?;
|
||||
self.register_number_conversion_vars()?;
|
||||
|
||||
// Double-cell arithmetic
|
||||
self.register_m_star()?;
|
||||
@@ -3325,7 +3431,7 @@ impl<R: Runtime> ForthVM<R> {
|
||||
let digest_len = algo.digest_len as i32;
|
||||
let func: HostFn = Box::new(move |ctx: &mut dyn HostAccess| {
|
||||
// Pop ( c-addr u )
|
||||
let dsp = ctx.get_dsp();
|
||||
let dsp = host_need(ctx, 2)?;
|
||||
let u = ctx.mem_read_i32(dsp) as u32;
|
||||
let c_addr = ctx.mem_read_i32(dsp + CELL_SIZE) as u32;
|
||||
|
||||
@@ -4077,8 +4183,9 @@ impl<R: Runtime> ForthVM<R> {
|
||||
fn register_roll(&mut self) -> anyhow::Result<()> {
|
||||
let func: HostFn = Box::new(move |ctx: &mut dyn HostAccess| {
|
||||
// Pop u from stack
|
||||
let sp = ctx.get_dsp();
|
||||
let sp = host_need(ctx, 1)?;
|
||||
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
|
||||
|
||||
if u == 0 {
|
||||
@@ -4263,7 +4370,7 @@ impl<R: Runtime> ForthVM<R> {
|
||||
|
||||
let func: HostFn = Box::new(move |ctx: &mut dyn HostAccess| {
|
||||
// Pop xt from data stack
|
||||
let sp = ctx.get_dsp();
|
||||
let sp = host_need(ctx, 1)?;
|
||||
let xt = ctx.mem_read_i32(sp as u32) as u32;
|
||||
|
||||
// Look up PFA for this xt
|
||||
@@ -4283,7 +4390,7 @@ impl<R: Runtime> ForthVM<R> {
|
||||
/// ENVIRONMENT? -- ( c-addr u -- false | value true ) query system parameters.
|
||||
fn register_environment_q(&mut self) -> anyhow::Result<()> {
|
||||
let func: HostFn = Box::new(move |ctx: &mut dyn HostAccess| {
|
||||
let sp = ctx.get_dsp();
|
||||
let sp = host_need(ctx, 2)?;
|
||||
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 addr = u32::from_le_bytes(b);
|
||||
@@ -5116,10 +5223,26 @@ impl<R: Runtime> ForthVM<R> {
|
||||
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.
|
||||
fn register_m_star(&mut self) -> anyhow::Result<()> {
|
||||
let func: HostFn = Box::new(move |ctx: &mut dyn HostAccess| {
|
||||
let sp = ctx.get_dsp();
|
||||
let sp = host_need(ctx, 2)?;
|
||||
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 n1 = i32::from_le_bytes(b) as i64;
|
||||
@@ -5140,7 +5263,7 @@ impl<R: Runtime> ForthVM<R> {
|
||||
/// UM* ( u1 u2 -- ud ) unsigned multiply producing double-cell result.
|
||||
fn register_um_star(&mut self) -> anyhow::Result<()> {
|
||||
let func: HostFn = Box::new(move |ctx: &mut dyn HostAccess| {
|
||||
let sp = ctx.get_dsp();
|
||||
let sp = host_need(ctx, 2)?;
|
||||
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 u1 = u32::from_le_bytes(b) as u64;
|
||||
@@ -5159,7 +5282,7 @@ impl<R: Runtime> ForthVM<R> {
|
||||
/// UM/MOD ( ud u -- rem quot ) unsigned double-cell divide.
|
||||
fn register_um_div_mod(&mut self) -> anyhow::Result<()> {
|
||||
let func: HostFn = Box::new(move |ctx: &mut dyn HostAccess| {
|
||||
let sp = ctx.get_dsp();
|
||||
let sp = host_need(ctx, 3)?;
|
||||
// Pop u (divisor)
|
||||
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
|
||||
@@ -5247,7 +5370,7 @@ impl<R: Runtime> ForthVM<R> {
|
||||
|
||||
let func: HostFn = Box::new(move |ctx: &mut dyn HostAccess| {
|
||||
// Pop xt from data stack
|
||||
let sp = ctx.get_dsp();
|
||||
let sp = host_need(ctx, 1)?;
|
||||
let xt = ctx.mem_read_i32(sp as u32) as u32;
|
||||
// Drop top of stack
|
||||
let new_sp = sp + 4;
|
||||
@@ -5332,7 +5455,7 @@ impl<R: Runtime> ForthVM<R> {
|
||||
|
||||
let func: HostFn = Box::new(move |ctx: &mut dyn HostAccess| {
|
||||
// ( c-addr u -- ) — pop both cells.
|
||||
let sp = ctx.get_dsp();
|
||||
let sp = host_need(ctx, 2)?;
|
||||
let u = ctx.mem_read_i32(sp) as u32;
|
||||
let addr = ctx.mem_read_i32(sp + CELL_SIZE) as u32;
|
||||
ctx.set_dsp(sp + 2 * CELL_SIZE);
|
||||
@@ -5453,10 +5576,7 @@ impl<R: Runtime> ForthVM<R> {
|
||||
/// WORD ( char -- c-addr ) reads from the WASM input buffer and updates >IN.
|
||||
fn register_word_word(&mut self) -> anyhow::Result<()> {
|
||||
let func: HostFn = Box::new(move |ctx: &mut dyn HostAccess| {
|
||||
// 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);
|
||||
let delim = host_pop(ctx)? as u8;
|
||||
|
||||
// Read >IN and #TIB from WASM memory
|
||||
let b: [u8; 4] = ctx.mem_read_i32(SYSVAR_TO_IN as u32).to_le_bytes();
|
||||
@@ -5502,8 +5622,8 @@ impl<R: Runtime> ForthVM<R> {
|
||||
ctx.mem_write_u8((dst_start + i) as u32, byte);
|
||||
}
|
||||
|
||||
// Push c-addr onto data stack
|
||||
let new_sp = sp; // We already popped delim, now push c-addr
|
||||
// Push c-addr onto data stack (reuse the popped delim's slot)
|
||||
let new_sp = ctx.get_dsp() - CELL_SIZE;
|
||||
ctx.mem_write_i32(new_sp, buf_addr as i32);
|
||||
ctx.set_dsp(new_sp);
|
||||
|
||||
@@ -5856,6 +5976,9 @@ impl<R: Runtime> ForthVM<R> {
|
||||
fn register_2r_fetch(&mut self) -> anyhow::Result<()> {
|
||||
let func: HostFn = Box::new(move |ctx: &mut dyn HostAccess| {
|
||||
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();
|
||||
// Return stack: x2 at rsp, x1 at rsp+4
|
||||
let b: [u8; 4] = ctx.mem_read_i32(rsp_val as u32).to_le_bytes();
|
||||
@@ -5963,9 +6086,7 @@ impl<R: Runtime> ForthVM<R> {
|
||||
|
||||
let state = Arc::clone(&self.rng_state);
|
||||
let func: HostFn = Box::new(move |ctx: &mut dyn HostAccess| {
|
||||
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 seed = host_pop(ctx)? as u32 as u64;
|
||||
let mut s = state.lock().unwrap();
|
||||
*s = if seed == 0 {
|
||||
0xDEAD_BEEF_CAFE_BABE
|
||||
@@ -5982,7 +6103,7 @@ impl<R: Runtime> ForthVM<R> {
|
||||
fn register_parse_host(&mut self) -> anyhow::Result<()> {
|
||||
let func: HostFn = Box::new(move |ctx: &mut dyn HostAccess| {
|
||||
// Pop delimiter from data stack
|
||||
let sp = ctx.get_dsp();
|
||||
let sp = host_need(ctx, 1)?;
|
||||
let delim = ctx.mem_read_i32(sp as u32) as u8;
|
||||
let sp = sp + CELL_SIZE; // pop delimiter
|
||||
|
||||
@@ -6100,7 +6221,7 @@ impl<R: Runtime> ForthVM<R> {
|
||||
// In non-interactive mode, return 0 (no input).
|
||||
let func: HostFn = Box::new(move |ctx: &mut dyn HostAccess| {
|
||||
// Pop +n1 (max count) and c-addr from stack
|
||||
let sp = ctx.get_dsp();
|
||||
let sp = host_need(ctx, 2)?;
|
||||
let new_sp = sp + CELL_SIZE; // pop +n1
|
||||
let new_sp = new_sp + CELL_SIZE; // pop c-addr
|
||||
// Push 0 (no characters received)
|
||||
@@ -6125,7 +6246,7 @@ impl<R: Runtime> ForthVM<R> {
|
||||
fn register_memory_alloc(&mut self) -> anyhow::Result<()> {
|
||||
// ALLOCATE ( u -- a-addr ior )
|
||||
let func: HostFn = Box::new(move |ctx: &mut dyn HostAccess| {
|
||||
let sp = ctx.get_dsp();
|
||||
let sp = host_need(ctx, 1)?;
|
||||
let size = ctx.mem_read_i32(sp as u32) as u32;
|
||||
|
||||
let mem_len = ctx.mem_len() as u32;
|
||||
@@ -6184,7 +6305,7 @@ impl<R: Runtime> ForthVM<R> {
|
||||
// FREE ( a-addr -- ior )
|
||||
let func: HostFn = Box::new(move |ctx: &mut dyn HostAccess| {
|
||||
// Simple allocator: FREE is a no-op (arena style), return ior=0
|
||||
let sp = ctx.get_dsp();
|
||||
let sp = host_need(ctx, 1)?;
|
||||
// Replace a-addr with ior=0
|
||||
ctx.mem_write_i32(sp as u32, 0i32 as i32);
|
||||
Ok(())
|
||||
@@ -6193,7 +6314,7 @@ impl<R: Runtime> ForthVM<R> {
|
||||
|
||||
// RESIZE ( a-addr u -- a-addr2 ior )
|
||||
let func: HostFn = Box::new(move |ctx: &mut dyn HostAccess| {
|
||||
let sp = ctx.get_dsp();
|
||||
let sp = host_need(ctx, 2)?;
|
||||
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 old_addr = u32::from_le_bytes(b);
|
||||
@@ -6614,8 +6735,14 @@ impl<R: Runtime> ForthVM<R> {
|
||||
{
|
||||
let so = Arc::clone(&self.search_order);
|
||||
let func: HostFn = Box::new(move |ctx: &mut dyn HostAccess| {
|
||||
let sp = ctx.get_dsp();
|
||||
let sp = host_need(ctx, 1)?;
|
||||
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 {
|
||||
*so.lock().unwrap() = vec![1];
|
||||
@@ -6718,8 +6845,9 @@ impl<R: Runtime> ForthVM<R> {
|
||||
fn register_n_to_r(&mut self) -> anyhow::Result<()> {
|
||||
// N>R ( xn..x1 n -- ; R: -- x1..xn n )
|
||||
let func: HostFn = Box::new(move |ctx: &mut dyn HostAccess| {
|
||||
let sp = ctx.get_dsp();
|
||||
let sp = host_need(ctx, 1)?;
|
||||
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();
|
||||
|
||||
@@ -6803,7 +6931,7 @@ impl<R: Runtime> ForthVM<R> {
|
||||
// UNESCAPE ( c-addr1 u1 c-addr2 -- c-addr2 u2 )
|
||||
// Copy string escaping each % as %%
|
||||
let func: HostFn = Box::new(move |ctx: &mut dyn HostAccess| {
|
||||
let sp = ctx.get_dsp();
|
||||
let sp = host_need(ctx, 3)?;
|
||||
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 u1 = u32::from_le_bytes(b);
|
||||
@@ -6841,7 +6969,7 @@ impl<R: Runtime> ForthVM<R> {
|
||||
// Define substitution: name (c-addr2 u2) → replacement (c-addr1 u1)
|
||||
let subs = Arc::clone(&self.substitutions);
|
||||
let func: HostFn = Box::new(move |ctx: &mut dyn HostAccess| {
|
||||
let sp = ctx.get_dsp();
|
||||
let sp = host_need(ctx, 4)?;
|
||||
// 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 b: [u8; 4] = ctx.mem_read_i32((sp + 4) as u32).to_le_bytes();
|
||||
@@ -6869,7 +6997,7 @@ impl<R: Runtime> ForthVM<R> {
|
||||
// Replace %name% patterns, %% → %
|
||||
let subs = Arc::clone(&self.substitutions);
|
||||
let func: HostFn = Box::new(move |ctx: &mut dyn HostAccess| {
|
||||
let sp = ctx.get_dsp();
|
||||
let sp = host_need(ctx, 4)?;
|
||||
// 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 b: [u8; 4] = ctx.mem_read_i32((sp + 4) as u32).to_le_bytes();
|
||||
@@ -6957,7 +7085,7 @@ impl<R: Runtime> ForthVM<R> {
|
||||
/// M*/ ( d n1 n2 -- d ) multiply d by n1, divide by n2.
|
||||
fn register_m_star_slash(&mut self) -> anyhow::Result<()> {
|
||||
let func: HostFn = Box::new(move |ctx: &mut dyn HostAccess| {
|
||||
let sp = ctx.get_dsp();
|
||||
let sp = host_need(ctx, 4)?;
|
||||
// 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 b: [u8; 4] = ctx.mem_read_i32((sp + 4) as u32).to_le_bytes();
|
||||
@@ -7093,7 +7221,7 @@ impl<R: Runtime> ForthVM<R> {
|
||||
/// SEARCH ( c-addr1 u1 c-addr2 u2 -- c-addr3 u3 flag ) search for substring.
|
||||
fn register_search(&mut self) -> anyhow::Result<()> {
|
||||
let func: HostFn = Box::new(move |ctx: &mut dyn HostAccess| {
|
||||
let sp = ctx.get_dsp();
|
||||
let sp = host_need(ctx, 4)?;
|
||||
// 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 b: [u8; 4] = ctx.mem_read_i32((sp + 4) as u32).to_le_bytes();
|
||||
@@ -7244,7 +7372,7 @@ impl<R: Runtime> ForthVM<R> {
|
||||
// FROT ( F: r1 r2 r3 -- r2 r3 r1 )
|
||||
{
|
||||
let func: HostFn = Box::new(move |ctx: &mut dyn HostAccess| {
|
||||
let sp = ctx.get_fsp();
|
||||
let sp = host_fneed(ctx, 3)?;
|
||||
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 a: [u8; 8] = ctx.mem_read_slice(sp + 16, 8).try_into().unwrap();
|
||||
@@ -7311,14 +7439,9 @@ impl<R: Runtime> ForthVM<R> {
|
||||
// If r3 < 0: true if |r1-r2| < |r3|*(|r1|+|r2|)
|
||||
{
|
||||
let func: HostFn = Box::new(move |ctx: &mut dyn HostAccess| {
|
||||
let sp = ctx.get_fsp();
|
||||
let r3_bytes: [u8; 8] = ctx.mem_read_slice(sp, 8).try_into().unwrap();
|
||||
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 r3 = host_fpop(ctx)?;
|
||||
let r2 = host_fpop(ctx)?;
|
||||
let r1 = host_fpop(ctx)?;
|
||||
|
||||
let result = if r3 > 0.0 {
|
||||
(r1 - r2).abs() < r3
|
||||
@@ -7366,7 +7489,7 @@ impl<R: Runtime> ForthVM<R> {
|
||||
// FALIGNED ( addr -- f-addr ) align to float boundary (8 bytes)
|
||||
{
|
||||
let func: HostFn = Box::new(move |ctx: &mut dyn HostAccess| {
|
||||
let sp = ctx.get_dsp();
|
||||
let sp = host_need(ctx, 1)?;
|
||||
let addr = ctx.mem_read_i32(sp as u32) as u32;
|
||||
let aligned = (addr + 7) & !7;
|
||||
ctx.mem_write_i32(sp as u32, aligned as i32);
|
||||
@@ -7411,7 +7534,7 @@ impl<R: Runtime> ForthVM<R> {
|
||||
// D>F ( d -- ) ( F: -- r ) convert double-cell integer to float
|
||||
{
|
||||
let func: HostFn = Box::new(move |ctx: &mut dyn HostAccess| {
|
||||
let sp = ctx.get_dsp();
|
||||
let sp = host_need(ctx, 2)?;
|
||||
// Double-cell: hi on top, lo below
|
||||
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();
|
||||
@@ -7434,11 +7557,7 @@ impl<R: Runtime> ForthVM<R> {
|
||||
// F>D ( -- d ) ( F: r -- ) convert float to double-cell integer
|
||||
{
|
||||
let func: HostFn = Box::new(move |ctx: &mut dyn HostAccess| {
|
||||
// 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);
|
||||
let f = host_fpop(ctx)?;
|
||||
// Convert to i64
|
||||
let d = f as i64;
|
||||
let lo = d as i32;
|
||||
@@ -7524,10 +7643,7 @@ impl<R: Runtime> ForthVM<R> {
|
||||
let output = Arc::clone(&self.output);
|
||||
let precision = Arc::clone(&self.float_precision);
|
||||
let func: HostFn = Box::new(move |ctx: &mut dyn HostAccess| {
|
||||
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 val = host_fpop(ctx)?;
|
||||
let prec = *precision.lock().unwrap();
|
||||
let s = format!("{val:.prec$} ");
|
||||
output.lock().unwrap().push_str(&s);
|
||||
@@ -7541,10 +7657,7 @@ impl<R: Runtime> ForthVM<R> {
|
||||
let output = Arc::clone(&self.output);
|
||||
let precision = Arc::clone(&self.float_precision);
|
||||
let func: HostFn = Box::new(move |ctx: &mut dyn HostAccess| {
|
||||
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 val = host_fpop(ctx)?;
|
||||
let prec = *precision.lock().unwrap();
|
||||
let s = format_engineering(val, prec);
|
||||
output.lock().unwrap().push_str(&s);
|
||||
@@ -7558,10 +7671,7 @@ impl<R: Runtime> ForthVM<R> {
|
||||
let output = Arc::clone(&self.output);
|
||||
let precision = Arc::clone(&self.float_precision);
|
||||
let func: HostFn = Box::new(move |ctx: &mut dyn HostAccess| {
|
||||
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 val = host_fpop(ctx)?;
|
||||
let prec = *precision.lock().unwrap();
|
||||
let s = format!("{val:.prec$E} ");
|
||||
output.lock().unwrap().push_str(&s);
|
||||
@@ -7588,9 +7698,7 @@ impl<R: Runtime> ForthVM<R> {
|
||||
{
|
||||
let precision = Arc::clone(&self.float_precision);
|
||||
let func: HostFn = Box::new(move |ctx: &mut dyn HostAccess| {
|
||||
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);
|
||||
let n = host_pop(ctx)? as usize;
|
||||
*precision.lock().unwrap() = n;
|
||||
Ok(())
|
||||
});
|
||||
@@ -7600,17 +7708,12 @@ impl<R: Runtime> ForthVM<R> {
|
||||
// REPRESENT ( c-addr u -- n flag1 flag2 ) ( F: r -- )
|
||||
{
|
||||
let func: HostFn = Box::new(move |ctx: &mut dyn HostAccess| {
|
||||
// Read all values from memory first
|
||||
let sp = ctx.get_dsp();
|
||||
let fsp_val = ctx.get_fsp();
|
||||
let sp = host_need(ctx, 2)?;
|
||||
let u = ctx.mem_read_i32(sp) as usize;
|
||||
let c_addr = ctx.mem_read_i32(sp + 4) 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);
|
||||
|
||||
// Update stack pointers: pop 2 data cells, pop 1 float
|
||||
let val = host_fpop(ctx)?;
|
||||
// Pop the 2 data cells
|
||||
ctx.set_dsp(sp + 8);
|
||||
ctx.set_fsp(fsp_val + FLOAT_SIZE);
|
||||
|
||||
let (digits, exp, is_negative, is_valid) = represent_float(val, u);
|
||||
|
||||
@@ -7638,7 +7741,7 @@ impl<R: Runtime> ForthVM<R> {
|
||||
// >FLOAT ( c-addr u -- flag ) ( F: -- r | ) parse string as float
|
||||
{
|
||||
let func: HostFn = Box::new(move |ctx: &mut dyn HostAccess| {
|
||||
let sp = ctx.get_dsp();
|
||||
let sp = host_need(ctx, 2)?;
|
||||
let u = ctx.mem_read_i32(sp) as usize;
|
||||
let c_addr = ctx.mem_read_i32(sp + 4) as u32;
|
||||
let s_bytes = ctx.mem_read_slice(c_addr, u);
|
||||
@@ -7679,14 +7782,9 @@ impl<R: Runtime> ForthVM<R> {
|
||||
// SF! ( sf-addr -- ) ( F: r -- ) store as single-precision float (f32)
|
||||
{
|
||||
let func: HostFn = Box::new(move |ctx: &mut dyn HostAccess| {
|
||||
let sp = ctx.get_dsp();
|
||||
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 addr = host_pop(ctx)? as u32;
|
||||
let val = host_fpop(ctx)?;
|
||||
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);
|
||||
Ok(())
|
||||
});
|
||||
@@ -7696,12 +7794,10 @@ impl<R: Runtime> ForthVM<R> {
|
||||
// SF@ ( sf-addr -- ) ( F: -- r ) fetch single-precision float (f32)
|
||||
{
|
||||
let func: HostFn = Box::new(move |ctx: &mut dyn HostAccess| {
|
||||
let sp = ctx.get_dsp();
|
||||
let addr = host_pop(ctx)? as u32;
|
||||
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 val = f32::from_le_bytes(f32_bytes) as f64;
|
||||
ctx.set_dsp(sp + CELL_SIZE);
|
||||
let new_fsp = fsp_val - FLOAT_SIZE;
|
||||
ctx.set_fsp(new_fsp);
|
||||
ctx.mem_write_slice(new_fsp, &val.to_le_bytes());
|
||||
@@ -7713,12 +7809,8 @@ impl<R: Runtime> ForthVM<R> {
|
||||
// DF! ( df-addr -- ) ( F: r -- ) same as F! (our floats are already f64)
|
||||
{
|
||||
let func: HostFn = Box::new(move |ctx: &mut dyn HostAccess| {
|
||||
let sp = ctx.get_dsp();
|
||||
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);
|
||||
let addr = host_pop(ctx)? as u32;
|
||||
let float_bytes = host_fpop(ctx)?.to_le_bytes();
|
||||
ctx.mem_write_slice(addr, &float_bytes);
|
||||
Ok(())
|
||||
});
|
||||
@@ -7728,12 +7820,10 @@ impl<R: Runtime> ForthVM<R> {
|
||||
// DF@ ( df-addr -- ) ( F: -- r ) same as F@ (our floats are already f64)
|
||||
{
|
||||
let func: HostFn = Box::new(move |ctx: &mut dyn HostAccess| {
|
||||
let sp = ctx.get_dsp();
|
||||
let addr = host_pop(ctx)? as u32;
|
||||
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 val = f64::from_le_bytes(float_bytes);
|
||||
ctx.set_dsp(sp + CELL_SIZE);
|
||||
let new_fsp = fsp_val - FLOAT_SIZE;
|
||||
ctx.set_fsp(new_fsp);
|
||||
ctx.mem_write_slice(new_fsp, &val.to_le_bytes());
|
||||
@@ -7745,7 +7835,7 @@ impl<R: Runtime> ForthVM<R> {
|
||||
// SFALIGNED, DFALIGNED (alignment words for single/double floats)
|
||||
{
|
||||
let func: HostFn = Box::new(move |ctx: &mut dyn HostAccess| {
|
||||
let sp = ctx.get_dsp();
|
||||
let sp = host_need(ctx, 1)?;
|
||||
let addr = ctx.mem_read_i32(sp as u32) as u32;
|
||||
let aligned = (addr + 3) & !3; // 4-byte alignment for single float
|
||||
ctx.mem_write_i32(sp as u32, aligned as i32);
|
||||
@@ -7757,7 +7847,7 @@ impl<R: Runtime> ForthVM<R> {
|
||||
// DFALIGNED is the same as FALIGNED (8-byte alignment)
|
||||
{
|
||||
let func: HostFn = Box::new(move |ctx: &mut dyn HostAccess| {
|
||||
let sp = ctx.get_dsp();
|
||||
let sp = host_need(ctx, 1)?;
|
||||
let addr = ctx.mem_read_i32(sp as u32) as u32;
|
||||
let aligned = (addr + 7) & !7;
|
||||
ctx.mem_write_i32(sp as u32, aligned as i32);
|
||||
@@ -9549,6 +9639,100 @@ mod tests {
|
||||
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) --
|
||||
|
||||
#[test]
|
||||
@@ -10732,6 +10916,111 @@ mod tests {
|
||||
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]
|
||||
fn optimizer_doesnt_break_basic_arithmetic() {
|
||||
assert_eq!(eval_stack("5 3 +"), vec![8]);
|
||||
|
||||
@@ -350,6 +350,16 @@ pub const WORD_DOCS: &[(&str, &str, &str)] = &[
|
||||
"Convert digits, accumulating into ud.",
|
||||
),
|
||||
("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."),
|
||||
("DECIMAL", "( -- )", "Set BASE to ten."),
|
||||
// -- Core: strings --
|
||||
|
||||
@@ -453,6 +453,21 @@ fn programs() -> Vec<Program> {
|
||||
expected: "99 \n",
|
||||
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 --
|
||||
Program {
|
||||
name: "s-quote-type",
|
||||
|
||||
@@ -12,7 +12,7 @@ workspace = true
|
||||
crate-type = ["cdylib", "rlib"]
|
||||
|
||||
[dependencies]
|
||||
wafer-core = { path = "../core", version = "0.2.0", default-features = false, features = ["crypto"] }
|
||||
wafer-core = { path = "../core", version = "0.2.3", default-features = false, features = ["crypto"] }
|
||||
wasm-bindgen = "0.2"
|
||||
js-sys = "0.3"
|
||||
send_wrapper = { workspace = true }
|
||||
|
||||
@@ -18,11 +18,11 @@ confidence-threshold = 0.8
|
||||
[bans]
|
||||
multiple-versions = "deny"
|
||||
wildcards = "deny"
|
||||
# Transitive duplicates from wasmtime v31 -- will resolve when upgrading
|
||||
# Transitive duplicates from wasmtime v47 dependencies
|
||||
skip = [
|
||||
"getrandom",
|
||||
"syn",
|
||||
"hashbrown",
|
||||
"r-efi",
|
||||
"thiserror",
|
||||
"thiserror-impl",
|
||||
"wasm-encoder",
|
||||
|
||||
Reference in New Issue
Block a user