Implement Double-Number and String word sets, fix memory panics

Double-Number (19 words): D+ D- DNEGATE DABS D2* D2/ D0= D0< D= D< DU<
  DMAX DMIN D>S M+ M*/ D. D.R 2ROT 2CONSTANT 2VARIABLE 2VALUE 2LITERAL
  Double-number literal parsing (tokens ending with '.')
String (5 words): COMPARE SEARCH /STRING BLANK -TRAILING SLITERAL
Fix all memory access panics with bounds checking throughout host functions.

8 word sets at 100%: Core, Core Ext, Exception, Double, String,
  Search-Order, Memory-Allocation, Programming-Tools
This commit is contained in:
2026-03-31 14:43:30 +02:00
parent 193ad7ec5a
commit dd389c6a3d
7 changed files with 1590 additions and 165 deletions
+25 -25
View File
@@ -100,20 +100,20 @@ tests/ Forth 2012 compliance suite (gerryjackson/forth2012-test-suite sub
### Core (Forth 2012 Section 6.1) -- In Progress ### Core (Forth 2012 Section 6.1) -- In Progress
| Category | Words | | Category | Words |
| ------------ | ---------------------------------------------------------------------------------------------------- | | ------------ | --------------------------------------------------------------------------------------------------------------- |
| Stack | `DUP DROP SWAP OVER ROT NIP TUCK 2DUP 2DROP 2SWAP 2OVER ?DUP PICK DEPTH` | | Stack | `DUP DROP SWAP OVER ROT NIP TUCK 2DUP 2DROP 2SWAP 2OVER ?DUP PICK DEPTH` |
| Arithmetic | `+ - * / MOD /MOD NEGATE ABS MIN MAX 1+ 1- 2* 2/ */ */MOD M* UM* UM/MOD FM/MOD SM/REM S>D <# # #S #> HOLD SIGN` | | Arithmetic | `+ - * / MOD /MOD NEGATE ABS MIN MAX 1+ 1- 2* 2/ */ */MOD M* UM* UM/MOD FM/MOD SM/REM S>D <# # #S #> HOLD SIGN` |
| Comparison | `= <> < > U< 0= 0< 0<> 0> WITHIN` | | Comparison | `= <> < > U< 0= 0< 0<> 0> WITHIN` |
| Logic | `AND OR XOR INVERT LSHIFT RSHIFT` | | Logic | `AND OR XOR INVERT LSHIFT RSHIFT` |
| Memory | `@ ! C@ C! +! 2@ 2! HERE ALLOT , C, CELLS CELL+ CHARS CHAR+ ALIGNED ALIGN MOVE FILL CMOVE CMOVE>` | | Memory | `@ ! C@ C! +! 2@ 2! HERE ALLOT , C, CELLS CELL+ CHARS CHAR+ ALIGNED ALIGN MOVE FILL CMOVE CMOVE>` |
| Control | `IF ELSE THEN DO LOOP +LOOP I J UNLOOP LEAVE BEGIN UNTIL WHILE REPEAT RECURSE EXIT` | | Control | `IF ELSE THEN DO LOOP +LOOP I J UNLOOP LEAVE BEGIN UNTIL WHILE REPEAT RECURSE EXIT` |
| Defining | `: ; VARIABLE CONSTANT CREATE DOES> IMMEDIATE` | | Defining | `: ; VARIABLE CONSTANT CREATE DOES> IMMEDIATE` |
| I/O | `. U. .S CR EMIT SPACE SPACES TYPE ." S" ACCEPT` | | I/O | `. U. .S CR EMIT SPACE SPACES TYPE ." S" ACCEPT` |
| Return stack | `>R R> R@` | | Return stack | `>R R> R@` |
| System | `EXECUTE ' CHAR [CHAR] ['] DECIMAL HEX BASE STATE >IN >BODY ENVIRONMENT? SOURCE ABORT TRUE FALSE BL` | | System | `EXECUTE ' CHAR [CHAR] ['] DECIMAL HEX BASE STATE >IN >BODY ENVIRONMENT? SOURCE ABORT TRUE FALSE BL` |
| Compiler | `LITERAL POSTPONE [ ] EVALUATE ABORT"` | | Compiler | `LITERAL POSTPONE [ ] EVALUATE ABORT"` |
| Parsing | `WORD FIND COUNT >NUMBER` | | Parsing | `WORD FIND COUNT >NUMBER` |
### Not Yet Implemented ### Not Yet Implemented
@@ -123,21 +123,21 @@ All Core and Core Extension words implemented. Exception word set (CATCH/THROW)
Targeting 100% Forth 2012 compliance via [Gerry Jackson's test suite](https://github.com/gerryjackson/forth2012-test-suite). Targeting 100% Forth 2012 compliance via [Gerry Jackson's test suite](https://github.com/gerryjackson/forth2012-test-suite).
| Word Set | Status | | Word Set | Status |
| ------------------ | ------------------ | | ------------------ | --------------------------------- |
| Core | **100%** (0 errors on test suite) | | Core | **100%** (0 errors on test suite) |
| Core Extensions | **100%** (0 errors on test suite) | | Core Extensions | **100%** (0 errors on test suite) |
| Double-Number | Pending | | Double-Number | Pending |
| Exception | **100%** (0 errors on test suite) | | Exception | **100%** (0 errors on test suite) |
| Facility | Pending | | Facility | Pending |
| File-Access | Pending | | File-Access | Pending |
| Floating-Point | Pending | | Floating-Point | Pending |
| Locals | Pending | | Locals | Pending |
| Memory-Allocation | Pending | | Memory-Allocation | Pending |
| Programming-Tools | Pending | | Programming-Tools | Pending |
| Search-Order | Pending | | Search-Order | Pending |
| String | Pending | | String | Pending |
| Extended-Character | Pending | | Extended-Character | Pending |
## License ## License
+4 -4
View File
@@ -41,14 +41,14 @@ const TYPE_I32: u32 = 1; // (i32) -> ()
const EMIT_FUNC: u32 = 0; const EMIT_FUNC: u32 = 0;
const WORD_FUNC: u32 = 1; const WORD_FUNC: u32 = 1;
/// Natural-alignment MemArg for 4-byte i32 operations. /// Natural-alignment `MemArg` for 4-byte i32 operations.
const MEM4: MemArg = MemArg { const MEM4: MemArg = MemArg {
offset: 0, offset: 0,
align: 2, // 2^2 = 4 align: 2, // 2^2 = 4
memory_index: MEMORY_INDEX, memory_index: MEMORY_INDEX,
}; };
/// MemArg for single-byte operations. /// `MemArg` for single-byte operations.
const MEM1: MemArg = MemArg { const MEM1: MemArg = MemArg {
offset: 0, offset: 0,
align: 0, // 2^0 = 1 align: 0, // 2^0 = 1
@@ -81,7 +81,7 @@ pub struct CompiledModule {
// Instruction-level helpers (free functions that take &mut Function) // Instruction-level helpers (free functions that take &mut Function)
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
/// Decrement `$dsp` by CELL_SIZE. /// Decrement `$dsp` by `CELL_SIZE`.
fn dsp_dec(f: &mut Function) { fn dsp_dec(f: &mut Function) {
f.instruction(&Instruction::GlobalGet(DSP)) f.instruction(&Instruction::GlobalGet(DSP))
.instruction(&Instruction::I32Const(CELL_SIZE as i32)) .instruction(&Instruction::I32Const(CELL_SIZE as i32))
@@ -89,7 +89,7 @@ fn dsp_dec(f: &mut Function) {
.instruction(&Instruction::GlobalSet(DSP)); .instruction(&Instruction::GlobalSet(DSP));
} }
/// Increment `$dsp` by CELL_SIZE. /// Increment `$dsp` by `CELL_SIZE`.
fn dsp_inc(f: &mut Function) { fn dsp_inc(f: &mut Function) {
f.instruction(&Instruction::GlobalGet(DSP)) f.instruction(&Instruction::GlobalGet(DSP))
.instruction(&Instruction::I32Const(CELL_SIZE as i32)) .instruction(&Instruction::I32Const(CELL_SIZE as i32))
+5 -5
View File
@@ -5,7 +5,7 @@
//! - Flags + name length (1 byte) //! - Flags + name length (1 byte)
//! - Name string (N bytes, padded to cell alignment) //! - Name string (N bytes, padded to cell alignment)
//! - Code field: function table index (4 bytes) //! - Code field: function table index (4 bytes)
//! - Parameter field: data for CREATEd words, DOES> action, etc. //! - Parameter field: data for `CREATEd` words, DOES> action, etc.
use crate::error::{WaferError, WaferResult}; use crate::error::{WaferError, WaferResult};
use crate::memory::{DICTIONARY_BASE, INITIAL_PAGES, PAGE_SIZE}; use crate::memory::{DICTIONARY_BASE, INITIAL_PAGES, PAGE_SIZE};
@@ -57,7 +57,7 @@ impl Dictionary {
} }
/// Create a new dictionary entry (like Forth's CREATE). /// Create a new dictionary entry (like Forth's CREATE).
/// Returns the WordId (function table index) assigned to this word. /// Returns the `WordId` (function table index) assigned to this word.
/// The word starts HIDDEN (will be revealed when compilation completes). /// The word starts HIDDEN (will be revealed when compilation completes).
pub fn create(&mut self, name: &str, immediate: bool) -> WaferResult<WordId> { pub fn create(&mut self, name: &str, immediate: bool) -> WaferResult<WordId> {
let name_upper = name.to_ascii_uppercase(); let name_upper = name.to_ascii_uppercase();
@@ -139,7 +139,7 @@ impl Dictionary {
} }
} }
/// Look up a word by name. Returns (word_address, word_id, is_immediate). /// Look up a word by name. Returns (`word_address`, `word_id`, `is_immediate`).
/// Searches from LATEST backward through the linked list. /// Searches from LATEST backward through the linked list.
/// Skips HIDDEN words. /// Skips HIDDEN words.
pub fn find(&self, name: &str) -> Option<(u32, WordId, bool)> { pub fn find(&self, name: &str) -> Option<(u32, WordId, bool)> {
@@ -347,7 +347,7 @@ impl Dictionary {
} }
/// Write a u32 in little-endian without bounds checking. /// Write a u32 in little-endian without bounds checking.
/// Caller must ensure addr + 4 <= memory.len(). /// Caller must ensure addr + 4 <= `memory.len()`.
fn write_u32_unchecked(&mut self, addr: u32, value: u32) { fn write_u32_unchecked(&mut self, addr: u32, value: u32) {
let a = addr as usize; let a = addr as usize;
let bytes = value.to_le_bytes(); let bytes = value.to_le_bytes();
@@ -355,7 +355,7 @@ impl Dictionary {
} }
/// Read a u32 in little-endian without bounds checking. /// Read a u32 in little-endian without bounds checking.
/// Caller must ensure addr + 4 <= memory.len(). /// Caller must ensure addr + 4 <= `memory.len()`.
fn read_u32_unchecked(&self, addr: u32) -> u32 { fn read_u32_unchecked(&self, addr: u32) -> u32 {
let a = addr as usize; let a = addr as usize;
u32::from_le_bytes([ u32::from_le_bytes([
+1 -1
View File
@@ -93,7 +93,7 @@ pub enum IrOp {
test: Vec<IrOp>, test: Vec<IrOp>,
body: Vec<IrOp>, body: Vec<IrOp>,
}, },
/// BEGIN test1 WHILE test2 WHILE body REPEAT after_repeat ELSE else_body THEN /// BEGIN test1 WHILE test2 WHILE body REPEAT `after_repeat` ELSE `else_body` THEN
/// ///
/// Two nested WHILEs in a single BEGIN loop. When the first WHILE fails, /// Two nested WHILEs in a single BEGIN loop. When the first WHILE fails,
/// control goes to `else_body`. When the second WHILE fails, control goes /// control goes to `else_body`. When the second WHILE fails, control goes
+1536 -125
View File
File diff suppressed because it is too large Load Diff
+15 -1
View File
@@ -25,8 +25,22 @@ allow = [
confidence-threshold = 0.8 confidence-threshold = 0.8
[bans] [bans]
multiple-versions = "warn" multiple-versions = "deny"
wildcards = "deny" wildcards = "deny"
# Transitive duplicates from wasmtime v31 -- will resolve when upgrading
skip = [
"getrandom",
"hashbrown",
"linux-raw-sys",
"object",
"rustix",
"thiserror",
"thiserror-impl",
"wasm-encoder",
"wasmparser",
"wast",
"windows-sys",
]
[sources] [sources]
unknown-registry = "deny" unknown-registry = "deny"
+4 -4
View File
@@ -62,7 +62,7 @@ After this, the dictionary contains all built-in words and the function table ha
### 3. REPL loop ### 3. REPL loop
The CLI enters a `rustyline` loop. The prompt is `> ` in interpret mode and ` ] ` in compile mode. Each line is passed to `vm.evaluate()`, which feeds it to the outer interpreter. The CLI enters a `rustyline` loop. The prompt is `>` in interpret mode and `]` in compile mode. Each line is passed to `vm.evaluate()`, which feeds it to the outer interpreter.
## Memory Layout ## Memory Layout
@@ -141,7 +141,7 @@ The colon handler in `interpret_token()`:
2. `dictionary.create("square", false)` -- creates a new HIDDEN entry, returns `WordId(N)` where N is the next function table index 2. `dictionary.create("square", false)` -- creates a new HIDDEN entry, returns `WordId(N)` where N is the next function table index
3. Clears `compiling_ir` (the IR accumulator) 3. Clears `compiling_ir` (the IR accumulator)
4. Sets `state = -1` (compile mode) 4. Sets `state = -1` (compile mode)
5. Prompt changes to ` ] ` 5. Prompt changes to `]`
#### Token: `dup` (in compile mode) #### Token: `dup` (in compile mode)
@@ -221,13 +221,13 @@ table.set(N, exported_fn) Install in function table
**Step 5 -- Return to interpret mode:** **Step 5 -- Return to interpret mode:**
`state = 0`, prompt returns to `> `. `state = 0`, prompt returns to `>`.
### What happens when you then type `7 square .` ### What happens when you then type `7 square .`
- `7` -- pushed to data stack (same as before) - `7` -- pushed to data stack (same as before)
- `square` -- `dictionary.find("SQUARE")` returns WordId(N), `execute_word(N)` calls the compiled function, which in turn calls DUP and * via `call_indirect`, leaving 49 on the stack - `square` -- `dictionary.find("SQUARE")` returns WordId(N), `execute_word(N)` calls the compiled function, which in turn calls DUP and * via `call_indirect`, leaving 49 on the stack
- `.` -- pops 49, prints `49 ` - `.` -- pops 49, prints `49`
Output: `49 ok` Output: `49 ok`