Compare commits
42 Commits
d1a7d55051
...
v0.2.7
| Author | SHA1 | Date | |
|---|---|---|---|
| 645c2dadd7 | |||
| 3bb613ece0 | |||
| b8dcc021a2 | |||
| fc34bd9b24 | |||
| e6c10a6fa1 | |||
| e110ca9516 | |||
| 8e2fd0d7d4 | |||
| 69309006a2 | |||
| 9b10723a95 | |||
| 15f8005b6d | |||
| 4769987b20 | |||
| d55a27873e | |||
| 645b00d6e8 | |||
| 9efb92ddc8 | |||
| 706c73ce2a | |||
| a89d7ca704 | |||
| 20b8754e27 | |||
| 17852ed459 | |||
| e6eabb098d | |||
| f8da87187f | |||
| 0645734d94 | |||
| f83c8f25e4 | |||
| 9b1cc0cace | |||
| dc6e0d45e1 | |||
| cda296aab5 | |||
| e31407ab58 | |||
| 2910884b83 | |||
| f584066a0a | |||
| 4980648982 | |||
| 31dc6c6397 | |||
| 35b78193fd | |||
| d5acdc0e7b | |||
| a66435c93c | |||
| bb217714ac | |||
| 67448caa9c | |||
| bcccdfb49d | |||
| be5dff243f | |||
| 49582f7e86 | |||
| 1a8f27b5bd | |||
| 6771f5d46b | |||
| 64f4b1e857 | |||
| f1752ededa |
@@ -3,3 +3,6 @@
|
|||||||
*.swp
|
*.swp
|
||||||
.DS_Store
|
.DS_Store
|
||||||
*.bk
|
*.bk
|
||||||
|
|
||||||
|
# Local planning notes — never tracked
|
||||||
|
/plans/
|
||||||
|
|||||||
+346
@@ -0,0 +1,346 @@
|
|||||||
|
# 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.7] - 2026-08-09
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- **A typed calling convention for words with a known stack effect.** Such a
|
||||||
|
word now compiles to two entry points: a fast one whose signature is
|
||||||
|
`(i32 x p) -> (i32 x q)`, carrying its stack items as WASM values, and the
|
||||||
|
usual `( -- )` wrapper that moves those items on and off the memory data
|
||||||
|
stack. The wrapper keeps the function-table slot, so `EXECUTE`, the outer
|
||||||
|
interpreter, host words and `CATCH` see exactly the ABI they saw before;
|
||||||
|
only direct calls inside a module take the fast entry.
|
||||||
|
|
||||||
|
This is what the SwiftForth gap was made of. sf64 keeps TOS in `RBX` and
|
||||||
|
the stack pointer in `RBP`, and both survive a `CALL` untouched, so its
|
||||||
|
`FIB` is 16 instructions and ~7 memory touches per node. WAFER kept the
|
||||||
|
whole stack in linear memory and flushed its cached `$dsp` to an imported
|
||||||
|
global before every call: ~36 memory touches per node. The stack simulator
|
||||||
|
that already promoted loop and `IF` bodies into WASM locals refused any
|
||||||
|
body containing a call or an `EXIT` -- exactly the words where the
|
||||||
|
convention cost the most. It now handles both.
|
||||||
|
|
||||||
|
Fibonacci(25) goes from 1035 to 366 µs, 4.3x slower than `sf64` to 1.2x.
|
||||||
|
Loop-heavy benchmarks are unchanged by this entry — see the region
|
||||||
|
promotion below for those. Words that keep the memory convention: anything
|
||||||
|
using `SP@`, `DEPTH`, `EXECUTE`, `>R`/`R>`, floats or locals; anything
|
||||||
|
calling a word that is itself untyped, which in the JIT path means every
|
||||||
|
call except `RECURSE`; mutually recursive words; and words whose effect is
|
||||||
|
not static -- branches that disagree on depth, `EXIT` at the wrong depth,
|
||||||
|
a non-neutral loop body, or a recursion that grows the stack per level.
|
||||||
|
|
||||||
|
`CONSOLIDATE` extends this across words, since it puts them all in one
|
||||||
|
module: the effects are solved to a fixpoint from the leaves outward, and
|
||||||
|
105 of 187 words in a booted dictionary end up typed.
|
||||||
|
|
||||||
|
Stack guards get cheap as a side effect -- they hang off the memory-stack
|
||||||
|
push/pop choke points, and a typed word barely has any. The default
|
||||||
|
guards-on configuration that the REPL and the web build use went from 1631
|
||||||
|
to 365 µs on the same benchmark.
|
||||||
|
|
||||||
|
`WAFER_TYPED_CALLS=0` falls back to the memory-stack convention.
|
||||||
|
|
||||||
|
- **Promotion is now per region, not per word.** Stack-to-local promotion
|
||||||
|
used to be all-or-nothing: a single `.`, `CR`, `>R` or host call
|
||||||
|
anywhere in a definition put the _entire_ body on the memory data
|
||||||
|
stack, hot loops included. The stack simulator now runs over each
|
||||||
|
stretch of a word that can live in WASM locals, loading what the
|
||||||
|
region reads and writing back what it leaves, with the rest of the
|
||||||
|
word unchanged around it.
|
||||||
|
|
||||||
|
The cliff this removes was steep. The same loop, same build:
|
||||||
|
|
||||||
|
| `: L1 0 5000000 0 DO 1+ LOOP DROP ;` reached as | µs | ns/iter |
|
||||||
|
| ----------------------------------------------- | ----- | ------- |
|
||||||
|
| its own word | 1571 | 0.31 |
|
||||||
|
| inlined into a caller with a `.` in it (before) | 11100 | 2.22 |
|
||||||
|
| the same, after this change | 1572 | 0.31 |
|
||||||
|
|
||||||
|
7x, for one `i32.add`: on the memory path the accumulator is stored to
|
||||||
|
linear memory and reloaded next iteration, so the loop-carried
|
||||||
|
dependency runs through store-to-load forwarding instead of a
|
||||||
|
register.
|
||||||
|
|
||||||
|
A region may only use `I` / `J` when the DO loops naming them are
|
||||||
|
inside the region, since the simulator resolves them against its own
|
||||||
|
loop stack. Straight-line regions have to be at least three operations
|
||||||
|
to be worth the load and store either side; a loop always is.
|
||||||
|
|
||||||
|
- **The inliner no longer drags a loop onto the memory stack.** It
|
||||||
|
inlined any callee of eight IR operations or fewer, so a small
|
||||||
|
loop-bearing word inlined into a caller that can never be promoted
|
||||||
|
lost its registers -- an optimisation pass applying the 7x
|
||||||
|
pessimisation above. Loop-bearing callees now stay put in that case:
|
||||||
|
one call is far cheaper than a loop's worth of memory traffic.
|
||||||
|
Straight-line words still inline everywhere.
|
||||||
|
|
||||||
|
- **`BEGIN` loops promote as well.** `BEGIN..UNTIL`, `BEGIN..AGAIN` and
|
||||||
|
`BEGIN..WHILE..REPEAT` were rejected outright by the eligibility check,
|
||||||
|
so any word built on the idiomatic Forth loop kept the memory data
|
||||||
|
stack no matter how hot it was. They are promoted now when the
|
||||||
|
construct is stack-neutral: `UNTIL` consumes exactly the flag its body
|
||||||
|
leaves, `AGAIN`'s body is neutral, and for `WHILE..REPEAT` the test and
|
||||||
|
the body balance separately -- `WHILE` leaves the loop between the two,
|
||||||
|
so a net that only added up over the pair would give the two exits
|
||||||
|
different stack shapes. Bodies containing an `EXIT` stay out, the same
|
||||||
|
rule `DO`/`LOOP` follows. `BEGIN..WHILE..WHILE..REPEAT` is still
|
||||||
|
excluded.
|
||||||
|
|
||||||
|
GCD 994 -> 540 µs, Collatz 428 -> 185.
|
||||||
|
|
||||||
|
Together these four entries put four of the five cross-engine
|
||||||
|
benchmarks past SwiftForth `sf64`: Factorial 0.29x, Collatz 0.30x,
|
||||||
|
NestedLoops 0.27x, GCD 0.67x. Fibonacci stays at 1.24x, being pure
|
||||||
|
call overhead with no loop to promote.
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- **A promoted loop or `IF` whose branch permutes the stack lost a value.**
|
||||||
|
At the bottom of a promoted loop the body's results are copied back into
|
||||||
|
the loop-top locals, and the join after a promoted `IF` copies one
|
||||||
|
branch's locals into the other's. Both did it one slot at a time in index
|
||||||
|
order, which is wrong as soon as a destination is also a later source:
|
||||||
|
`: C 3 4 2 0 DO SWAP LOOP . . ;` printed `4 4` where gforth and
|
||||||
|
SwiftForth print `4 3`, and `2 0 DO ROT LOOP` over three cells printed
|
||||||
|
`3 2 3` instead of `2 1 3`. The copies are now ordered so every source is
|
||||||
|
read before it is overwritten, with one scratch local to break a cycle.
|
||||||
|
Present since stack-to-local promotion was introduced; reachable from
|
||||||
|
any `DO` loop or `IF` whose body reorders cells it did not create.
|
||||||
|
|
||||||
|
- The Forth 2012 Core suite now also runs against consolidated code
|
||||||
|
(`compliance_core_after_consolidate`). `CONSOLIDATE` had no correctness
|
||||||
|
test at all before -- only benchmarks.
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- **Three cross-engine benchmarks were too small to be measured.** GCD ran
|
||||||
|
in 14 µs, Factorial in 49 and NestedLoops in 51, where per-run scatter is
|
||||||
|
a good fraction of the total and fixed per-invocation costs in the other
|
||||||
|
engines dominate. Scaled to Factorial x100K, GCD-bench(20K) and
|
||||||
|
NestedLoops(50)x1K, all now around 0.5-1 ms.
|
||||||
|
|
||||||
|
This changed a result rather than just steadying it: GCD looked like a
|
||||||
|
win at 0.42x of `sf64` and was in fact a loss at 1.17x. That is what
|
||||||
|
pointed at `BEGIN` loops as the remaining gap -- GCD is the one benchmark
|
||||||
|
whose loop is a `BEGIN ... WHILE ... REPEAT` -- and with those promoted it
|
||||||
|
now reads 0.67x. The regression limits, which had drifted to 3-6x looser
|
||||||
|
than the measurements they guard, were retightened to ~45% above the
|
||||||
|
current ratios.
|
||||||
|
|
||||||
|
## [0.2.6] - 2026-08-07
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- **An uncaught `ABORT` no longer prints anything.** It used to report
|
||||||
|
`ABORT (throw -1)`, but the standard defines `ABORT` as "empty the data
|
||||||
|
stack and perform the function of `QUIT`", and `QUIT` displays no
|
||||||
|
message. gforth and SwiftForth are both silent here. `CATCH` still
|
||||||
|
reports -1 as before, and `ABORT"` still prints its text — that is a
|
||||||
|
different word with a different code (-2).
|
||||||
|
- **Compile-only words used in interpretation state name the condition.**
|
||||||
|
`ABORT"`, `IF`, `THEN`, `LOOP`, `LITERAL`, `RECURSE` and the rest of
|
||||||
|
the compile-time constructs claimed to be an `unknown word`, which is
|
||||||
|
actively misleading for a word the system obviously knows. They now
|
||||||
|
report `interpreting a compile-only word: <name> (throw -14)`, the
|
||||||
|
standard condition both reference engines give. A genuine typo still
|
||||||
|
reports `unknown word`.
|
||||||
|
|
||||||
|
## [0.2.5] - 2026-08-06
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- **`QUIT`** ( -- ) ( R: i\*x -- ), the CORE word that was missing: empty
|
||||||
|
the return stack, enter interpretation state, hand the input source
|
||||||
|
back to the user input device and return to the interpreter without a
|
||||||
|
message. The data stack is deliberately left alone — that is the whole
|
||||||
|
difference to `ABORT`, which the standard defines as "empty the data
|
||||||
|
stack, then `QUIT`". It unwinds through nested `EVALUATE` and
|
||||||
|
`INCLUDE`, abandoning them, and `SOURCE-ID` is restored to 0.
|
||||||
|
|
||||||
|
`CATCH` does **not** report it: `QUIT` rides throw code -56, which the
|
||||||
|
interpreter treats as a return to the prompt rather than an exception.
|
||||||
|
Both behaviours were checked against gforth 0.7.3 and SwiftForth
|
||||||
|
`sf64`, which agree — `1 2 ' QUIT CATCH .` prints nothing and leaves
|
||||||
|
`1 2` on the stack in all three engines.
|
||||||
|
|
||||||
|
The gap had gone unnoticed because the Forth 2012 test suite skips it
|
||||||
|
by its own admission ("I HAVEN'T FIGURED OUT HOW TO TEST KEY, QUIT,
|
||||||
|
ABORT, OR ABORT\""), and because `HELP`'s coverage lint compares the
|
||||||
|
dictionary against the docs — a word absent from both looks complete.
|
||||||
|
`docs/wafer-anki.txt` had been documenting `QUIT` as if it existed.
|
||||||
|
|
||||||
|
Note that `ABORT` was already correct: executing it while a definition
|
||||||
|
is open does clear both stacks and return to interpretation state.
|
||||||
|
Typing `ABORT` (or `QUIT`) into an unfinished definition compiles it
|
||||||
|
rather than running it, exactly as in every other Forth; `[` is the
|
||||||
|
word that gets you out.
|
||||||
|
|
||||||
|
## [0.2.4] - 2026-08-06
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- **Errors from host words in the browser build read like Forth errors
|
||||||
|
again.** A host word signals failure by throwing across the JS
|
||||||
|
boundary, and the browser runtime reported the exception with its
|
||||||
|
`Debug` form, so an empty-stack `RESIZE` came back as
|
||||||
|
`call_func(134) failed: JsValue(Error: Stack underflow ...)` trailed by
|
||||||
|
an engine stack trace. The thrown message is the Forth message, so it
|
||||||
|
is now surfaced verbatim — `Stack underflow`, exactly what the native
|
||||||
|
CLI prints. Exceptions that carry no message keep the call context,
|
||||||
|
since those are genuine runtime faults rather than Forth throws.
|
||||||
|
`CATCH` was never affected: it reads the throw code from its own
|
||||||
|
channel, not from the message.
|
||||||
|
|
||||||
|
## [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
|
||||||
|
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.7]: https://github.com/ok2/wafer/compare/v0.2.6...v0.2.7
|
||||||
|
[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
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
## What is WAFER?
|
## What is WAFER?
|
||||||
|
|
||||||
WAFER (WebAssembly Forth Engine in Rust) is an optimizing Forth 2012 compiler targeting WebAssembly. Currently a working Forth system with 200+ words, JIT compilation, 12 word sets at 100% compliance, and a full optimization pipeline (peephole, constant folding, inlining, strength reduction, DCE, tail calls, stack-to-local promotion with loop/IF support, self-recursive direct calls, consolidation). Beats gforth on all benchmarks in release mode. Includes a browser-based REPL via wasm-pack.
|
WAFER (WebAssembly Forth Engine in Rust) is an optimizing Forth 2012 compiler targeting WebAssembly. Currently a working Forth system with 200+ words, JIT compilation, 12 word sets at 100% compliance, and a full optimization pipeline (peephole, constant folding, inlining, strength reduction, DCE, tail calls, per-region stack-to-local promotion with DO/BEGIN loop and IF support, self-recursive direct calls, a typed calling convention for words with a known stack effect, consolidation). Beats gforth on all benchmarks in release mode, and SwiftForth `sf64` on four of five. Includes a browser-based REPL via wasm-pack.
|
||||||
|
|
||||||
## Architecture
|
## Architecture
|
||||||
|
|
||||||
@@ -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 431 unit + 1 benchmark + 11 compliance + 9 comparison)
|
- Run `cargo test --workspace` before committing (currently 601 unit + 1 benchmark + 12 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
+308
-631
File diff suppressed because it is too large
Load Diff
+14
-6
@@ -3,7 +3,7 @@ members = ["crates/*"]
|
|||||||
resolver = "2"
|
resolver = "2"
|
||||||
|
|
||||||
[workspace.package]
|
[workspace.package]
|
||||||
version = "0.1.0"
|
version = "0.2.7"
|
||||||
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,21 @@ needless_collect = "warn"
|
|||||||
or_fun_call = "warn"
|
or_fun_call = "warn"
|
||||||
|
|
||||||
[workspace.dependencies]
|
[workspace.dependencies]
|
||||||
wasm-encoder = "0.246"
|
wasm-encoder = "0.255"
|
||||||
wasmparser = "0.246"
|
wasmparser = "0.255"
|
||||||
wasmtime = "43"
|
wasmtime = "47"
|
||||||
anyhow = "1"
|
anyhow = "1"
|
||||||
thiserror = "2"
|
thiserror = "2"
|
||||||
proptest = "1"
|
proptest = "1"
|
||||||
insta = "1"
|
insta = "1"
|
||||||
sha1 = "0.11"
|
sha1 = "0.10"
|
||||||
sha2 = "0.11"
|
sha2 = "0.10"
|
||||||
send_wrapper = "0.6"
|
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
|
||||||
|
|||||||
@@ -43,6 +43,14 @@ bench:
|
|||||||
bench-opts:
|
bench-opts:
|
||||||
cargo test -p wafer-core --test benchmark_report -- --nocapture --ignored
|
cargo test -p wafer-core --test benchmark_report -- --nocapture --ignored
|
||||||
|
|
||||||
|
# Cross-engine performance report: WAFER vs gforth vs SwiftForth (sf64)
|
||||||
|
bench-compare:
|
||||||
|
CARGO_PROFILE_RELEASE_STRIP=none cargo test -p wafer-core --release --test comparison -- --nocapture --ignored performance_report
|
||||||
|
|
||||||
|
# Cross-engine correctness lanes: program corpus vs gforth + sf64 oracles
|
||||||
|
compare-correctness:
|
||||||
|
cargo test -p wafer-core --test comparison -- --nocapture --ignored compare_all_programs
|
||||||
|
|
||||||
# Check dependency licenses and advisories
|
# Check dependency licenses and advisories
|
||||||
deny:
|
deny:
|
||||||
cargo deny check
|
cargo deny check
|
||||||
@@ -57,3 +65,16 @@ ci: fmt clippy deny test
|
|||||||
# Check compilation without running
|
# Check compilation without running
|
||||||
check:
|
check:
|
||||||
cargo check --workspace
|
cargo check --workspace
|
||||||
|
|
||||||
|
# Install the wafer CLI (release build) and bat syntax highlighting.
|
||||||
|
# STRIP=none: Cargo's release default (strip = "debuginfo") emits dylibs that
|
||||||
|
# macOS 27's dyld rejects ("mis-aligned LINKEDIT string pool"), so proc macros
|
||||||
|
# fail to load during the build itself.
|
||||||
|
install: install-syntax
|
||||||
|
CARGO_PROFILE_RELEASE_STRIP=none cargo install --path crates/cli --locked
|
||||||
|
|
||||||
|
# Install bat syntax highlighting for WAFER / Forth
|
||||||
|
install-syntax:
|
||||||
|
mkdir -p ~/.config/bat/syntaxes
|
||||||
|
cp tools/editor-support/bat/WAFER.sublime-syntax ~/.config/bat/syntaxes/
|
||||||
|
bat cache --build
|
||||||
|
|||||||
@@ -7,10 +7,11 @@ An optimizing Forth 2012 compiler targeting WebAssembly. WAFER JIT-compiles each
|
|||||||
## Highlights
|
## Highlights
|
||||||
|
|
||||||
- **200+ words** across 12 Forth 2012 word sets, all at **100% compliance**
|
- **200+ words** across 12 Forth 2012 word sets, all at **100% compliance**
|
||||||
- **Optimizing compiler** with 6 IR passes + stack-to-local promotion (loops + IF) + consolidation
|
- **Optimizing compiler** with 6 IR passes + stack-to-local promotion (per region, so a hot loop keeps its registers even inside a word that does I/O; `DO` and `BEGIN` loops alike) + consolidation
|
||||||
- **Faster than gforth** on all benchmarks in release mode (2-10x faster)
|
- **Faster than gforth** on all benchmarks in release mode (2-10x faster)
|
||||||
- **JIT compilation** — each `:` definition compiles to its own WASM module
|
- **JIT compilation** — each `:` definition compiles to its own WASM module
|
||||||
- **Self-recursive direct calls** — RECURSE compiles to native `call` instead of `call_indirect`
|
- **Self-recursive direct calls** — RECURSE compiles to native `call` instead of `call_indirect`
|
||||||
|
- **Typed calling convention** — a word with a statically known stack effect passes its stack items as WASM values, so a call keeps them in registers instead of round-tripping through memory
|
||||||
- **Consolidation mode** — recompile all words into a single optimized WASM module
|
- **Consolidation mode** — recompile all words into a single optimized WASM module
|
||||||
- **Interactive REPL** with line editing (rustyline)
|
- **Interactive REPL** with line editing (rustyline)
|
||||||
- **Browser REPL** — runs entirely in the browser via wasm-pack + js-sys
|
- **Browser REPL** — runs entirely in the browser via wasm-pack + js-sys
|
||||||
@@ -79,23 +80,36 @@ git submodule update --init
|
|||||||
|
|
||||||
## Performance
|
## Performance
|
||||||
|
|
||||||
WAFER beats gforth (the GNU Forth reference implementation) on all benchmarks in release mode:
|
WAFER beats gforth (the GNU Forth reference implementation) on all benchmarks in release mode, and is within
|
||||||
|
reach of SwiftForth `sf64`, which compiles to native code:
|
||||||
|
|
||||||
```
|
```
|
||||||
Benchmark WAFER CONSOL gforth WAFER/gf
|
Benchmark WAFER CONSOL gforth sf64 WAFER/gf WAFER/sf
|
||||||
Fibonacci(25) 1629 1535 3422 0.45x
|
Fibonacci(25) 356 361 3389 287 0.11x 1.24x
|
||||||
Factorial(12)x10K 340 339 638 0.53x
|
Factorial(12)x100K 479 495 6249 1650 0.08x 0.29x
|
||||||
GCD-bench(500) 18 15 30 0.50x
|
GCD-bench(20K) 540 559 1801 801 0.30x 0.67x
|
||||||
NestedLoops(50) 84 73 720 0.10x
|
NestedLoops(50)x1K 509 501 7023 1887 0.07x 0.27x
|
||||||
Collatz(2K) 1212 1202 3914 0.31x
|
Collatz(2K) 185 213 3873 610 0.05x 0.30x
|
||||||
```
|
```
|
||||||
|
|
||||||
Times in microseconds. WAFER/gf < 1.0 means WAFER is faster. CONSOL = after `CONSOLIDATE`.
|
Times in microseconds. WAFER/gf < 1.0 means WAFER is faster. CONSOL = after `CONSOLIDATE`.
|
||||||
|
|
||||||
|
Two caveats on the `sf64` column. The SwiftForth build here is x86-64 running under Rosetta 2
|
||||||
|
while WAFER and gforth are native arm64, so it is a native-vs-emulated comparison; and sf64
|
||||||
|
uses 64-bit cells to WAFER's 32-bit. WAFER is ahead on the four loop-heavy benchmarks and
|
||||||
|
behind on Fibonacci, which is one call per node with no loop to promote.
|
||||||
|
|
||||||
|
A word whose stack effect is statically known gets a **typed entry point**: its stack items travel in and out
|
||||||
|
as WASM values instead of through the memory data stack, so cranelift keeps them in registers across a call
|
||||||
|
the way a native Forth keeps TOS in one. The word also keeps a `( -- )` wrapper, which is what the function
|
||||||
|
table, `EXECUTE` and the outer interpreter reach, so nothing about the memory ABI changes from the outside.
|
||||||
|
Call-heavy code is what this pays for -- Fibonacci went from 4.3x slower than `sf64` to 1.2x. Set
|
||||||
|
`WAFER_TYPED_CALLS=0` to fall back to the memory-stack convention.
|
||||||
|
|
||||||
## Testing
|
## Testing
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# All tests (~450 currently passing)
|
# All tests (~628 currently passing)
|
||||||
cargo test --workspace
|
cargo test --workspace
|
||||||
|
|
||||||
# Forth 2012 compliance suite
|
# Forth 2012 compliance suite
|
||||||
@@ -128,7 +142,7 @@ Forth Source -> Outer Interpreter -> IR -> [Optimize] -> WASM Codegen (wasm-enco
|
|||||||
- `WebRuntime` — browser WebAssembly API via js-sys, for the browser REPL
|
- `WebRuntime` — browser WebAssembly API via js-sys, for the browser REPL
|
||||||
- **Subroutine threading** via WASM function tables (`call_indirect` for cross-word, direct `call` for self-recursion)
|
- **Subroutine threading** via WASM function tables (`call_indirect` for cross-word, direct `call` for self-recursion)
|
||||||
- **JIT mode**: each new word compiles to a separate WASM module linked to shared memory/globals/table
|
- **JIT mode**: each new word compiles to a separate WASM module linked to shared memory/globals/table
|
||||||
- **IR-based pipeline** with 6 optimization passes (peephole, constant folding, strength reduction, DCE, tail call detection, inlining) plus stack-to-local promotion (with loop and IF/ELSE support), DO/LOOP index locals, and consolidation
|
- **IR-based pipeline** with 6 optimization passes (peephole, constant folding, strength reduction, DCE, tail call detection, inlining) plus per-region stack-to-local promotion (DO and BEGIN loops, IF/ELSE), DO/LOOP index locals, typed entry points for words with a known stack effect, and consolidation
|
||||||
- **Dictionary**: linked-list word headers in simulated linear memory
|
- **Dictionary**: linked-list word headers in simulated linear memory
|
||||||
|
|
||||||
## Project Structure
|
## Project Structure
|
||||||
@@ -185,6 +199,7 @@ Over 200 words are implemented across the following categories:
|
|||||||
| Strings | `COMPARE SEARCH SLITERAL REPLACES SUBSTITUTE UNESCAPE` |
|
| Strings | `COMPARE SEARCH SLITERAL REPLACES SUBSTITUTE UNESCAPE` |
|
||||||
| Floating-Pt | `F+ F- F* F/ FABS FNEGATE FSQRT FSIN FCOS FTAN FEXP FLOG FMIN FMAX` and 55+ more |
|
| Floating-Pt | `F+ F- F* F/ FABS FNEGATE FSQRT FSIN FCOS FTAN FEXP FLOG FMIN FMAX` and 55+ more |
|
||||||
| Case | `CASE OF ENDOF ENDCASE` |
|
| Case | `CASE OF ENDOF ENDCASE` |
|
||||||
|
| Tools | `WORDS SEE SEE-IR HELP INCLUDE INCLUDED .S F.S ? DUMP MARKER REMEMBER EMPTY GILD BYE` |
|
||||||
|
|
||||||
## Web REPL
|
## Web REPL
|
||||||
|
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ license.workspace = true
|
|||||||
workspace = true
|
workspace = true
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
wafer-core = { path = "../core", version = "0.1.0" }
|
wafer-core = { path = "../core", version = "0.2.7" }
|
||||||
wasmtime = { workspace = true }
|
wasmtime = { workspace = true }
|
||||||
anyhow = { workspace = true }
|
anyhow = { workspace = true }
|
||||||
clap = { version = "4", features = ["derive"] }
|
clap = { version = "4", features = ["derive"] }
|
||||||
|
|||||||
+192
-58
@@ -137,7 +137,9 @@ fn cmd_build(
|
|||||||
) -> anyhow::Result<()> {
|
) -> anyhow::Result<()> {
|
||||||
let source = std::fs::read_to_string(file)?;
|
let source = std::fs::read_to_string(file)?;
|
||||||
|
|
||||||
let mut vm = ForthVM::<NativeRuntime>::new()?;
|
// Exported modules are production artifacts: no stack guards by default
|
||||||
|
let mut vm = ForthVM::<NativeRuntime>::new_with_config(vm_config(false))?;
|
||||||
|
vm.set_source_loader(fs_loader());
|
||||||
vm.set_recording(true);
|
vm.set_recording(true);
|
||||||
vm.evaluate(&source)?;
|
vm.evaluate(&source)?;
|
||||||
|
|
||||||
@@ -260,18 +262,40 @@ fn cmd_run(file: &str) -> anyhow::Result<()> {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// `WaferConfig` for CLI-created VMs. `WAFER_STACK_GUARDS=0|1` overrides
|
||||||
|
/// the per-command default (REPL/file execution on, build off);
|
||||||
|
/// `WAFER_TYPED_CALLS=0` falls back to the memory-stack calling convention.
|
||||||
|
fn vm_config(default_guards: bool) -> wafer_core::config::WaferConfig {
|
||||||
|
let mut cfg = wafer_core::config::WaferConfig::all();
|
||||||
|
cfg.codegen.stack_guards = match std::env::var("WAFER_STACK_GUARDS").ok().as_deref() {
|
||||||
|
Some("0") => false,
|
||||||
|
Some(_) => true,
|
||||||
|
None => default_guards,
|
||||||
|
};
|
||||||
|
cfg.codegen.typed_calls = std::env::var("WAFER_TYPED_CALLS").ok().as_deref() != Some("0");
|
||||||
|
cfg
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Filesystem source loader for INCLUDE/INCLUDED.
|
||||||
|
fn fs_loader() -> Box<dyn Fn(&str) -> anyhow::Result<String> + Send + Sync> {
|
||||||
|
Box::new(|path| Ok(std::fs::read_to_string(path)?))
|
||||||
|
}
|
||||||
|
|
||||||
/// `wafer` (REPL) or `wafer program.fth` (evaluate and exit)
|
/// `wafer` (REPL) or `wafer program.fth` (evaluate and exit)
|
||||||
fn cmd_eval_or_repl(file: Option<&str>) -> anyhow::Result<()> {
|
fn cmd_eval_or_repl(file: Option<&str>) -> anyhow::Result<()> {
|
||||||
let mut vm = ForthVM::<NativeRuntime>::new()?;
|
let mut vm = ForthVM::<NativeRuntime>::new_with_config(vm_config(true))?;
|
||||||
|
vm.set_source_loader(fs_loader());
|
||||||
|
|
||||||
match file {
|
match file {
|
||||||
Some(file) => {
|
Some(file) => {
|
||||||
let source = std::fs::read_to_string(file)?;
|
// Through the include machinery: file:line error context and a
|
||||||
vm.evaluate(&source)?;
|
// base directory for nested INCLUDEs.
|
||||||
|
let result = vm.include(file);
|
||||||
let output = vm.take_output();
|
let output = vm.take_output();
|
||||||
if !output.is_empty() {
|
if !output.is_empty() {
|
||||||
print!("{output}");
|
print!("{output}");
|
||||||
}
|
}
|
||||||
|
result?;
|
||||||
}
|
}
|
||||||
None => {
|
None => {
|
||||||
if !stdin_is_tty() {
|
if !stdin_is_tty() {
|
||||||
@@ -285,66 +309,17 @@ fn cmd_eval_or_repl(file: Option<&str>) -> anyhow::Result<()> {
|
|||||||
if !output.is_empty() {
|
if !output.is_empty() {
|
||||||
print!("{output}");
|
print!("{output}");
|
||||||
}
|
}
|
||||||
|
if vm.bye_requested() {
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
eprintln!("Error: {e}");
|
eprintln!("Error: {e:#}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Interactive REPL
|
run_repl(&mut vm)?;
|
||||||
println!(
|
|
||||||
"WAFER v{} - WebAssembly Forth Engine in Rust",
|
|
||||||
env!("CARGO_PKG_VERSION")
|
|
||||||
);
|
|
||||||
println!("Type BYE to exit.");
|
|
||||||
|
|
||||||
let mut rl = rustyline::DefaultEditor::new()?;
|
|
||||||
loop {
|
|
||||||
let prompt = if vm.is_compiling() { " ] " } else { "> " };
|
|
||||||
match rl.readline(prompt) {
|
|
||||||
Ok(line) => {
|
|
||||||
let trimmed = line.trim();
|
|
||||||
if trimmed.eq_ignore_ascii_case("BYE") {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
let _ = rl.add_history_entry(&line);
|
|
||||||
match vm.evaluate(&line) {
|
|
||||||
Ok(()) => {
|
|
||||||
let output = vm.take_output();
|
|
||||||
// PAGE (form feed) clears the terminal
|
|
||||||
if output.contains('\x0C') {
|
|
||||||
print!("\x1b[2J\x1b[H");
|
|
||||||
}
|
|
||||||
let output = output.replace('\x0C', "");
|
|
||||||
if !vm.is_compiling() {
|
|
||||||
// Move cursor back up to end of input line so
|
|
||||||
// output appears inline, like traditional Forth:
|
|
||||||
// > 2 2 + . 4 ok
|
|
||||||
let col = prompt.len() + line.len() + 1;
|
|
||||||
print!("\x1b[A\x1b[{col}G {output} ok");
|
|
||||||
println!();
|
|
||||||
} else if !output.is_empty() {
|
|
||||||
print!("{output}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Err(e) => {
|
|
||||||
eprintln!("Error: {e}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Err(
|
|
||||||
rustyline::error::ReadlineError::Interrupted
|
|
||||||
| rustyline::error::ReadlineError::Eof,
|
|
||||||
) => {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
Err(e) => {
|
|
||||||
eprintln!("Readline error: {e}");
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -357,3 +332,162 @@ fn stdin_is_tty() -> bool {
|
|||||||
use std::io::IsTerminal;
|
use std::io::IsTerminal;
|
||||||
std::io::stdin().is_terminal()
|
std::io::stdin().is_terminal()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Completes the token under the cursor against the live dictionary.
|
||||||
|
struct WaferHelper {
|
||||||
|
words: Vec<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl rustyline::completion::Completer for WaferHelper {
|
||||||
|
type Candidate = String;
|
||||||
|
|
||||||
|
fn complete(
|
||||||
|
&self,
|
||||||
|
line: &str,
|
||||||
|
pos: usize,
|
||||||
|
_ctx: &rustyline::Context<'_>,
|
||||||
|
) -> rustyline::Result<(usize, Vec<String>)> {
|
||||||
|
let start = line[..pos]
|
||||||
|
.rfind(|c: char| c.is_whitespace())
|
||||||
|
.map_or(0, |i| i + 1);
|
||||||
|
let prefix = line[start..pos].to_ascii_uppercase();
|
||||||
|
let mut matches: Vec<String> = self
|
||||||
|
.words
|
||||||
|
.iter()
|
||||||
|
.filter(|w| w.to_ascii_uppercase().starts_with(&prefix))
|
||||||
|
.cloned()
|
||||||
|
.collect();
|
||||||
|
matches.sort();
|
||||||
|
matches.dedup();
|
||||||
|
Ok((start, matches))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl rustyline::hint::Hinter for WaferHelper {
|
||||||
|
type Hint = String;
|
||||||
|
}
|
||||||
|
impl rustyline::highlight::Highlighter for WaferHelper {}
|
||||||
|
impl rustyline::validate::Validator for WaferHelper {}
|
||||||
|
impl rustyline::Helper for WaferHelper {}
|
||||||
|
|
||||||
|
/// History file: `$WAFER_HISTORY`, else `$XDG_STATE_HOME/wafer/history`,
|
||||||
|
/// else `~/.local/state/wafer/history`.
|
||||||
|
fn history_path() -> Option<std::path::PathBuf> {
|
||||||
|
if let Some(p) = std::env::var_os("WAFER_HISTORY") {
|
||||||
|
return Some(p.into());
|
||||||
|
}
|
||||||
|
let base = std::env::var_os("XDG_STATE_HOME")
|
||||||
|
.map(std::path::PathBuf::from)
|
||||||
|
.or_else(|| {
|
||||||
|
std::env::var_os("HOME").map(|h| std::path::PathBuf::from(h).join(".local/state"))
|
||||||
|
})?;
|
||||||
|
Some(base.join("wafer/history"))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Interactive REPL: line editing, persistent history with prefix search
|
||||||
|
/// on Up/Down, and Tab completion over the live dictionary.
|
||||||
|
fn run_repl(vm: &mut ForthVM<NativeRuntime>) -> anyhow::Result<()> {
|
||||||
|
use rustyline::{Cmd, Editor, EventHandler, KeyCode, KeyEvent, Modifiers};
|
||||||
|
|
||||||
|
println!(
|
||||||
|
"WAFER v{} - WebAssembly Forth Engine in Rust",
|
||||||
|
env!("CARGO_PKG_VERSION")
|
||||||
|
);
|
||||||
|
println!("Type BYE to exit.");
|
||||||
|
|
||||||
|
let config = rustyline::Config::builder()
|
||||||
|
.completion_type(rustyline::CompletionType::List)
|
||||||
|
.history_ignore_dups(true)?
|
||||||
|
.build();
|
||||||
|
let mut rl: Editor<WaferHelper, rustyline::history::DefaultHistory> =
|
||||||
|
Editor::with_config(config)?;
|
||||||
|
rl.set_helper(Some(WaferHelper {
|
||||||
|
words: vm.word_names(),
|
||||||
|
}));
|
||||||
|
// Up/Down recall only entries starting with the typed prefix
|
||||||
|
rl.bind_sequence(
|
||||||
|
KeyEvent(KeyCode::Up, Modifiers::NONE),
|
||||||
|
EventHandler::Simple(Cmd::HistorySearchBackward),
|
||||||
|
);
|
||||||
|
rl.bind_sequence(
|
||||||
|
KeyEvent(KeyCode::Down, Modifiers::NONE),
|
||||||
|
EventHandler::Simple(Cmd::HistorySearchForward),
|
||||||
|
);
|
||||||
|
|
||||||
|
let history = history_path();
|
||||||
|
if let Some(path) = &history {
|
||||||
|
if let Some(dir) = path.parent() {
|
||||||
|
let _ = std::fs::create_dir_all(dir);
|
||||||
|
}
|
||||||
|
let _ = rl.load_history(path);
|
||||||
|
}
|
||||||
|
let save_history = |rl: &mut Editor<WaferHelper, rustyline::history::DefaultHistory>| {
|
||||||
|
if let Some(path) = &history {
|
||||||
|
let _ = rl.save_history(path);
|
||||||
|
#[cfg(unix)]
|
||||||
|
{
|
||||||
|
use std::os::unix::fs::PermissionsExt;
|
||||||
|
let _ = std::fs::set_permissions(path, std::fs::Permissions::from_mode(0o600));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
loop {
|
||||||
|
let prompt = if vm.is_compiling() { " ] " } else { "> " };
|
||||||
|
match rl.readline(prompt) {
|
||||||
|
Ok(line) => {
|
||||||
|
let _ = rl.add_history_entry(&line);
|
||||||
|
match vm.evaluate(&line) {
|
||||||
|
Ok(()) => {
|
||||||
|
let output = vm.take_output();
|
||||||
|
if vm.bye_requested() {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
// PAGE (form feed) clears the terminal
|
||||||
|
if output.contains('\x0C') {
|
||||||
|
print!("\x1b[2J\x1b[H");
|
||||||
|
}
|
||||||
|
let output = output.replace('\x0C', "");
|
||||||
|
if !vm.is_compiling() {
|
||||||
|
if output.contains('\n') {
|
||||||
|
// Multi-line output (DUMP, WORDS, ...):
|
||||||
|
// print as a block, then ok on its own line
|
||||||
|
print!("{output}");
|
||||||
|
if !output.ends_with('\n') {
|
||||||
|
println!();
|
||||||
|
}
|
||||||
|
println!(" ok");
|
||||||
|
} else {
|
||||||
|
// Move cursor back up to end of input line so
|
||||||
|
// output appears inline, like traditional Forth:
|
||||||
|
// > 2 2 + . 4 ok
|
||||||
|
let col = prompt.len() + line.len() + 1;
|
||||||
|
print!("\x1b[A\x1b[{col}G {output} ok");
|
||||||
|
println!();
|
||||||
|
}
|
||||||
|
} else if !output.is_empty() {
|
||||||
|
print!("{output}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
eprintln!("Error: {e:#}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// New definitions may have appeared: refresh completion
|
||||||
|
if let Some(h) = rl.helper_mut() {
|
||||||
|
h.words = vm.word_names();
|
||||||
|
}
|
||||||
|
save_history(&mut rl);
|
||||||
|
}
|
||||||
|
// Ctrl-C abandons the current line, Ctrl-D exits
|
||||||
|
Err(rustyline::error::ReadlineError::Interrupted) => {}
|
||||||
|
Err(rustyline::error::ReadlineError::Eof) => break,
|
||||||
|
Err(e) => {
|
||||||
|
eprintln!("Readline error: {e}");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
save_history(&mut rl);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|||||||
+69
-2
@@ -72,6 +72,19 @@
|
|||||||
1-
|
1-
|
||||||
REPEAT ;
|
REPEAT ;
|
||||||
|
|
||||||
|
\ ---------------------------------------------------------------
|
||||||
|
\ Common extensions (not in Forth 2012, gforth-compatible)
|
||||||
|
\ ---------------------------------------------------------------
|
||||||
|
|
||||||
|
\ -ROT ( x1 x2 x3 -- x3 x1 x2 ) rotate top item to third place
|
||||||
|
: -ROT ROT ROT ;
|
||||||
|
|
||||||
|
\ <= ( n1 n2 -- flag ) true if n1 <= n2 (signed)
|
||||||
|
: <= > 0= ;
|
||||||
|
|
||||||
|
\ >= ( n1 n2 -- flag ) true if n1 >= n2 (signed)
|
||||||
|
: >= < 0= ;
|
||||||
|
|
||||||
\ ---------------------------------------------------------------
|
\ ---------------------------------------------------------------
|
||||||
\ Phase 2: Double-cell arithmetic
|
\ Phase 2: Double-cell arithmetic
|
||||||
\ ---------------------------------------------------------------
|
\ ---------------------------------------------------------------
|
||||||
@@ -184,8 +197,8 @@
|
|||||||
\ TYPE ( c-addr u -- ) output u characters
|
\ TYPE ( c-addr u -- ) output u characters
|
||||||
: TYPE 0 ?DO DUP C@ EMIT 1+ LOOP DROP ;
|
: TYPE 0 ?DO DUP C@ EMIT 1+ LOOP DROP ;
|
||||||
|
|
||||||
\ SPACES ( n -- ) output n spaces
|
\ SPACES ( n -- ) output n spaces (nothing for n <= 0, per 6.1.2230)
|
||||||
: SPACES 0 ?DO SPACE LOOP ;
|
: SPACES 0 MAX 0 ?DO SPACE LOOP ;
|
||||||
|
|
||||||
\ Pictured numeric output constants
|
\ Pictured numeric output constants
|
||||||
\ PICT_BUF_TOP = 0x05C0 = 1472, SYSVAR_HLD = 28
|
\ PICT_BUF_TOP = 0x05C0 = 1472, SYSVAR_HLD = 28
|
||||||
@@ -230,6 +243,9 @@
|
|||||||
\ U. ( u -- ) print unsigned number and space
|
\ U. ( u -- ) print unsigned number and space
|
||||||
: U. 0 <# #S #> TYPE SPACE ;
|
: U. 0 <# #S #> TYPE SPACE ;
|
||||||
|
|
||||||
|
\ ? ( a-addr -- ) fetch and print
|
||||||
|
: ? @ . ;
|
||||||
|
|
||||||
\ .R ( n width -- ) print right-justified signed number
|
\ .R ( n width -- ) print right-justified signed number
|
||||||
: .R >R DUP ABS 0 <# #S ROT SIGN #> R> OVER - SPACES TYPE ;
|
: .R >R DUP ABS 0 <# #S ROT SIGN #> R> OVER - SPACES TYPE ;
|
||||||
|
|
||||||
@@ -242,6 +258,21 @@
|
|||||||
\ D.R ( d width -- ) print right-justified signed double
|
\ D.R ( d width -- ) print right-justified signed double
|
||||||
: D.R >R SWAP OVER DABS <# #S ROT SIGN #> R> OVER - SPACES TYPE ;
|
: D.R >R SWAP OVER DABS <# #S ROT SIGN #> R> OVER - SPACES TYPE ;
|
||||||
|
|
||||||
|
\ ---------------------------------------------------------------
|
||||||
|
\ Return-stack introspection (debug aids)
|
||||||
|
\ ---------------------------------------------------------------
|
||||||
|
|
||||||
|
\ RDEPTH ( -- n ) number of cells on the return stack
|
||||||
|
\ RETURN_STACK_TOP = 9728 (0x2600). Only >R temps and loop params
|
||||||
|
\ live there; return addresses are on the WASM call stack.
|
||||||
|
: RDEPTH 9728 RP@ - 2 RSHIFT ;
|
||||||
|
|
||||||
|
\ .RS ( -- ) print the return stack bottom-to-top, like .S
|
||||||
|
\ Walks with BEGIN/WHILE (not DO) so the walk itself never pushes
|
||||||
|
\ onto the return stack it is printing.
|
||||||
|
: .RS ." R:<" RDEPTH 0 .R ." > "
|
||||||
|
9728 BEGIN DUP RP@ > WHILE 4 - DUP @ . REPEAT DROP ;
|
||||||
|
|
||||||
\ ---------------------------------------------------------------
|
\ ---------------------------------------------------------------
|
||||||
\ Phase 6: DEFER support
|
\ Phase 6: DEFER support
|
||||||
\ ---------------------------------------------------------------
|
\ ---------------------------------------------------------------
|
||||||
@@ -310,3 +341,39 @@
|
|||||||
\ State-smart string literal for the next whitespace-delimited token.
|
\ State-smart string literal for the next whitespace-delimited token.
|
||||||
\ Handled in Rust (outer.rs interpret_token_immediate / compile_token)
|
\ Handled in Rust (outer.rs interpret_token_immediate / compile_token)
|
||||||
\ so the string survives REFILL in interpret mode.
|
\ so the string survives REFILL in interpret mode.
|
||||||
|
|
||||||
|
\ ---------------------------------------------------------------
|
||||||
|
\ Structures (Forth 2012 Facility-ext 10.6.2.0935 family)
|
||||||
|
\ ---------------------------------------------------------------
|
||||||
|
\ Usage:
|
||||||
|
\ BEGIN-STRUCTURE POINT FIELD: P.X FIELD: P.Y END-STRUCTURE
|
||||||
|
\ CREATE ORIGIN POINT ALLOT
|
||||||
|
\ 1 ORIGIN P.X ! 2 ORIGIN P.Y !
|
||||||
|
|
||||||
|
\ Each defining word factored inline (CREATE .. DOES>). WAFER dispatches
|
||||||
|
\ DOES>-defining words only at the outer interpreter, so they can't be
|
||||||
|
\ factored through other compiled words (FIELD: -> +FIELD would no-op).
|
||||||
|
|
||||||
|
: BEGIN-STRUCTURE ( "name" -- struct-sys 0 )
|
||||||
|
CREATE HERE 0 0 , DOES> @ ;
|
||||||
|
|
||||||
|
: END-STRUCTURE ( struct-sys +n -- )
|
||||||
|
SWAP ! ;
|
||||||
|
|
||||||
|
: +FIELD ( n1 "name" n2 -- n3 )
|
||||||
|
CREATE OVER , + DOES> @ + ;
|
||||||
|
|
||||||
|
: FIELD: ( n1 "name" -- n2 )
|
||||||
|
CREATE ALIGNED DUP , 1 CELLS + DOES> @ + ;
|
||||||
|
|
||||||
|
: CFIELD: ( n1 "name" -- n2 )
|
||||||
|
CREATE DUP , 1 CHARS + DOES> @ + ;
|
||||||
|
|
||||||
|
: FFIELD: ( n1 "name" -- n2 )
|
||||||
|
CREATE FALIGNED DUP , 1 FLOATS + DOES> @ + ;
|
||||||
|
|
||||||
|
: SFFIELD: ( n1 "name" -- n2 )
|
||||||
|
CREATE SFALIGNED DUP , 1 SFLOATS + DOES> @ + ;
|
||||||
|
|
||||||
|
: DFFIELD: ( n1 "name" -- n2 )
|
||||||
|
CREATE DFALIGNED DUP , 1 DFLOATS + DOES> @ + ;
|
||||||
|
|||||||
+1372
-128
File diff suppressed because it is too large
Load Diff
@@ -7,6 +7,16 @@ use crate::optimizer::OptConfig;
|
|||||||
pub struct CodegenOpts {
|
pub struct CodegenOpts {
|
||||||
/// Enable stack-to-local promotion for straight-line words.
|
/// Enable stack-to-local promotion for straight-line words.
|
||||||
pub stack_to_local_promotion: bool,
|
pub stack_to_local_promotion: bool,
|
||||||
|
/// Emit stack under/overflow guards in compiled words. Faults throw
|
||||||
|
/// standard codes (-3/-4/-5/-6/-44/-45) instead of silently
|
||||||
|
/// corrupting stack pointers. On by default; benchmarks and
|
||||||
|
/// exported production modules turn it off.
|
||||||
|
pub stack_guards: bool,
|
||||||
|
/// Compile words with a statically known stack effect to a typed entry
|
||||||
|
/// point that carries stack items in WASM values, so a call keeps them
|
||||||
|
/// in registers instead of round-tripping through the memory stack.
|
||||||
|
/// On by default; `WAFER_TYPED_CALLS=0` turns it off.
|
||||||
|
pub typed_calls: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Master configuration for all WAFER optimizations.
|
/// Master configuration for all WAFER optimizations.
|
||||||
@@ -32,6 +42,8 @@ impl WaferConfig {
|
|||||||
},
|
},
|
||||||
codegen: CodegenOpts {
|
codegen: CodegenOpts {
|
||||||
stack_to_local_promotion: true,
|
stack_to_local_promotion: true,
|
||||||
|
stack_guards: true,
|
||||||
|
typed_calls: true,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -49,6 +61,8 @@ impl WaferConfig {
|
|||||||
},
|
},
|
||||||
codegen: CodegenOpts {
|
codegen: CodegenOpts {
|
||||||
stack_to_local_promotion: false,
|
stack_to_local_promotion: false,
|
||||||
|
stack_guards: false,
|
||||||
|
typed_calls: false,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ mod tests {
|
|||||||
// Empty word list should produce nothing (but we guard against this at call site)
|
// Empty word list should produce nothing (but we guard against this at call site)
|
||||||
let words = vec![];
|
let words = vec![];
|
||||||
let map = HashMap::new();
|
let map = HashMap::new();
|
||||||
let result = compile_consolidated_module(&words, &map, 16);
|
let result = compile_consolidated_module(&words, &map, 16, None, true);
|
||||||
// Empty is valid -- should produce a valid module with no functions
|
// Empty is valid -- should produce a valid module with no functions
|
||||||
assert!(result.is_ok());
|
assert!(result.is_ok());
|
||||||
}
|
}
|
||||||
@@ -31,7 +31,7 @@ mod tests {
|
|||||||
let words = vec![(WordId(1), vec![IrOp::PushI32(42)])];
|
let words = vec![(WordId(1), vec![IrOp::PushI32(42)])];
|
||||||
let mut map = HashMap::new();
|
let mut map = HashMap::new();
|
||||||
map.insert(WordId(1), 1u32); // function index 1 (after emit import)
|
map.insert(WordId(1), 1u32); // function index 1 (after emit import)
|
||||||
let result = compile_consolidated_module(&words, &map, 16);
|
let result = compile_consolidated_module(&words, &map, 16, None, true);
|
||||||
assert!(result.is_ok());
|
assert!(result.is_ok());
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -49,7 +49,7 @@ mod tests {
|
|||||||
map.insert(WordId(1), 1u32);
|
map.insert(WordId(1), 1u32);
|
||||||
map.insert(WordId(2), 2u32);
|
map.insert(WordId(2), 2u32);
|
||||||
map.insert(WordId(3), 3u32);
|
map.insert(WordId(3), 3u32);
|
||||||
let result = compile_consolidated_module(&words, &map, 16);
|
let result = compile_consolidated_module(&words, &map, 16, None, true);
|
||||||
assert!(result.is_ok());
|
assert!(result.is_ok());
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -59,7 +59,7 @@ mod tests {
|
|||||||
let words = vec![(WordId(3), vec![IrOp::Call(WordId(99))])];
|
let words = vec![(WordId(3), vec![IrOp::Call(WordId(99))])];
|
||||||
let mut map = HashMap::new();
|
let mut map = HashMap::new();
|
||||||
map.insert(WordId(3), 1u32);
|
map.insert(WordId(3), 1u32);
|
||||||
let result = compile_consolidated_module(&words, &map, 256);
|
let result = compile_consolidated_module(&words, &map, 256, None, true);
|
||||||
assert!(result.is_ok());
|
assert!(result.is_ok());
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -72,7 +72,7 @@ mod tests {
|
|||||||
let mut map = HashMap::new();
|
let mut map = HashMap::new();
|
||||||
map.insert(WordId(1), 1u32);
|
map.insert(WordId(1), 1u32);
|
||||||
map.insert(WordId(2), 2u32);
|
map.insert(WordId(2), 2u32);
|
||||||
let result = compile_consolidated_module(&words, &map, 16);
|
let result = compile_consolidated_module(&words, &map, 16, None, true);
|
||||||
assert!(result.is_ok());
|
assert!(result.is_ok());
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -95,7 +95,7 @@ mod tests {
|
|||||||
let mut map = HashMap::new();
|
let mut map = HashMap::new();
|
||||||
map.insert(WordId(1), 1u32);
|
map.insert(WordId(1), 1u32);
|
||||||
map.insert(WordId(2), 2u32);
|
map.insert(WordId(2), 2u32);
|
||||||
let result = compile_consolidated_module(&words, &map, 16);
|
let result = compile_consolidated_module(&words, &map, 16, None, true);
|
||||||
assert!(result.is_ok());
|
assert!(result.is_ok());
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -120,7 +120,7 @@ mod tests {
|
|||||||
let mut map = HashMap::new();
|
let mut map = HashMap::new();
|
||||||
map.insert(WordId(1), 1u32);
|
map.insert(WordId(1), 1u32);
|
||||||
map.insert(WordId(2), 2u32);
|
map.insert(WordId(2), 2u32);
|
||||||
let result = compile_consolidated_module(&words, &map, 16);
|
let result = compile_consolidated_module(&words, &map, 16, None, true);
|
||||||
assert!(result.is_ok());
|
assert!(result.is_ok());
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -141,7 +141,7 @@ mod tests {
|
|||||||
let mut map = HashMap::new();
|
let mut map = HashMap::new();
|
||||||
map.insert(WordId(1), 1u32);
|
map.insert(WordId(1), 1u32);
|
||||||
map.insert(WordId(2), 2u32);
|
map.insert(WordId(2), 2u32);
|
||||||
let result = compile_consolidated_module(&words, &map, 16);
|
let result = compile_consolidated_module(&words, &map, 16, None, true);
|
||||||
assert!(result.is_ok());
|
assert!(result.is_ok());
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -163,7 +163,7 @@ mod tests {
|
|||||||
let mut map = HashMap::new();
|
let mut map = HashMap::new();
|
||||||
map.insert(WordId(1), 1u32);
|
map.insert(WordId(1), 1u32);
|
||||||
map.insert(WordId(2), 2u32);
|
map.insert(WordId(2), 2u32);
|
||||||
let result = compile_consolidated_module(&words, &map, 16);
|
let result = compile_consolidated_module(&words, &map, 16, None, true);
|
||||||
assert!(result.is_ok());
|
assert!(result.is_ok());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -80,7 +80,10 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn sha1_rfc3174_abc() {
|
fn sha1_rfc3174_abc() {
|
||||||
assert_eq!(hex(&sha1_hash(b"abc")), "a9993e364706816aba3e25717850c26c9cd0d89d");
|
assert_eq!(
|
||||||
|
hex(&sha1_hash(b"abc")),
|
||||||
|
"a9993e364706816aba3e25717850c26c9cd0d89d"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
@@ -18,6 +18,8 @@ pub mod flags {
|
|||||||
pub const IMMEDIATE: u8 = 0x80;
|
pub const IMMEDIATE: u8 = 0x80;
|
||||||
/// Word is hidden (being compiled, not yet findable).
|
/// Word is hidden (being compiled, not yet findable).
|
||||||
pub const HIDDEN: u8 = 0x40;
|
pub const HIDDEN: u8 = 0x40;
|
||||||
|
/// Word is an implementation detail: findable, but skipped by WORDS.
|
||||||
|
pub const INTERNAL: u8 = 0x20;
|
||||||
/// Mask for the name length (lower 5 bits).
|
/// Mask for the name length (lower 5 bits).
|
||||||
pub const LENGTH_MASK: u8 = 0x1F;
|
pub const LENGTH_MASK: u8 = 0x1F;
|
||||||
/// Maximum word name length.
|
/// Maximum word name length.
|
||||||
@@ -95,11 +97,17 @@ impl Dictionary {
|
|||||||
// Write link field (points to previous LATEST)
|
// Write link field (points to previous LATEST)
|
||||||
self.write_u32_unchecked(entry_start, self.latest);
|
self.write_u32_unchecked(entry_start, self.latest);
|
||||||
|
|
||||||
// Write flags byte: HIDDEN | length, optionally IMMEDIATE
|
// Write flags byte: HIDDEN | length, optionally IMMEDIATE.
|
||||||
|
// Underscore-prefixed names are implementation details by repo
|
||||||
|
// convention (see tools/editor-support): flag them INTERNAL so
|
||||||
|
// WORDS and completion skip them while FIND still works.
|
||||||
let mut flag_byte = flags::HIDDEN | (name_len as u8 & flags::LENGTH_MASK);
|
let mut flag_byte = flags::HIDDEN | (name_len as u8 & flags::LENGTH_MASK);
|
||||||
if immediate {
|
if immediate {
|
||||||
flag_byte |= flags::IMMEDIATE;
|
flag_byte |= flags::IMMEDIATE;
|
||||||
}
|
}
|
||||||
|
if name_bytes.first() == Some(&b'_') {
|
||||||
|
flag_byte |= flags::INTERNAL;
|
||||||
|
}
|
||||||
self.memory[(entry_start + 4) as usize] = flag_byte;
|
self.memory[(entry_start + 4) as usize] = flag_byte;
|
||||||
|
|
||||||
// Write name bytes
|
// Write name bytes
|
||||||
@@ -184,10 +192,9 @@ impl Dictionary {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Fallback: return newest entry across all wordlists
|
// In no wordlist of the search order: not findable
|
||||||
if let Some(&(_wid, word_addr, fn_index, is_immediate)) = entries.last() {
|
// (Forth 2012 §16.3.3 — the order is authoritative).
|
||||||
return Some((word_addr, WordId(fn_index), is_immediate));
|
return None;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fallback: linked-list walk (for words not yet in the index)
|
// Fallback: linked-list walk (for words not yet in the index)
|
||||||
@@ -410,8 +417,21 @@ impl Dictionary {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Return names of all visible (non-hidden) words, newest first.
|
/// Return names of all visible (non-hidden) words, newest first.
|
||||||
pub fn visible_words(&self) -> Vec<String> {
|
/// With `include_internal` false, words flagged INTERNAL are skipped.
|
||||||
let mut names = Vec::new();
|
pub fn visible_words(&self, include_internal: bool) -> Vec<String> {
|
||||||
|
self.visible_entries()
|
||||||
|
.into_iter()
|
||||||
|
.filter(|(_, _, internal)| include_internal || !internal)
|
||||||
|
.map(|(name, _, _)| name)
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// All visible (non-hidden) entries, newest first:
|
||||||
|
/// (name, wordlist id, INTERNAL flag). The wid comes from the hash
|
||||||
|
/// index (entries themselves store no wid); words missing from the
|
||||||
|
/// index default to wid 1 (FORTH).
|
||||||
|
pub fn visible_entries(&self) -> Vec<(String, u32, bool)> {
|
||||||
|
let mut entries = Vec::new();
|
||||||
let mut addr = self.latest;
|
let mut addr = self.latest;
|
||||||
while addr != 0 {
|
while addr != 0 {
|
||||||
let flags_byte = self.memory[(addr + 4) as usize];
|
let flags_byte = self.memory[(addr + 4) as usize];
|
||||||
@@ -420,7 +440,12 @@ impl Dictionary {
|
|||||||
let name_start = (addr + 5) as usize;
|
let name_start = (addr + 5) as usize;
|
||||||
let name = String::from_utf8_lossy(&self.memory[name_start..name_start + name_len])
|
let name = String::from_utf8_lossy(&self.memory[name_start..name_start + name_len])
|
||||||
.to_string();
|
.to_string();
|
||||||
names.push(name);
|
let wid = self
|
||||||
|
.index
|
||||||
|
.get(&name)
|
||||||
|
.and_then(|es| es.iter().find(|e| e.1 == addr))
|
||||||
|
.map_or(1, |e| e.0);
|
||||||
|
entries.push((name, wid, flags_byte & flags::INTERNAL != 0));
|
||||||
}
|
}
|
||||||
let link = self.read_u32_unchecked(addr);
|
let link = self.read_u32_unchecked(addr);
|
||||||
if link == addr {
|
if link == addr {
|
||||||
@@ -428,7 +453,7 @@ impl Dictionary {
|
|||||||
}
|
}
|
||||||
addr = link;
|
addr = link;
|
||||||
}
|
}
|
||||||
names
|
entries
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get a reference to the raw memory buffer.
|
/// Get a reference to the raw memory buffer.
|
||||||
|
|||||||
@@ -61,6 +61,13 @@ pub enum WaferError {
|
|||||||
|
|
||||||
#[error("{0}")]
|
#[error("{0}")]
|
||||||
Abort(String),
|
Abort(String),
|
||||||
|
|
||||||
|
/// An uncaught Forth THROW as reported to the user. `message` is the
|
||||||
|
/// full display text (standard message or ABORT" payload); `code`
|
||||||
|
/// carries the THROW code for typed consumers (CLI exit paths, web
|
||||||
|
/// REPL styling) via `Error::downcast_ref`.
|
||||||
|
#[error("{message}")]
|
||||||
|
UncaughtThrow { code: i32, message: String },
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Result type alias for WAFER operations.
|
/// Result type alias for WAFER operations.
|
||||||
|
|||||||
@@ -120,7 +120,14 @@ pub fn export_module(
|
|||||||
metadata_json: metadata_json.as_bytes(),
|
metadata_json: metadata_json.as_bytes(),
|
||||||
};
|
};
|
||||||
|
|
||||||
let wasm_bytes = compile_exportable_module(&words, &local_fn_map, table_size, &export_sections)
|
let wasm_bytes = compile_exportable_module(
|
||||||
|
&words,
|
||||||
|
&local_fn_map,
|
||||||
|
table_size,
|
||||||
|
&export_sections,
|
||||||
|
vm.stack_guard_param(),
|
||||||
|
vm.typed_calls(),
|
||||||
|
)
|
||||||
.map_err(|e| anyhow::anyhow!("export codegen error: {e}"))?;
|
.map_err(|e| anyhow::anyhow!("export codegen error: {e}"))?;
|
||||||
|
|
||||||
Ok((wasm_bytes, metadata))
|
Ok((wasm_bytes, metadata))
|
||||||
@@ -131,11 +138,9 @@ pub fn export_module(
|
|||||||
fn collect_external_calls(ops: &[IrOp], ir_ids: &HashSet<WordId>, host_ids: &mut HashSet<WordId>) {
|
fn collect_external_calls(ops: &[IrOp], ir_ids: &HashSet<WordId>, host_ids: &mut HashSet<WordId>) {
|
||||||
for op in ops {
|
for op in ops {
|
||||||
match op {
|
match op {
|
||||||
IrOp::Call(id) | IrOp::TailCall(id) => {
|
IrOp::Call(id) | IrOp::TailCall(id) if !ir_ids.contains(id) => {
|
||||||
if !ir_ids.contains(id) {
|
|
||||||
host_ids.insert(*id);
|
host_ids.insert(*id);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
IrOp::If {
|
IrOp::If {
|
||||||
then_body,
|
then_body,
|
||||||
else_body,
|
else_body,
|
||||||
|
|||||||
@@ -139,6 +139,10 @@ pub enum IrOp {
|
|||||||
ForthLocalGet(u32),
|
ForthLocalGet(u32),
|
||||||
/// Set Forth local variable N: ( x -- )
|
/// Set Forth local variable N: ( x -- )
|
||||||
ForthLocalSet(u32),
|
ForthLocalSet(u32),
|
||||||
|
/// Push float-typed Forth local N: ( F: -- r )
|
||||||
|
ForthFLocalGet(u32),
|
||||||
|
/// Set float-typed Forth local N: ( F: r -- )
|
||||||
|
ForthFLocalSet(u32),
|
||||||
|
|
||||||
// -- I/O --
|
// -- I/O --
|
||||||
/// Output character: ( char -- )
|
/// Output character: ( char -- )
|
||||||
@@ -155,6 +159,8 @@ pub enum IrOp {
|
|||||||
Execute,
|
Execute,
|
||||||
/// Push the current data-stack pointer: ( -- addr )
|
/// Push the current data-stack pointer: ( -- addr )
|
||||||
SpFetch,
|
SpFetch,
|
||||||
|
/// Push the current return-stack pointer: ( -- addr )
|
||||||
|
RpFetch,
|
||||||
|
|
||||||
// -- Float stack manipulation --
|
// -- Float stack manipulation --
|
||||||
/// Float duplicate: ( F: r -- r r )
|
/// Float duplicate: ( F: r -- r r )
|
||||||
|
|||||||
@@ -24,6 +24,8 @@ pub mod ir;
|
|||||||
pub mod memory;
|
pub mod memory;
|
||||||
pub mod optimizer;
|
pub mod optimizer;
|
||||||
pub mod runtime;
|
pub mod runtime;
|
||||||
|
pub mod see;
|
||||||
|
pub mod wordhelp;
|
||||||
|
|
||||||
// Outer interpreter: runtime-agnostic, works with any Runtime impl
|
// Outer interpreter: runtime-agnostic, works with any Runtime impl
|
||||||
#[allow(trivial_numeric_casts, clippy::unnecessary_cast)]
|
#[allow(trivial_numeric_casts, clippy::unnecessary_cast)]
|
||||||
|
|||||||
@@ -50,23 +50,23 @@ pub const DATA_STACK_BASE: u32 = WORD_BUF_BASE + WORD_BUF_SIZE; // 0x0600
|
|||||||
pub const DATA_STACK_SIZE: u32 = 4096; // 1024 cells
|
pub const DATA_STACK_SIZE: u32 = 4096; // 1024 cells
|
||||||
|
|
||||||
/// Return stack region. Grows downward.
|
/// Return stack region. Grows downward.
|
||||||
pub const RETURN_STACK_BASE: u32 = DATA_STACK_BASE + DATA_STACK_SIZE; // 0x1540
|
pub const RETURN_STACK_BASE: u32 = DATA_STACK_BASE + DATA_STACK_SIZE; // 0x1600
|
||||||
/// Size of return stack region.
|
/// Size of return stack region.
|
||||||
pub const RETURN_STACK_SIZE: u32 = 4096;
|
pub const RETURN_STACK_SIZE: u32 = 4096;
|
||||||
|
|
||||||
/// Floating-point stack region (fallback). Grows downward.
|
/// Floating-point stack region (fallback). Grows downward.
|
||||||
pub const FLOAT_STACK_BASE: u32 = RETURN_STACK_BASE + RETURN_STACK_SIZE; // 0x2540
|
pub const FLOAT_STACK_BASE: u32 = RETURN_STACK_BASE + RETURN_STACK_SIZE; // 0x2600
|
||||||
/// Size of float stack region.
|
/// Size of float stack region.
|
||||||
pub const FLOAT_STACK_SIZE: u32 = 2048; // 256 doubles
|
pub const FLOAT_STACK_SIZE: u32 = 2048; // 256 doubles
|
||||||
|
|
||||||
/// Hash scratch region — output buffer for `SHA1`/`SHA256`/`SHA512` and
|
/// Hash scratch region — output buffer for `SHA1`/`SHA256`/`SHA512` and
|
||||||
/// other hash host words. Sized for the largest supported digest (SHA512 = 64 B).
|
/// other hash host words. Sized for the largest supported digest (SHA512 = 64 B).
|
||||||
pub const HASH_SCRATCH_BASE: u32 = FLOAT_STACK_BASE + FLOAT_STACK_SIZE; // 0x2D40
|
pub const HASH_SCRATCH_BASE: u32 = FLOAT_STACK_BASE + FLOAT_STACK_SIZE; // 0x2E00
|
||||||
/// Size of hash scratch region.
|
/// Size of hash scratch region.
|
||||||
pub const HASH_SCRATCH_SIZE: u32 = 128;
|
pub const HASH_SCRATCH_SIZE: u32 = 128;
|
||||||
|
|
||||||
/// Dictionary region start. Grows upward.
|
/// Dictionary region start. Grows upward.
|
||||||
pub const DICTIONARY_BASE: u32 = HASH_SCRATCH_BASE + HASH_SCRATCH_SIZE; // 0x2DC0
|
pub const DICTIONARY_BASE: u32 = HASH_SCRATCH_BASE + HASH_SCRATCH_SIZE; // 0x2E80
|
||||||
|
|
||||||
/// Initial top of data stack (grows down from here).
|
/// Initial top of data stack (grows down from here).
|
||||||
pub const DATA_STACK_TOP: u32 = DATA_STACK_BASE + DATA_STACK_SIZE;
|
pub const DATA_STACK_TOP: u32 = DATA_STACK_BASE + DATA_STACK_SIZE;
|
||||||
@@ -106,6 +106,25 @@ pub const SYSVAR_NUM_TIB: u32 = SYSVAR_BASE + 24;
|
|||||||
pub const SYSVAR_HLD: u32 = SYSVAR_BASE + 28;
|
pub const SYSVAR_HLD: u32 = SYSVAR_BASE + 28;
|
||||||
/// LEAVE flag: nonzero when LEAVE has been called inside a DO loop.
|
/// LEAVE flag: nonzero when LEAVE has been called inside a DO loop.
|
||||||
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_`.
|
||||||
|
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 {
|
||||||
@@ -147,6 +166,9 @@ 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);
|
||||||
|
|||||||
@@ -53,7 +53,12 @@ pub fn optimize(
|
|||||||
|
|
||||||
// Phase 2: inline then simplify again
|
// Phase 2: inline then simplify again
|
||||||
if config.inline {
|
if config.inline {
|
||||||
ir = inline(ir, bodies, 8);
|
// A caller that can never leave the memory data stack would drag an
|
||||||
|
// inlined loop down with it, so leave those callees where they are:
|
||||||
|
// as their own word the loop keeps its registers, and one call is far
|
||||||
|
// cheaper than a loop's worth of memory traffic.
|
||||||
|
let keep_loops_out = !crate::codegen::promotable_modulo_calls(&ir);
|
||||||
|
ir = inline(ir, bodies, 8, keep_loops_out);
|
||||||
}
|
}
|
||||||
if config.peephole {
|
if config.peephole {
|
||||||
ir = peephole(ir);
|
ir = peephole(ir);
|
||||||
@@ -496,7 +501,12 @@ fn dce(ops: Vec<IrOp>) -> Vec<IrOp> {
|
|||||||
|
|
||||||
/// Inline small word bodies: replaces `Call(id)` with the word's IR body
|
/// Inline small word bodies: replaces `Call(id)` with the word's IR body
|
||||||
/// if the body is small enough and not recursive.
|
/// if the body is small enough and not recursive.
|
||||||
fn inline(ops: Vec<IrOp>, bodies: &HashMap<WordId, Vec<IrOp>>, max_size: usize) -> Vec<IrOp> {
|
fn inline(
|
||||||
|
ops: Vec<IrOp>,
|
||||||
|
bodies: &HashMap<WordId, Vec<IrOp>>,
|
||||||
|
max_size: usize,
|
||||||
|
keep_loops_out: bool,
|
||||||
|
) -> Vec<IrOp> {
|
||||||
let mut out = Vec::new();
|
let mut out = Vec::new();
|
||||||
for op in ops {
|
for op in ops {
|
||||||
match &op {
|
match &op {
|
||||||
@@ -505,6 +515,7 @@ fn inline(ops: Vec<IrOp>, bodies: &HashMap<WordId, Vec<IrOp>>, max_size: usize)
|
|||||||
&& body.len() <= max_size
|
&& body.len() <= max_size
|
||||||
&& !contains_call_to(body, *id)
|
&& !contains_call_to(body, *id)
|
||||||
&& !contains_exit(body)
|
&& !contains_exit(body)
|
||||||
|
&& !(keep_loops_out && crate::codegen::contains_loop(body))
|
||||||
{
|
{
|
||||||
// Inline the body, recursively converting TailCall back to Call
|
// Inline the body, recursively converting TailCall back to Call
|
||||||
// (tail position in the callee is not tail position in the caller).
|
// (tail position in the callee is not tail position in the caller).
|
||||||
@@ -517,7 +528,7 @@ fn inline(ops: Vec<IrOp>, bodies: &HashMap<WordId, Vec<IrOp>>, max_size: usize)
|
|||||||
}
|
}
|
||||||
_ => {
|
_ => {
|
||||||
out.push(apply_to_bodies(op, &|inner| {
|
out.push(apply_to_bodies(op, &|inner| {
|
||||||
inline(inner, bodies, max_size)
|
inline(inner, bodies, max_size, keep_loops_out)
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -591,16 +602,16 @@ fn contains_call_to(ops: &[IrOp], target: WordId) -> bool {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
IrOp::DoLoop { body, .. } | IrOp::BeginUntil { body } | IrOp::BeginAgain { body } => {
|
IrOp::DoLoop { body, .. } | IrOp::BeginUntil { body } | IrOp::BeginAgain { body }
|
||||||
if contains_call_to(body, target) {
|
if contains_call_to(body, target) =>
|
||||||
|
{
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
IrOp::BeginWhileRepeat { test, body }
|
||||||
IrOp::BeginWhileRepeat { test, body } => {
|
if contains_call_to(test, target) || contains_call_to(body, target) =>
|
||||||
if contains_call_to(test, target) || contains_call_to(body, target) {
|
{
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
IrOp::BeginDoubleWhileRepeat {
|
IrOp::BeginDoubleWhileRepeat {
|
||||||
outer_test,
|
outer_test,
|
||||||
inner_test,
|
inner_test,
|
||||||
@@ -633,7 +644,11 @@ fn contains_call_to(ops: &[IrOp], target: WordId) -> bool {
|
|||||||
fn contains_exit(ops: &[IrOp]) -> bool {
|
fn contains_exit(ops: &[IrOp]) -> bool {
|
||||||
for op in ops {
|
for op in ops {
|
||||||
match op {
|
match op {
|
||||||
IrOp::Exit | IrOp::ForthLocalGet(_) | IrOp::ForthLocalSet(_) => return true,
|
IrOp::Exit
|
||||||
|
| IrOp::ForthLocalGet(_)
|
||||||
|
| IrOp::ForthLocalSet(_)
|
||||||
|
| IrOp::ForthFLocalGet(_)
|
||||||
|
| IrOp::ForthFLocalSet(_) => return true,
|
||||||
IrOp::If {
|
IrOp::If {
|
||||||
then_body,
|
then_body,
|
||||||
else_body,
|
else_body,
|
||||||
@@ -647,16 +662,14 @@ fn contains_exit(ops: &[IrOp]) -> bool {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
IrOp::DoLoop { body, .. } | IrOp::BeginUntil { body } | IrOp::BeginAgain { body } => {
|
IrOp::DoLoop { body, .. } | IrOp::BeginUntil { body } | IrOp::BeginAgain { body }
|
||||||
if contains_exit(body) {
|
if contains_exit(body) =>
|
||||||
|
{
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
IrOp::BeginWhileRepeat { test, body } if contains_exit(test) || contains_exit(body) => {
|
||||||
IrOp::BeginWhileRepeat { test, body } => {
|
|
||||||
if contains_exit(test) || contains_exit(body) {
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1010,4 +1023,54 @@ mod tests {
|
|||||||
let result = optimize(vec![IrOp::Call(WordId(5))], &config, &bodies);
|
let result = optimize(vec![IrOp::Call(WordId(5))], &config, &bodies);
|
||||||
assert_eq!(result, vec![IrOp::Call(WordId(5))]);
|
assert_eq!(result, vec![IrOp::Call(WordId(5))]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn keeps_a_loop_out_of_a_caller_stuck_on_the_memory_stack() {
|
||||||
|
// The caller has a `.`, so it can never leave the memory data stack.
|
||||||
|
// Inlining the loop would drag it down too; as its own word the loop
|
||||||
|
// keeps its registers and the caller just pays one call.
|
||||||
|
let mut bodies = HashMap::new();
|
||||||
|
bodies.insert(
|
||||||
|
WordId(5),
|
||||||
|
vec![IrOp::DoLoop {
|
||||||
|
body: vec![IrOp::PushI32(1), IrOp::Add],
|
||||||
|
is_plus_loop: false,
|
||||||
|
}],
|
||||||
|
);
|
||||||
|
let result = opt_with_inline(vec![IrOp::Call(WordId(5)), IrOp::Dot], &bodies);
|
||||||
|
assert!(
|
||||||
|
matches!(result.first(), Some(IrOp::Call(WordId(5)))),
|
||||||
|
"loop should not have been inlined, got {result:?}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn still_inlines_a_loop_into_a_caller_that_can_be_promoted() {
|
||||||
|
let mut bodies = HashMap::new();
|
||||||
|
bodies.insert(
|
||||||
|
WordId(5),
|
||||||
|
vec![IrOp::DoLoop {
|
||||||
|
body: vec![IrOp::PushI32(1), IrOp::Add],
|
||||||
|
is_plus_loop: false,
|
||||||
|
}],
|
||||||
|
);
|
||||||
|
let result = opt_with_inline(vec![IrOp::Call(WordId(5)), IrOp::Dup], &bodies);
|
||||||
|
assert!(
|
||||||
|
!result.iter().any(|op| matches!(op, IrOp::Call(_))),
|
||||||
|
"loop should have been inlined, got {result:?}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn still_inlines_straight_line_words_anywhere() {
|
||||||
|
// Only loops are held back; a small straight-line word is still
|
||||||
|
// better off inlined even into an unpromotable caller.
|
||||||
|
let mut bodies = HashMap::new();
|
||||||
|
bodies.insert(WordId(5), vec![IrOp::Dup, IrOp::Mul]);
|
||||||
|
let result = opt_with_inline(vec![IrOp::Call(WordId(5)), IrOp::Dot], &bodies);
|
||||||
|
assert!(
|
||||||
|
!result.iter().any(|op| matches!(op, IrOp::Call(_))),
|
||||||
|
"straight-line word should still inline, got {result:?}"
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+3131
-405
File diff suppressed because it is too large
Load Diff
@@ -98,11 +98,29 @@ impl HostAccess for CallerHostAccess<'_, '_> {
|
|||||||
let func = *func_ref
|
let func = *func_ref
|
||||||
.unwrap_func()
|
.unwrap_func()
|
||||||
.ok_or_else(|| anyhow::anyhow!("call_func: null funcref {fn_index}"))?;
|
.ok_or_else(|| anyhow::anyhow!("call_func: null funcref {fn_index}"))?;
|
||||||
func.call(&mut *self.caller, &[], &mut [])?;
|
func.call(&mut *self.caller, &[], &mut [])
|
||||||
|
.map_err(name_trap_frame)?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Prefix a wasmtime trap error with the innermost named WASM frame.
|
||||||
|
/// Compiled words carry their Forth name in the module name section, so a
|
||||||
|
/// genuine trap reads "in <WORD>: wasm trap: ...". THROW-driven unwinds
|
||||||
|
/// also pass through here, but CATCH and `describe_uncaught` key on the
|
||||||
|
/// shared `throw_code` cell, never on the message, so the wrap is inert
|
||||||
|
/// for them.
|
||||||
|
fn name_trap_frame(e: wasmtime::Error) -> wasmtime::Error {
|
||||||
|
let name = e
|
||||||
|
.downcast_ref::<wasmtime::WasmBacktrace>()
|
||||||
|
.and_then(|bt| bt.frames().iter().find_map(|f| f.func_name()))
|
||||||
|
.map(str::to_string);
|
||||||
|
match name {
|
||||||
|
Some(n) => e.context(format!("in {n}")),
|
||||||
|
None => e,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Wasmtime-based native runtime.
|
/// Wasmtime-based native runtime.
|
||||||
pub struct NativeRuntime {
|
pub struct NativeRuntime {
|
||||||
engine: Engine,
|
engine: Engine,
|
||||||
@@ -293,7 +311,8 @@ impl Runtime for NativeRuntime {
|
|||||||
let func = *r
|
let func = *r
|
||||||
.unwrap_func()
|
.unwrap_func()
|
||||||
.ok_or_else(|| anyhow::anyhow!("word {fn_index} is null funcref"))?;
|
.ok_or_else(|| anyhow::anyhow!("word {fn_index} is null funcref"))?;
|
||||||
func.call(&mut self.store, &[], &mut [])?;
|
func.call(&mut self.store, &[], &mut [])
|
||||||
|
.map_err(name_trap_frame)?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,376 @@
|
|||||||
|
//! IR pretty-printer for `SEE-IR` and the `SEE` fallback path.
|
||||||
|
//!
|
||||||
|
//! Renders a post-optimization IR body as indented, one-op-per-line text.
|
||||||
|
//! Simple ops print as short lowercase mnemonics (Forth glyphs where they
|
||||||
|
//! are universally recognizable: `@`, `!`, `0=`, `>r`, ...); structured ops
|
||||||
|
//! print as Forth control words with 2-space indented bodies. Calls resolve
|
||||||
|
//! `WordId`s to names through an optional resolver so the formatter itself
|
||||||
|
//! stays independent of the VM.
|
||||||
|
|
||||||
|
use crate::dictionary::WordId;
|
||||||
|
use crate::ir::IrOp;
|
||||||
|
|
||||||
|
/// Format an IR body as indented, one-op-per-line text.
|
||||||
|
pub fn format_ir(ops: &[IrOp]) -> String {
|
||||||
|
format_ir_with(ops, &|_| None)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Like [`format_ir`], resolving `Call`/`TailCall`/`Execute` targets to word
|
||||||
|
/// names via `resolve`; unresolved ids print as `#N`.
|
||||||
|
pub fn format_ir_with(ops: &[IrOp], resolve: &dyn Fn(WordId) -> Option<String>) -> String {
|
||||||
|
let mut out = String::new();
|
||||||
|
write_ops(&mut out, ops, 0, resolve);
|
||||||
|
out
|
||||||
|
}
|
||||||
|
|
||||||
|
fn line(out: &mut String, depth: usize, text: &str) {
|
||||||
|
for _ in 0..depth {
|
||||||
|
out.push_str(" ");
|
||||||
|
}
|
||||||
|
out.push_str(text);
|
||||||
|
out.push('\n');
|
||||||
|
}
|
||||||
|
|
||||||
|
fn callee(id: WordId, resolve: &dyn Fn(WordId) -> Option<String>) -> String {
|
||||||
|
resolve(id).unwrap_or_else(|| format!("#{}", id.0))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn write_ops(
|
||||||
|
out: &mut String,
|
||||||
|
ops: &[IrOp],
|
||||||
|
depth: usize,
|
||||||
|
resolve: &dyn Fn(WordId) -> Option<String>,
|
||||||
|
) {
|
||||||
|
for op in ops {
|
||||||
|
write_op(out, op, depth, resolve);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn write_op(out: &mut String, op: &IrOp, depth: usize, resolve: &dyn Fn(WordId) -> Option<String>) {
|
||||||
|
// Exhaustive on purpose: a new IrOp variant must show up here at
|
||||||
|
// compile time, not silently render wrong.
|
||||||
|
let simple: String = match op {
|
||||||
|
// -- Literals --
|
||||||
|
IrOp::PushI32(v) => format!("push {v}"),
|
||||||
|
IrOp::PushI64(v) => format!("push64 {v}"),
|
||||||
|
IrOp::PushF64(v) => format!("fpush {v}"),
|
||||||
|
|
||||||
|
// -- Stack manipulation --
|
||||||
|
IrOp::Drop => "drop".into(),
|
||||||
|
IrOp::Dup => "dup".into(),
|
||||||
|
IrOp::Swap => "swap".into(),
|
||||||
|
IrOp::Over => "over".into(),
|
||||||
|
IrOp::Rot => "rot".into(),
|
||||||
|
IrOp::Nip => "nip".into(),
|
||||||
|
IrOp::Tuck => "tuck".into(),
|
||||||
|
IrOp::TwoDup => "2dup".into(),
|
||||||
|
IrOp::TwoDrop => "2drop".into(),
|
||||||
|
|
||||||
|
// -- Arithmetic --
|
||||||
|
IrOp::Add => "add".into(),
|
||||||
|
IrOp::Sub => "sub".into(),
|
||||||
|
IrOp::Mul => "mul".into(),
|
||||||
|
IrOp::DivMod => "divmod".into(),
|
||||||
|
IrOp::Negate => "negate".into(),
|
||||||
|
IrOp::Abs => "abs".into(),
|
||||||
|
|
||||||
|
// -- Comparison --
|
||||||
|
IrOp::Eq => "eq".into(),
|
||||||
|
IrOp::NotEq => "ne".into(),
|
||||||
|
IrOp::Lt => "lt".into(),
|
||||||
|
IrOp::Gt => "gt".into(),
|
||||||
|
IrOp::LtUnsigned => "u<".into(),
|
||||||
|
IrOp::ZeroEq => "0=".into(),
|
||||||
|
IrOp::ZeroLt => "0<".into(),
|
||||||
|
|
||||||
|
// -- Logic --
|
||||||
|
IrOp::And => "and".into(),
|
||||||
|
IrOp::Or => "or".into(),
|
||||||
|
IrOp::Xor => "xor".into(),
|
||||||
|
IrOp::Invert => "invert".into(),
|
||||||
|
IrOp::Lshift => "lshift".into(),
|
||||||
|
IrOp::Rshift => "rshift".into(),
|
||||||
|
IrOp::ArithRshift => "arshift".into(),
|
||||||
|
|
||||||
|
// -- Memory --
|
||||||
|
IrOp::Fetch => "@".into(),
|
||||||
|
IrOp::Store => "!".into(),
|
||||||
|
IrOp::CFetch => "c@".into(),
|
||||||
|
IrOp::CStore => "c!".into(),
|
||||||
|
IrOp::PlusStore => "+!".into(),
|
||||||
|
|
||||||
|
// -- Calls --
|
||||||
|
IrOp::Call(id) => format!("call {}", callee(*id, resolve)),
|
||||||
|
IrOp::TailCall(id) => format!("tail-call {}", callee(*id, resolve)),
|
||||||
|
|
||||||
|
// -- Structured control flow (multi-line) --
|
||||||
|
IrOp::If {
|
||||||
|
then_body,
|
||||||
|
else_body,
|
||||||
|
} => {
|
||||||
|
line(out, depth, "if");
|
||||||
|
write_ops(out, then_body, depth + 1, resolve);
|
||||||
|
if let Some(eb) = else_body {
|
||||||
|
line(out, depth, "else");
|
||||||
|
write_ops(out, eb, depth + 1, resolve);
|
||||||
|
}
|
||||||
|
line(out, depth, "then");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
IrOp::DoLoop { body, is_plus_loop } => {
|
||||||
|
line(out, depth, "do");
|
||||||
|
write_ops(out, body, depth + 1, resolve);
|
||||||
|
line(out, depth, if *is_plus_loop { "+loop" } else { "loop" });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
IrOp::BeginUntil { body } => {
|
||||||
|
line(out, depth, "begin");
|
||||||
|
write_ops(out, body, depth + 1, resolve);
|
||||||
|
line(out, depth, "until");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
IrOp::BeginAgain { body } => {
|
||||||
|
line(out, depth, "begin");
|
||||||
|
write_ops(out, body, depth + 1, resolve);
|
||||||
|
line(out, depth, "again");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
IrOp::BeginWhileRepeat { test, body } => {
|
||||||
|
line(out, depth, "begin");
|
||||||
|
write_ops(out, test, depth + 1, resolve);
|
||||||
|
line(out, depth, "while");
|
||||||
|
write_ops(out, body, depth + 1, resolve);
|
||||||
|
line(out, depth, "repeat");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
IrOp::BeginDoubleWhileRepeat {
|
||||||
|
outer_test,
|
||||||
|
inner_test,
|
||||||
|
body,
|
||||||
|
after_repeat,
|
||||||
|
else_body,
|
||||||
|
} => {
|
||||||
|
line(out, depth, "begin");
|
||||||
|
write_ops(out, outer_test, depth + 1, resolve);
|
||||||
|
line(out, depth, "while");
|
||||||
|
write_ops(out, inner_test, depth + 1, resolve);
|
||||||
|
line(out, depth, "while");
|
||||||
|
write_ops(out, body, depth + 1, resolve);
|
||||||
|
line(out, depth, "repeat");
|
||||||
|
write_ops(out, after_repeat, depth + 1, resolve);
|
||||||
|
if let Some(eb) = else_body {
|
||||||
|
line(out, depth, "else");
|
||||||
|
write_ops(out, eb, depth + 1, resolve);
|
||||||
|
}
|
||||||
|
line(out, depth, "then");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
IrOp::Exit => "exit".into(),
|
||||||
|
IrOp::LoopRestartIfFalse => "loop-restart-if-false".into(),
|
||||||
|
|
||||||
|
// -- Flat forward branches --
|
||||||
|
IrOp::Block(l) => format!("block L{l}"),
|
||||||
|
IrOp::BranchIfFalse(l) => format!("branch-if-false L{l}"),
|
||||||
|
IrOp::EndBlock(l) => format!("end-block L{l}"),
|
||||||
|
|
||||||
|
// -- Return stack --
|
||||||
|
IrOp::ToR => ">r".into(),
|
||||||
|
IrOp::FromR => "r>".into(),
|
||||||
|
IrOp::RFetch => "r@".into(),
|
||||||
|
IrOp::LoopJ => "j".into(),
|
||||||
|
|
||||||
|
// -- Forth locals --
|
||||||
|
IrOp::ForthLocalGet(n) => format!("local@ {n}"),
|
||||||
|
IrOp::ForthLocalSet(n) => format!("local! {n}"),
|
||||||
|
IrOp::ForthFLocalGet(n) => format!("flocal@ {n}"),
|
||||||
|
IrOp::ForthFLocalSet(n) => format!("flocal! {n}"),
|
||||||
|
|
||||||
|
// -- I/O --
|
||||||
|
IrOp::Emit => "emit".into(),
|
||||||
|
IrOp::Dot => ".".into(),
|
||||||
|
IrOp::Cr => "cr".into(),
|
||||||
|
IrOp::Type => "type".into(),
|
||||||
|
|
||||||
|
// -- System --
|
||||||
|
IrOp::Execute => "execute".into(),
|
||||||
|
IrOp::SpFetch => "sp@".into(),
|
||||||
|
IrOp::RpFetch => "rp@".into(),
|
||||||
|
|
||||||
|
// -- Float stack --
|
||||||
|
IrOp::FDup => "fdup".into(),
|
||||||
|
IrOp::FDrop => "fdrop".into(),
|
||||||
|
IrOp::FSwap => "fswap".into(),
|
||||||
|
IrOp::FOver => "fover".into(),
|
||||||
|
|
||||||
|
// -- Float arithmetic --
|
||||||
|
IrOp::FAdd => "fadd".into(),
|
||||||
|
IrOp::FSub => "fsub".into(),
|
||||||
|
IrOp::FMul => "fmul".into(),
|
||||||
|
IrOp::FDiv => "fdiv".into(),
|
||||||
|
IrOp::FNegate => "fnegate".into(),
|
||||||
|
IrOp::FAbs => "fabs".into(),
|
||||||
|
IrOp::FSqrt => "fsqrt".into(),
|
||||||
|
IrOp::FMin => "fmin".into(),
|
||||||
|
IrOp::FMax => "fmax".into(),
|
||||||
|
IrOp::FFloor => "ffloor".into(),
|
||||||
|
IrOp::FRound => "fround".into(),
|
||||||
|
|
||||||
|
// -- Float comparisons --
|
||||||
|
IrOp::FZeroEq => "f0=".into(),
|
||||||
|
IrOp::FZeroLt => "f0<".into(),
|
||||||
|
IrOp::FEq => "f=".into(),
|
||||||
|
IrOp::FLt => "f<".into(),
|
||||||
|
|
||||||
|
// -- Float memory --
|
||||||
|
IrOp::FetchFloat => "f@".into(),
|
||||||
|
IrOp::StoreFloat => "f!".into(),
|
||||||
|
|
||||||
|
// -- Conversions --
|
||||||
|
IrOp::StoF => "s>f".into(),
|
||||||
|
IrOp::FtoS => "f>s".into(),
|
||||||
|
};
|
||||||
|
line(out, depth, &simple);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn simple_ops_one_per_line() {
|
||||||
|
let out = format_ir(&[IrOp::Dup, IrOp::Mul, IrOp::PushI32(7)]);
|
||||||
|
assert_eq!(out, "dup\nmul\npush 7\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn call_resolves_via_resolver() {
|
||||||
|
let ops = [IrOp::Call(WordId(12)), IrOp::TailCall(WordId(13))];
|
||||||
|
assert_eq!(format_ir(&ops), "call #12\ntail-call #13\n");
|
||||||
|
let named = format_ir_with(&ops, &|id| (id.0 == 12).then(|| "SQ".to_string()));
|
||||||
|
assert_eq!(named, "call SQ\ntail-call #13\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn nested_if_inside_do_loop_indents() {
|
||||||
|
let ops = [IrOp::DoLoop {
|
||||||
|
body: vec![
|
||||||
|
IrOp::Dup,
|
||||||
|
IrOp::If {
|
||||||
|
then_body: vec![IrOp::Dup, IrOp::Mul],
|
||||||
|
else_body: Some(vec![IrOp::Drop]),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
is_plus_loop: false,
|
||||||
|
}];
|
||||||
|
let expected = "do\n dup\n if\n dup\n mul\n else\n drop\n then\nloop\n";
|
||||||
|
assert_eq!(format_ir(&ops), expected);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn while_loops_and_flat_branches() {
|
||||||
|
let ops = [
|
||||||
|
IrOp::BeginWhileRepeat {
|
||||||
|
test: vec![IrOp::Dup],
|
||||||
|
body: vec![IrOp::PushI32(1), IrOp::Sub],
|
||||||
|
},
|
||||||
|
IrOp::Block(3),
|
||||||
|
IrOp::BranchIfFalse(3),
|
||||||
|
IrOp::EndBlock(3),
|
||||||
|
];
|
||||||
|
let expected = "begin\n dup\nwhile\n push 1\n sub\nrepeat\nblock L3\nbranch-if-false L3\nend-block L3\n";
|
||||||
|
assert_eq!(format_ir(&ops), expected);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn every_simple_variant_renders() {
|
||||||
|
// One of each non-structured op; count of output lines must match.
|
||||||
|
let ops = vec![
|
||||||
|
IrOp::PushI32(1),
|
||||||
|
IrOp::PushI64(2),
|
||||||
|
IrOp::PushF64(1.5),
|
||||||
|
IrOp::Drop,
|
||||||
|
IrOp::Dup,
|
||||||
|
IrOp::Swap,
|
||||||
|
IrOp::Over,
|
||||||
|
IrOp::Rot,
|
||||||
|
IrOp::Nip,
|
||||||
|
IrOp::Tuck,
|
||||||
|
IrOp::TwoDup,
|
||||||
|
IrOp::TwoDrop,
|
||||||
|
IrOp::Add,
|
||||||
|
IrOp::Sub,
|
||||||
|
IrOp::Mul,
|
||||||
|
IrOp::DivMod,
|
||||||
|
IrOp::Negate,
|
||||||
|
IrOp::Abs,
|
||||||
|
IrOp::Eq,
|
||||||
|
IrOp::NotEq,
|
||||||
|
IrOp::Lt,
|
||||||
|
IrOp::Gt,
|
||||||
|
IrOp::LtUnsigned,
|
||||||
|
IrOp::ZeroEq,
|
||||||
|
IrOp::ZeroLt,
|
||||||
|
IrOp::And,
|
||||||
|
IrOp::Or,
|
||||||
|
IrOp::Xor,
|
||||||
|
IrOp::Invert,
|
||||||
|
IrOp::Lshift,
|
||||||
|
IrOp::Rshift,
|
||||||
|
IrOp::ArithRshift,
|
||||||
|
IrOp::Fetch,
|
||||||
|
IrOp::Store,
|
||||||
|
IrOp::CFetch,
|
||||||
|
IrOp::CStore,
|
||||||
|
IrOp::PlusStore,
|
||||||
|
IrOp::Call(WordId(1)),
|
||||||
|
IrOp::TailCall(WordId(2)),
|
||||||
|
IrOp::Exit,
|
||||||
|
IrOp::LoopRestartIfFalse,
|
||||||
|
IrOp::Block(1),
|
||||||
|
IrOp::BranchIfFalse(1),
|
||||||
|
IrOp::EndBlock(1),
|
||||||
|
IrOp::ToR,
|
||||||
|
IrOp::FromR,
|
||||||
|
IrOp::RFetch,
|
||||||
|
IrOp::LoopJ,
|
||||||
|
IrOp::ForthLocalGet(0),
|
||||||
|
IrOp::ForthLocalSet(0),
|
||||||
|
IrOp::ForthFLocalGet(0),
|
||||||
|
IrOp::ForthFLocalSet(0),
|
||||||
|
IrOp::Emit,
|
||||||
|
IrOp::Dot,
|
||||||
|
IrOp::Cr,
|
||||||
|
IrOp::Type,
|
||||||
|
IrOp::Execute,
|
||||||
|
IrOp::SpFetch,
|
||||||
|
IrOp::RpFetch,
|
||||||
|
IrOp::FDup,
|
||||||
|
IrOp::FDrop,
|
||||||
|
IrOp::FSwap,
|
||||||
|
IrOp::FOver,
|
||||||
|
IrOp::FAdd,
|
||||||
|
IrOp::FSub,
|
||||||
|
IrOp::FMul,
|
||||||
|
IrOp::FDiv,
|
||||||
|
IrOp::FNegate,
|
||||||
|
IrOp::FAbs,
|
||||||
|
IrOp::FSqrt,
|
||||||
|
IrOp::FMin,
|
||||||
|
IrOp::FMax,
|
||||||
|
IrOp::FFloor,
|
||||||
|
IrOp::FRound,
|
||||||
|
IrOp::FZeroEq,
|
||||||
|
IrOp::FZeroLt,
|
||||||
|
IrOp::FEq,
|
||||||
|
IrOp::FLt,
|
||||||
|
IrOp::FetchFloat,
|
||||||
|
IrOp::StoreFloat,
|
||||||
|
IrOp::StoF,
|
||||||
|
IrOp::FtoS,
|
||||||
|
];
|
||||||
|
let out = format_ir(&ops);
|
||||||
|
assert_eq!(out.lines().count(), ops.len());
|
||||||
|
// Every line non-empty, no accidental blank rendering.
|
||||||
|
assert!(out.lines().all(|l| !l.trim().is_empty()));
|
||||||
|
}
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
+215
-69
@@ -1,8 +1,10 @@
|
|||||||
#![allow(dead_code)]
|
#![allow(dead_code)]
|
||||||
//! Cross-engine comparison tests: WAFER vs gforth.
|
//! Cross-engine comparison tests: WAFER vs gforth (and `SwiftForth` for perf).
|
||||||
//!
|
//!
|
||||||
//! Validates that WAFER produces identical output to gforth for standard
|
//! Validates that WAFER produces identical output to gforth for standard
|
||||||
//! Forth programs, and benchmarks performance of both engines.
|
//! Forth programs, and benchmarks performance of the engines. `SwiftForth`
|
||||||
|
//! (`sf64`, native-code commercial compiler) joins the performance report
|
||||||
|
//! as an upper-bound reference when installed.
|
||||||
//!
|
//!
|
||||||
//! WAFER-only correctness: `cargo test -p wafer-core --test comparison`
|
//! WAFER-only correctness: `cargo test -p wafer-core --test comparison`
|
||||||
//! Full comparison + perf: `cargo test -p wafer-core --test comparison -- --nocapture --ignored`
|
//! Full comparison + perf: `cargo test -p wafer-core --test comparison -- --nocapture --ignored`
|
||||||
@@ -26,8 +28,7 @@ fn probe_gforth(candidate: &str) -> bool {
|
|||||||
.arg("-e")
|
.arg("-e")
|
||||||
.arg("bye")
|
.arg("bye")
|
||||||
.output()
|
.output()
|
||||||
.map(|o| o.status.success())
|
.is_ok_and(|o| o.status.success())
|
||||||
.unwrap_or(false)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn find_gforth() -> Option<&'static str> {
|
fn find_gforth() -> Option<&'static str> {
|
||||||
@@ -64,6 +65,48 @@ fn find_gforth_fast() -> Option<&'static str> {
|
|||||||
.as_deref()
|
.as_deref()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// -----------------------------------------------------------------------
|
||||||
|
// SwiftForth (sf64) discovery (cached)
|
||||||
|
// -----------------------------------------------------------------------
|
||||||
|
|
||||||
|
static SF64_PATH: OnceLock<Option<String>> = OnceLock::new();
|
||||||
|
|
||||||
|
/// Probe sf64 by piping `bye` via stdin — sf64 has no `-e` flag; it takes
|
||||||
|
/// Forth source from stdin or as bare command-line arguments.
|
||||||
|
fn probe_sf64(candidate: &str) -> bool {
|
||||||
|
run_via_stdin(candidate, "bye\n").is_some_and(|o| o.status.success())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn find_sf64() -> Option<&'static str> {
|
||||||
|
SF64_PATH
|
||||||
|
.get_or_init(|| {
|
||||||
|
for candidate in &["/Applications/ForthInc/SwiftForth/bin/macos/sf64", "sf64"] {
|
||||||
|
if probe_sf64(candidate) {
|
||||||
|
return Some(candidate.to_string());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None
|
||||||
|
})
|
||||||
|
.as_deref()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Spawn `binary`, write `input` to its stdin, and collect the output.
|
||||||
|
fn run_via_stdin(binary: &str, input: &str) -> Option<std::process::Output> {
|
||||||
|
Command::new(binary)
|
||||||
|
// Perf lanes measure unguarded code (only the wafer binary reads this)
|
||||||
|
.env("WAFER_STACK_GUARDS", "0")
|
||||||
|
.stdin(std::process::Stdio::piped())
|
||||||
|
.stdout(std::process::Stdio::piped())
|
||||||
|
.stderr(std::process::Stdio::piped())
|
||||||
|
.spawn()
|
||||||
|
.and_then(|mut child| {
|
||||||
|
use std::io::Write;
|
||||||
|
child.stdin.take().unwrap().write_all(input.as_bytes())?;
|
||||||
|
child.wait_with_output()
|
||||||
|
})
|
||||||
|
.ok()
|
||||||
|
}
|
||||||
|
|
||||||
// -----------------------------------------------------------------------
|
// -----------------------------------------------------------------------
|
||||||
// Engine runners
|
// Engine runners
|
||||||
// -----------------------------------------------------------------------
|
// -----------------------------------------------------------------------
|
||||||
@@ -410,6 +453,26 @@ 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,
|
||||||
|
},
|
||||||
|
// QUIT is deliberately absent from this corpus: what it abandons is
|
||||||
|
// "the input source", and each engine here is fed differently (wafer
|
||||||
|
// line by line, gforth from a file, sf64 from a prompting stdin), so
|
||||||
|
// a comparison would measure the harness. Its semantics are pinned by
|
||||||
|
// the QUIT tests in outer.rs, checked by hand against both engines.
|
||||||
// -- Strings --
|
// -- Strings --
|
||||||
Program {
|
Program {
|
||||||
name: "s-quote-type",
|
name: "s-quote-type",
|
||||||
@@ -581,6 +644,81 @@ fn compare_all_programs() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// -----------------------------------------------------------------------
|
||||||
|
// Cross-engine behavioral comparison (requires SwiftForth sf64) -- WS-003
|
||||||
|
// -----------------------------------------------------------------------
|
||||||
|
|
||||||
|
/// Run Forth code through `SwiftForth`. Piped sf64 is quiet (no banner, no
|
||||||
|
/// `ok` echo), truncates input lines at ~256 chars, and exits 243 after an
|
||||||
|
/// error, so statements are fed one per line with a final `bye`.
|
||||||
|
fn run_sf64_code(sf64: &str, code: &str) -> Option<EngineResult> {
|
||||||
|
let mut input = String::new();
|
||||||
|
for line in code.lines() {
|
||||||
|
let t = line.trim();
|
||||||
|
if !t.is_empty() {
|
||||||
|
input.push_str(t);
|
||||||
|
input.push('\n');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
input.push_str("bye\n");
|
||||||
|
let out = run_via_stdin(sf64, &input)?;
|
||||||
|
Some(EngineResult {
|
||||||
|
output: String::from_utf8_lossy(&out.stdout).to_string(),
|
||||||
|
success: out.status.success(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Correctness lane against `SwiftForth`: the same program corpus as the
|
||||||
|
/// gforth comparison, sf64 as the oracle. Skips gracefully when sf64 is
|
||||||
|
/// not installed (CI/linux). Programs listed in `SF64_SKIP` use words or
|
||||||
|
/// output conventions `SwiftForth` does not share.
|
||||||
|
#[test]
|
||||||
|
#[ignore = "requires SwiftForth sf64 (run with -- --ignored)"]
|
||||||
|
fn compare_all_programs_sf64() {
|
||||||
|
// dot-quote: `."` outside a definition is a no-op in SwiftForth
|
||||||
|
// (compile-only); WAFER supports the interpret-mode extension.
|
||||||
|
const SF64_SKIP: &[&str] = &["dot-quote"];
|
||||||
|
let Some(sf64) = find_sf64() else {
|
||||||
|
eprintln!("SKIP: sf64 not found");
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let progs = programs();
|
||||||
|
let mut passed = 0;
|
||||||
|
let mut skipped = 0;
|
||||||
|
for prog in &progs {
|
||||||
|
if SF64_SKIP.contains(&prog.name) {
|
||||||
|
skipped += 1;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let wafer = run_wafer(prog.code);
|
||||||
|
assert!(wafer.success, "{}: WAFER execution failed", prog.name);
|
||||||
|
let Some(sf) = run_sf64_code(sf64, prog.code) else {
|
||||||
|
skipped += 1;
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
if !sf.success {
|
||||||
|
eprintln!(" WARN {}: sf64 execution failed, skipping", prog.name);
|
||||||
|
skipped += 1;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
// SwiftForth prints numbers space-prefixed and echoes piped input
|
||||||
|
// lines, so byte-exact comparison is meaningless; compare the
|
||||||
|
// whitespace-token stream (the printed values and strings).
|
||||||
|
let wafer_tokens: Vec<&str> = wafer.output.split_whitespace().collect();
|
||||||
|
let sf_tokens: Vec<&str> = sf.output.split_whitespace().collect();
|
||||||
|
assert_eq!(
|
||||||
|
wafer_tokens, sf_tokens,
|
||||||
|
"{}: output differs\n WAFER: {:?}\n sf64: {:?}",
|
||||||
|
prog.name, wafer.output, sf.output
|
||||||
|
);
|
||||||
|
passed += 1;
|
||||||
|
}
|
||||||
|
eprintln!(
|
||||||
|
"\nsf64 behavioral comparison: {passed} passed, {skipped} skipped (of {})",
|
||||||
|
progs.len()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// -----------------------------------------------------------------------
|
// -----------------------------------------------------------------------
|
||||||
// Performance comparison (requires gforth)
|
// Performance comparison (requires gforth)
|
||||||
// -----------------------------------------------------------------------
|
// -----------------------------------------------------------------------
|
||||||
@@ -608,37 +746,37 @@ fn perf_benchmarks() -> Vec<PerfBenchmark> {
|
|||||||
verify: "25 FIB",
|
verify: "25 FIB",
|
||||||
expected: 75025,
|
expected: 75025,
|
||||||
samples: 5,
|
samples: 5,
|
||||||
max_ratio: 0.65,
|
max_ratio: 0.17,
|
||||||
},
|
},
|
||||||
PerfBenchmark {
|
PerfBenchmark {
|
||||||
name: "Factorial(12)x10K",
|
name: "Factorial(12)x100K",
|
||||||
define: ": FACT 1 SWAP 1+ 1 ?DO I * LOOP ; \
|
define: ": FACT 1 SWAP 1+ 1 ?DO I * LOOP ; \
|
||||||
: FACT-BENCH 10000 0 DO 12 FACT DROP LOOP ;",
|
: FACT-BENCH 100000 0 DO 12 FACT DROP LOOP ;",
|
||||||
run_code: "FACT-BENCH",
|
run_code: "FACT-BENCH",
|
||||||
verify: "12 FACT",
|
verify: "12 FACT",
|
||||||
expected: 479001600,
|
expected: 479001600,
|
||||||
samples: 5,
|
samples: 5,
|
||||||
max_ratio: 0.75,
|
max_ratio: 0.12,
|
||||||
},
|
},
|
||||||
PerfBenchmark {
|
PerfBenchmark {
|
||||||
name: "GCD-bench(500)",
|
name: "GCD-bench(20K)",
|
||||||
define: ": GCD BEGIN DUP WHILE TUCK MOD REPEAT DROP ; \
|
define: ": GCD BEGIN DUP WHILE TUCK MOD REPEAT DROP ; \
|
||||||
: GCD-BENCH 0 DO 10000 I 1+ GCD DROP LOOP ;",
|
: GCD-BENCH 0 DO 10000 I 1+ GCD DROP LOOP ;",
|
||||||
run_code: "500 GCD-BENCH",
|
run_code: "20000 GCD-BENCH",
|
||||||
verify: "48 36 GCD",
|
verify: "48 36 GCD",
|
||||||
expected: 12,
|
expected: 12,
|
||||||
samples: 5,
|
samples: 5,
|
||||||
max_ratio: 0.70,
|
max_ratio: 0.45,
|
||||||
},
|
},
|
||||||
PerfBenchmark {
|
PerfBenchmark {
|
||||||
name: "NestedLoops(50)",
|
name: "NestedLoops(50)x1K",
|
||||||
define: ": NESTED 0 SWAP 0 DO I 0 ?DO I J + DROP LOOP LOOP ; \
|
define: ": NESTED 0 SWAP 0 DO I 0 ?DO I J + DROP LOOP LOOP ; \
|
||||||
: NESTED-BENCH 100 0 DO 50 NESTED DROP LOOP ;",
|
: NESTED-BENCH 1000 0 DO 50 NESTED DROP LOOP ;",
|
||||||
run_code: "NESTED-BENCH",
|
run_code: "NESTED-BENCH",
|
||||||
verify: "5 NESTED",
|
verify: "5 NESTED",
|
||||||
expected: 0,
|
expected: 0,
|
||||||
samples: 3,
|
samples: 5,
|
||||||
max_ratio: 0.20,
|
max_ratio: 0.11,
|
||||||
},
|
},
|
||||||
PerfBenchmark {
|
PerfBenchmark {
|
||||||
name: "Collatz(2K)",
|
name: "Collatz(2K)",
|
||||||
@@ -650,7 +788,7 @@ fn perf_benchmarks() -> Vec<PerfBenchmark> {
|
|||||||
verify: "27 COLLATZ",
|
verify: "27 COLLATZ",
|
||||||
expected: 111,
|
expected: 111,
|
||||||
samples: 3,
|
samples: 3,
|
||||||
max_ratio: 0.45,
|
max_ratio: 0.08,
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -699,31 +837,11 @@ fn measure_wafer_release(wafer: &str, bench: &PerfBenchmark) -> Option<u64> {
|
|||||||
define = bench.define,
|
define = bench.define,
|
||||||
run = bench.run_code,
|
run = bench.run_code,
|
||||||
);
|
);
|
||||||
let output = Command::new(wafer)
|
let output = run_via_stdin(wafer, &code)?;
|
||||||
.stdin(std::process::Stdio::piped())
|
|
||||||
.stdout(std::process::Stdio::piped())
|
|
||||||
.stderr(std::process::Stdio::piped())
|
|
||||||
.spawn()
|
|
||||||
.and_then(|mut child| {
|
|
||||||
use std::io::Write;
|
|
||||||
child.stdin.take().unwrap().write_all(code.as_bytes())?;
|
|
||||||
child.wait_with_output()
|
|
||||||
})
|
|
||||||
.ok()?;
|
|
||||||
if !output.status.success() {
|
if !output.status.success() {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
let stdout = String::from_utf8_lossy(&output.stdout);
|
median_printed_time(&output.stdout)
|
||||||
let mut times: Vec<u64> = stdout
|
|
||||||
.trim()
|
|
||||||
.lines()
|
|
||||||
.filter_map(|l| l.trim().parse::<u64>().ok())
|
|
||||||
.collect();
|
|
||||||
times.sort();
|
|
||||||
if times.is_empty() {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
Some(times[times.len() / 2])
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Measure WAFER execution time after CONSOLIDATE (direct calls between all words).
|
/// Measure WAFER execution time after CONSOLIDATE (direct calls between all words).
|
||||||
@@ -735,21 +853,17 @@ fn measure_wafer_consolidated(wafer: &str, bench: &PerfBenchmark) -> Option<u64>
|
|||||||
define = bench.define,
|
define = bench.define,
|
||||||
run = bench.run_code,
|
run = bench.run_code,
|
||||||
);
|
);
|
||||||
let output = Command::new(wafer)
|
let output = run_via_stdin(wafer, &code)?;
|
||||||
.stdin(std::process::Stdio::piped())
|
|
||||||
.stdout(std::process::Stdio::piped())
|
|
||||||
.stderr(std::process::Stdio::piped())
|
|
||||||
.spawn()
|
|
||||||
.and_then(|mut child| {
|
|
||||||
use std::io::Write;
|
|
||||||
child.stdin.take().unwrap().write_all(code.as_bytes())?;
|
|
||||||
child.wait_with_output()
|
|
||||||
})
|
|
||||||
.ok()?;
|
|
||||||
if !output.status.success() {
|
if !output.status.success() {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
let stdout = String::from_utf8_lossy(&output.stdout);
|
median_printed_time(&output.stdout)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Parse the microsecond values printed by TIMED-BENCH (one per line) and
|
||||||
|
/// return the median.
|
||||||
|
fn median_printed_time(stdout: &[u8]) -> Option<u64> {
|
||||||
|
let stdout = String::from_utf8_lossy(stdout);
|
||||||
let mut times: Vec<u64> = stdout
|
let mut times: Vec<u64> = stdout
|
||||||
.trim()
|
.trim()
|
||||||
.lines()
|
.lines()
|
||||||
@@ -779,18 +893,28 @@ fn measure_gforth(gforth: &str, bench: &PerfBenchmark) -> Option<u64> {
|
|||||||
if !output.status.success() {
|
if !output.status.success() {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
let stdout = String::from_utf8_lossy(&output.stdout);
|
median_printed_time(&output.stdout)
|
||||||
// Parse the 3 timing values and take the median
|
}
|
||||||
let mut times: Vec<u64> = stdout
|
|
||||||
.trim()
|
/// Measure `SwiftForth` (`sf64`) execution time using Forth-level `ucounter`
|
||||||
.lines()
|
/// (double-cell microsecond counter; `2swap d- drop` yields elapsed us —
|
||||||
.filter_map(|l| l.trim().parse::<u64>().ok())
|
/// the same wrapper shape as gforth's `utime`). Timing excludes startup.
|
||||||
.collect();
|
/// sf64 has no `-e` flag, so the program is piped via stdin — one statement
|
||||||
times.sort();
|
/// per line, because sf64 truncates input lines at ~256 chars.
|
||||||
if times.is_empty() {
|
/// Returns microseconds, or None if sf64 is unavailable or fails.
|
||||||
|
fn measure_sf64(sf64: &str, bench: &PerfBenchmark) -> Option<u64> {
|
||||||
|
let code = format!(
|
||||||
|
"{define}\n{run}\n\
|
||||||
|
: TIMED-BENCH ucounter {run} ucounter 2swap d- drop . cr ;\n\
|
||||||
|
TIMED-BENCH\nTIMED-BENCH\nTIMED-BENCH\nbye\n",
|
||||||
|
define = bench.define,
|
||||||
|
run = bench.run_code,
|
||||||
|
);
|
||||||
|
let output = run_via_stdin(sf64, &code)?;
|
||||||
|
if !output.status.success() {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
Some(times[times.len() / 2])
|
median_printed_time(&output.stdout)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -831,18 +955,31 @@ fn performance_report() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
let sep = "=".repeat(80);
|
let sf64 = find_sf64();
|
||||||
let thin = "-".repeat(80);
|
if sf64.is_none() {
|
||||||
|
eprintln!("NOTE: sf64 (SwiftForth) not found — column skipped");
|
||||||
|
}
|
||||||
|
|
||||||
|
let sep = "=".repeat(100);
|
||||||
|
let thin = "-".repeat(100);
|
||||||
println!("\n{sep}");
|
println!("\n{sep}");
|
||||||
println!(" WAFER vs Gforth Performance Comparison (release mode)");
|
println!(" WAFER vs Gforth vs SwiftForth Performance Comparison (release mode)");
|
||||||
println!("{sep}\n");
|
println!("{sep}\n");
|
||||||
println!(
|
println!(
|
||||||
"{:<22} {:>10} {:>10} {:>10} {:>10} {:>10} {:>10}",
|
"{:<22} {:>9} {:>9} {:>9} {:>9} {:>9} {:>9} {:>9} {:>9}",
|
||||||
"Benchmark", "WAFER", "CONSOL", "gforth", "gf-fast", "WAFER/gf", "limit"
|
"Benchmark",
|
||||||
|
"WAFER",
|
||||||
|
"CONSOL",
|
||||||
|
"gforth",
|
||||||
|
"gf-fast",
|
||||||
|
"sf64",
|
||||||
|
"WAFER/gf",
|
||||||
|
"WAFER/sf",
|
||||||
|
"limit"
|
||||||
);
|
);
|
||||||
println!(
|
println!(
|
||||||
"{:<22} {:>10} {:>10} {:>10} {:>10} {:>10} {:>10}",
|
"{:<22} {:>9} {:>9} {:>9} {:>9} {:>9} {:>9} {:>9} {:>9}",
|
||||||
"", "(us)", "(us)", "(us)", "(us)", "", ""
|
"", "(us)", "(us)", "(us)", "(us)", "(us)", "", "", ""
|
||||||
);
|
);
|
||||||
println!("{thin}");
|
println!("{thin}");
|
||||||
|
|
||||||
@@ -857,9 +994,11 @@ fn performance_report() {
|
|||||||
.unwrap_or(0);
|
.unwrap_or(0);
|
||||||
let gf = gforth.and_then(|g| measure_gforth(g, bench));
|
let gf = gforth.and_then(|g| measure_gforth(g, bench));
|
||||||
let gf_fast = gforth_fast.and_then(|g| measure_gforth(g, bench));
|
let gf_fast = gforth_fast.and_then(|g| measure_gforth(g, bench));
|
||||||
|
let sf = sf64.and_then(|s| measure_sf64(s, bench));
|
||||||
|
|
||||||
let gf_str = gf.map_or_else(|| "-".to_string(), |v| format!("{v}"));
|
let gf_str = gf.map_or_else(|| "-".to_string(), |v| format!("{v}"));
|
||||||
let gf_fast_str = gf_fast.map_or_else(|| "-".to_string(), |v| format!("{v}"));
|
let gf_fast_str = gf_fast.map_or_else(|| "-".to_string(), |v| format!("{v}"));
|
||||||
|
let sf_str = sf.map_or_else(|| "-".to_string(), |v| format!("{v}"));
|
||||||
let best_wafer = if consol > 0 && consol < wafer {
|
let best_wafer = if consol > 0 && consol < wafer {
|
||||||
consol
|
consol
|
||||||
} else {
|
} else {
|
||||||
@@ -873,11 +1012,15 @@ fn performance_report() {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
let ratio = ratio_val.map_or_else(|| "-".to_string(), |r| format!("{r:.2}x"));
|
let ratio = ratio_val.map_or_else(|| "-".to_string(), |r| format!("{r:.2}x"));
|
||||||
|
let sf_ratio = sf.filter(|&s| s > 0).map_or_else(
|
||||||
|
|| "-".to_string(),
|
||||||
|
|s| format!("{:.2}x", best_wafer as f64 / s as f64),
|
||||||
|
);
|
||||||
let limit_str = format!("{:.2}x", bench.max_ratio);
|
let limit_str = format!("{:.2}x", bench.max_ratio);
|
||||||
|
|
||||||
println!(
|
println!(
|
||||||
"{:<22} {:>10} {:>10} {:>10} {:>10} {:>10} {:>10}",
|
"{:<22} {:>9} {:>9} {:>9} {:>9} {:>9} {:>9} {:>9} {:>9}",
|
||||||
bench.name, wafer, consol, gf_str, gf_fast_str, ratio, limit_str
|
bench.name, wafer, consol, gf_str, gf_fast_str, sf_str, ratio, sf_ratio, limit_str
|
||||||
);
|
);
|
||||||
|
|
||||||
// Check regression limits
|
// Check regression limits
|
||||||
@@ -900,6 +1043,9 @@ fn performance_report() {
|
|||||||
println!("{thin}");
|
println!("{thin}");
|
||||||
println!(" WAFER = all optimizations, CONSOL = after CONSOLIDATE");
|
println!(" WAFER = all optimizations, CONSOL = after CONSOLIDATE");
|
||||||
println!(" WAFER/gf = best(WAFER,CONSOL) vs gforth, < 1.0 means WAFER faster");
|
println!(" WAFER/gf = best(WAFER,CONSOL) vs gforth, < 1.0 means WAFER faster");
|
||||||
|
println!(
|
||||||
|
" WAFER/sf = best(WAFER,CONSOL) vs SwiftForth sf64 (native code; informational, no limit)"
|
||||||
|
);
|
||||||
println!("{sep}\n");
|
println!("{sep}\n");
|
||||||
|
|
||||||
if !regressions.is_empty() {
|
if !regressions.is_empty() {
|
||||||
|
|||||||
+214
-24
@@ -13,41 +13,170 @@ const SUITE_DIR: &str = concat!(
|
|||||||
"/../../tests/forth2012-test-suite/src"
|
"/../../tests/forth2012-test-suite/src"
|
||||||
);
|
);
|
||||||
|
|
||||||
/// Load a file and evaluate it line by line, ignoring errors on individual lines.
|
/// Load a file line-by-line, returning the number of lines that raised an
|
||||||
fn load_file(vm: &mut ForthVM<NativeRuntime>, path: &str) {
|
/// `evaluate` error. Each failing line is printed (visible under
|
||||||
|
/// `cargo test -- --nocapture`) so failures can be triaged without a
|
||||||
|
/// debugger.
|
||||||
|
///
|
||||||
|
/// Historically this helper discarded errors silently, which caused tests
|
||||||
|
/// like LT32 in `localstest.fth` (compile errors from unknown words such
|
||||||
|
/// as `(LOCAL)` before it was implemented) to vanish — the T{ }T error
|
||||||
|
/// counter was never incremented because the `:` definition never ran.
|
||||||
|
/// Returning the count surfaces silent skips as real failures.
|
||||||
|
///
|
||||||
|
/// **Note on multi-line definitions.** WAFER's DOES> handler collects
|
||||||
|
/// the does-body to `;` via `next_token()` within a *single* `evaluate`
|
||||||
|
/// call and treats end-of-input as end-of-body. Files with a `DOES>`
|
||||||
|
/// split across lines (e.g. `errorreport.fth`) therefore cannot be
|
||||||
|
/// loaded line-by-line; use [`load_file_whole`] for those.
|
||||||
|
fn load_file(vm: &mut ForthVM<NativeRuntime>, path: &str) -> u32 {
|
||||||
let source = std::fs::read_to_string(path).unwrap_or_else(|_| panic!("Failed to read {path}"));
|
let source = std::fs::read_to_string(path).unwrap_or_else(|_| panic!("Failed to read {path}"));
|
||||||
for line in source.lines() {
|
let mut fails = 0u32;
|
||||||
let _ = vm.evaluate(line);
|
for (lineno, line) in source.lines().enumerate() {
|
||||||
|
if let Err(e) = vm.evaluate(line) {
|
||||||
|
fails += 1;
|
||||||
|
eprintln!("{path}:{}: {e}\n line: {line}", lineno + 1);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
vm.take_output(); // discard output
|
vm.take_output(); // discard output
|
||||||
|
fails
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Load a file as a single `evaluate` call (not line-by-line). Required
|
||||||
|
/// for files with multi-line definitions that WAFER's per-line handlers
|
||||||
|
/// can't stitch across calls (notably `: X ... DOES> ... ;` spanning
|
||||||
|
/// lines — see [`load_file`] note).
|
||||||
|
///
|
||||||
|
/// Returns `1` on any failure, `0` on success, so the caller can apply
|
||||||
|
/// baselines the same way as [`load_file`].
|
||||||
|
fn load_file_whole(vm: &mut ForthVM<NativeRuntime>, path: &str) -> u32 {
|
||||||
|
let source = std::fs::read_to_string(path).unwrap_or_else(|_| panic!("Failed to read {path}"));
|
||||||
|
let fails = match vm.evaluate(&source) {
|
||||||
|
Ok(()) => 0,
|
||||||
|
Err(e) => {
|
||||||
|
eprintln!("{path}: {e}");
|
||||||
|
1
|
||||||
|
}
|
||||||
|
};
|
||||||
|
vm.take_output();
|
||||||
|
fails
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Baseline of *known* line-level failures per prerequisite file. The runner
|
||||||
|
/// asserts `load_fails == expected_load_failures(path)`, so any regression
|
||||||
|
/// above (or silently-fixed case below) the baseline is caught.
|
||||||
|
///
|
||||||
|
/// Baselines are not an allowlist to paper over bugs — they are an explicit
|
||||||
|
/// tech-debt ledger. Each non-zero entry here is a bug that should be fixed
|
||||||
|
/// and the baseline lowered to zero. See the in-tree follow-up tasks.
|
||||||
|
fn expected_load_failures(path: &str) -> u32 {
|
||||||
|
// core.fr exercises two constructs WAFER does not yet support:
|
||||||
|
// 1. Nested colon definitions (`: NOP : POSTPONE ; ;` at line 751,
|
||||||
|
// defining NOP, NOP1, NOP2 — four silent lines).
|
||||||
|
// 2. `SOURCE`/`>IN` round-trip through `EVALUATE` at line 797
|
||||||
|
// (GS1 definition) — one line.
|
||||||
|
// Total: 5. Fix these and drop the baseline to 0.
|
||||||
|
if path.ends_with("/core.fr") {
|
||||||
|
return 5;
|
||||||
|
}
|
||||||
|
// coreexttest.fth uses two Core-Extension features WAFER lacks:
|
||||||
|
// 1. SAVE-INPUT / RESTORE-INPUT at line 548 — not implemented.
|
||||||
|
// 2. `.(` inside `[ ... ]` brackets at line 559 — `.(` isn't
|
||||||
|
// handled by `compile_token`'s `[ ... ]` interpret-mode path,
|
||||||
|
// so `First message via .(` tokens leak to the compiler as
|
||||||
|
// undefined words.
|
||||||
|
// Total: 2. Fix these and drop the baseline to 0.
|
||||||
|
if path.ends_with("/coreexttest.fth") {
|
||||||
|
return 2;
|
||||||
|
}
|
||||||
|
// exceptiontest.fth line 95 fails with a garbled parse ("unknown word"
|
||||||
|
// over non-ASCII bytes): WAFER's parser reads past a prior test's
|
||||||
|
// scratch region after the preceding `C6` / `T9` frame exercises
|
||||||
|
// CATCH/THROW source stacking. Root cause not yet diagnosed; baseline
|
||||||
|
// until fixed.
|
||||||
|
if path.ends_with("/exceptiontest.fth") {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
// toolstest.fth uses the `\?` conditional-skip idiom defined in
|
||||||
|
// utilities.fth:37 as `: \? (\?) @ IF EXIT THEN SOURCE >IN ! DROP ;
|
||||||
|
// IMMEDIATE`. Under WAFER's per-line `evaluate` loader, the
|
||||||
|
// `SOURCE >IN ! DROP` path does not consume the remainder of the
|
||||||
|
// current line correctly, so 37 `\?`-guarded lines inside the
|
||||||
|
// TRAVERSE-WORDLIST / NAME>COMPILE / NAME>INTERPRET blocks leak as
|
||||||
|
// unknown-word errors. Fix the SOURCE/`>IN` interaction with
|
||||||
|
// line-mode input and drop this to 0.
|
||||||
|
//
|
||||||
|
// The 38th: line 368 `R> DROP TRUE` runs interpreted (its enclosing
|
||||||
|
// definition aborted on the missing NAME?), and the bare `R>` used
|
||||||
|
// to underflow the return stack silently; stack guards now report
|
||||||
|
// it as "Return stack underflow (throw -6)".
|
||||||
|
if path.ends_with("/toolstest.fth") {
|
||||||
|
return 38;
|
||||||
|
}
|
||||||
|
0
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Assert a file loaded with exactly its baseline number of line-level
|
||||||
|
/// failures. Used for prerequisites; keeps the runner tight without
|
||||||
|
/// blocking the whole suite on known gaps.
|
||||||
|
fn assert_load_fails_within_baseline(path: &str, fails: u32) {
|
||||||
|
let expected = expected_load_failures(path);
|
||||||
|
assert_eq!(
|
||||||
|
fails, expected,
|
||||||
|
"{path} had {fails} line-level failures (expected baseline: {expected})"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Boot a WAFER VM with full prerequisites loaded.
|
/// Boot a WAFER VM with full prerequisites loaded.
|
||||||
|
///
|
||||||
|
/// Every prerequisite file must load with zero line-level errors. Any
|
||||||
|
/// regression here points to a missing primitive or a parser bug and must
|
||||||
|
/// be fixed, not silently tolerated.
|
||||||
fn boot_with_prerequisites() -> ForthVM<NativeRuntime> {
|
fn boot_with_prerequisites() -> ForthVM<NativeRuntime> {
|
||||||
let mut vm = ForthVM::<NativeRuntime>::new().expect("Failed to create ForthVM");
|
let mut vm = ForthVM::<NativeRuntime>::new().expect("Failed to create ForthVM");
|
||||||
|
|
||||||
// Load test framework
|
// Load test framework
|
||||||
load_file(&mut vm, &format!("{SUITE_DIR}/tester.fr"));
|
let tester_path = format!("{SUITE_DIR}/tester.fr");
|
||||||
|
let f1 = load_file(&mut vm, &tester_path);
|
||||||
|
assert_load_fails_within_baseline(&tester_path, f1);
|
||||||
// Load core tests (prerequisite)
|
// Load core tests (prerequisite)
|
||||||
load_file(&mut vm, &format!("{SUITE_DIR}/core.fr"));
|
let core_path = format!("{SUITE_DIR}/core.fr");
|
||||||
|
let f2 = load_file(&mut vm, &core_path);
|
||||||
|
assert_load_fails_within_baseline(&core_path, f2);
|
||||||
// Switch to decimal and load utilities
|
// Switch to decimal and load utilities
|
||||||
let _ = vm.evaluate("DECIMAL");
|
let _ = vm.evaluate("DECIMAL");
|
||||||
vm.take_output();
|
vm.take_output();
|
||||||
load_file(&mut vm, &format!("{SUITE_DIR}/utilities.fth"));
|
let util_path = format!("{SUITE_DIR}/utilities.fth");
|
||||||
|
let f3 = load_file(&mut vm, &util_path);
|
||||||
|
assert_load_fails_within_baseline(&util_path, f3);
|
||||||
|
// errorreport.fth defines SET-ERROR-COUNT and the per-wordset counter
|
||||||
|
// accessors (CORE-ERRORS, STRING-ERRORS, LOCALS-ERRORS, ...). Every
|
||||||
|
// suite's final `X-ERRORS SET-ERROR-COUNT` line depends on this file,
|
||||||
|
// and silently errored before the runner was tightened.
|
||||||
|
let errorreport_path = format!("{SUITE_DIR}/errorreport.fth");
|
||||||
|
let f_err = load_file_whole(&mut vm, &errorreport_path);
|
||||||
|
assert_load_fails_within_baseline(&errorreport_path, f_err);
|
||||||
// Load core extensions
|
// Load core extensions
|
||||||
load_file(&mut vm, &format!("{SUITE_DIR}/coreexttest.fth"));
|
let ext_path = format!("{SUITE_DIR}/coreexttest.fth");
|
||||||
|
let f4 = load_file(&mut vm, &ext_path);
|
||||||
|
assert_load_fails_within_baseline(&ext_path, f4);
|
||||||
|
|
||||||
vm
|
vm
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Run a test suite file and return the #ERRORS count.
|
/// Run a test suite file and return the *total* error count:
|
||||||
|
/// `#ERRORS` from the Forth test framework plus any lines where
|
||||||
|
/// `vm.evaluate` itself failed (e.g. unknown word in a `:` definition
|
||||||
|
/// outside `T{ }T`, which the framework cannot catch).
|
||||||
fn run_suite(vm: &mut ForthVM<NativeRuntime>, test_file: &str) -> u32 {
|
fn run_suite(vm: &mut ForthVM<NativeRuntime>, test_file: &str) -> u32 {
|
||||||
// Reset error counter
|
// Reset error counter
|
||||||
let _ = vm.evaluate("DECIMAL 0 #ERRORS !");
|
let _ = vm.evaluate("DECIMAL 0 #ERRORS !");
|
||||||
vm.take_output();
|
vm.take_output();
|
||||||
|
|
||||||
// Load the test file
|
// Load the test file
|
||||||
load_file(vm, &format!("{SUITE_DIR}/{test_file}"));
|
let file_path = format!("{SUITE_DIR}/{test_file}");
|
||||||
|
let load_fails = load_file(vm, &file_path);
|
||||||
|
assert_load_fails_within_baseline(&file_path, load_fails);
|
||||||
|
|
||||||
// Read error count -- try multiple approaches to be robust
|
// Read error count -- try multiple approaches to be robust
|
||||||
let _ = vm.evaluate("DECIMAL");
|
let _ = vm.evaluate("DECIMAL");
|
||||||
@@ -76,8 +205,12 @@ fn run_suite(vm: &mut ForthVM<NativeRuntime>, test_file: &str) -> u32 {
|
|||||||
#[test]
|
#[test]
|
||||||
fn compliance_core() {
|
fn compliance_core() {
|
||||||
let mut vm = ForthVM::<NativeRuntime>::new().expect("Failed to create ForthVM");
|
let mut vm = ForthVM::<NativeRuntime>::new().expect("Failed to create ForthVM");
|
||||||
load_file(&mut vm, &format!("{SUITE_DIR}/tester.fr"));
|
let tester_path = format!("{SUITE_DIR}/tester.fr");
|
||||||
load_file(&mut vm, &format!("{SUITE_DIR}/core.fr"));
|
let f1 = load_file(&mut vm, &tester_path);
|
||||||
|
assert_load_fails_within_baseline(&tester_path, f1);
|
||||||
|
let core_path = format!("{SUITE_DIR}/core.fr");
|
||||||
|
let f2 = load_file(&mut vm, &core_path);
|
||||||
|
assert_load_fails_within_baseline(&core_path, f2);
|
||||||
|
|
||||||
let _ = vm.evaluate("DECIMAL #ERRORS @");
|
let _ = vm.evaluate("DECIMAL #ERRORS @");
|
||||||
let errors = vm.data_stack().first().copied().unwrap_or(-1);
|
let errors = vm.data_stack().first().copied().unwrap_or(-1);
|
||||||
@@ -96,17 +229,31 @@ fn compliance_core_ext() {
|
|||||||
// Core Extensions are loaded as part of prerequisites.
|
// Core Extensions are loaded as part of prerequisites.
|
||||||
// Run from scratch to get a clean error count.
|
// Run from scratch to get a clean error count.
|
||||||
let mut vm = ForthVM::<NativeRuntime>::new().expect("Failed to create ForthVM");
|
let mut vm = ForthVM::<NativeRuntime>::new().expect("Failed to create ForthVM");
|
||||||
load_file(&mut vm, &format!("{SUITE_DIR}/tester.fr"));
|
let tester_path = format!("{SUITE_DIR}/tester.fr");
|
||||||
load_file(&mut vm, &format!("{SUITE_DIR}/core.fr"));
|
let f1 = load_file(&mut vm, &tester_path);
|
||||||
|
assert_load_fails_within_baseline(&tester_path, f1);
|
||||||
|
let core_path = format!("{SUITE_DIR}/core.fr");
|
||||||
|
let f2 = load_file(&mut vm, &core_path);
|
||||||
|
assert_load_fails_within_baseline(&core_path, f2);
|
||||||
let _ = vm.evaluate("DECIMAL");
|
let _ = vm.evaluate("DECIMAL");
|
||||||
vm.take_output();
|
vm.take_output();
|
||||||
load_file(&mut vm, &format!("{SUITE_DIR}/utilities.fth"));
|
let util_path = format!("{SUITE_DIR}/utilities.fth");
|
||||||
|
let f3 = load_file(&mut vm, &util_path);
|
||||||
|
assert_load_fails_within_baseline(&util_path, f3);
|
||||||
|
let errorreport_path = format!("{SUITE_DIR}/errorreport.fth");
|
||||||
|
let f_err = load_file_whole(&mut vm, &errorreport_path);
|
||||||
|
assert_load_fails_within_baseline(&errorreport_path, f_err);
|
||||||
let _ = vm.evaluate("DECIMAL 0 #ERRORS !");
|
let _ = vm.evaluate("DECIMAL 0 #ERRORS !");
|
||||||
vm.take_output();
|
vm.take_output();
|
||||||
load_file(&mut vm, &format!("{SUITE_DIR}/coreexttest.fth"));
|
let ext_path = format!("{SUITE_DIR}/coreexttest.fth");
|
||||||
|
let load_fails = load_file(&mut vm, &ext_path);
|
||||||
|
assert_load_fails_within_baseline(&ext_path, load_fails);
|
||||||
let _ = vm.evaluate("DECIMAL #ERRORS @");
|
let _ = vm.evaluate("DECIMAL #ERRORS @");
|
||||||
let errors = vm.data_stack().first().copied().unwrap_or(-1) as u32;
|
let framework_errors = vm.data_stack().first().copied().unwrap_or(-1) as u32;
|
||||||
assert_eq!(errors, 0, "Core Extensions: {errors} test failures");
|
assert_eq!(
|
||||||
|
framework_errors, 0,
|
||||||
|
"Core Extensions: {framework_errors} framework test failures"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -164,17 +311,31 @@ fn compliance_string() {
|
|||||||
// Run from scratch -- the stringtest includes CoreExt tests that
|
// Run from scratch -- the stringtest includes CoreExt tests that
|
||||||
// cascade failures when run on top of an already-loaded CoreExt suite.
|
// cascade failures when run on top of an already-loaded CoreExt suite.
|
||||||
let mut vm = ForthVM::<NativeRuntime>::new().expect("Failed to create ForthVM");
|
let mut vm = ForthVM::<NativeRuntime>::new().expect("Failed to create ForthVM");
|
||||||
load_file(&mut vm, &format!("{SUITE_DIR}/tester.fr"));
|
let tester_path = format!("{SUITE_DIR}/tester.fr");
|
||||||
load_file(&mut vm, &format!("{SUITE_DIR}/core.fr"));
|
let f1 = load_file(&mut vm, &tester_path);
|
||||||
|
assert_load_fails_within_baseline(&tester_path, f1);
|
||||||
|
let core_path = format!("{SUITE_DIR}/core.fr");
|
||||||
|
let f2 = load_file(&mut vm, &core_path);
|
||||||
|
assert_load_fails_within_baseline(&core_path, f2);
|
||||||
let _ = vm.evaluate("DECIMAL");
|
let _ = vm.evaluate("DECIMAL");
|
||||||
vm.take_output();
|
vm.take_output();
|
||||||
load_file(&mut vm, &format!("{SUITE_DIR}/utilities.fth"));
|
let util_path = format!("{SUITE_DIR}/utilities.fth");
|
||||||
|
let f3 = load_file(&mut vm, &util_path);
|
||||||
|
assert_load_fails_within_baseline(&util_path, f3);
|
||||||
|
let errorreport_path = format!("{SUITE_DIR}/errorreport.fth");
|
||||||
|
let f_err = load_file_whole(&mut vm, &errorreport_path);
|
||||||
|
assert_load_fails_within_baseline(&errorreport_path, f_err);
|
||||||
let _ = vm.evaluate("DECIMAL 0 #ERRORS !");
|
let _ = vm.evaluate("DECIMAL 0 #ERRORS !");
|
||||||
vm.take_output();
|
vm.take_output();
|
||||||
load_file(&mut vm, &format!("{SUITE_DIR}/stringtest.fth"));
|
let str_path = format!("{SUITE_DIR}/stringtest.fth");
|
||||||
|
let load_fails = load_file(&mut vm, &str_path);
|
||||||
|
assert_load_fails_within_baseline(&str_path, load_fails);
|
||||||
let _ = vm.evaluate("DECIMAL #ERRORS @");
|
let _ = vm.evaluate("DECIMAL #ERRORS @");
|
||||||
let errors = vm.data_stack().first().copied().unwrap_or(-1) as u32;
|
let framework_errors = vm.data_stack().first().copied().unwrap_or(-1) as u32;
|
||||||
assert_eq!(errors, 0, "String: {errors} test failures");
|
assert_eq!(
|
||||||
|
framework_errors, 0,
|
||||||
|
"String: {framework_errors} framework test failures"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -183,3 +344,32 @@ fn compliance_tools() {
|
|||||||
let errors = run_suite(&mut vm, "toolstest.fth");
|
let errors = run_suite(&mut vm, "toolstest.fth");
|
||||||
assert_eq!(errors, 0, "Programming-Tools: {errors} test failures");
|
assert_eq!(errors, 0, "Programming-Tools: {errors} test failures");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The Forth 2012 Core suite against consolidated code.
|
||||||
|
///
|
||||||
|
/// `CONSOLIDATE` recompiles the whole dictionary into one WASM module, which
|
||||||
|
/// is where cross-word typed calls live: a word with a known stack effect
|
||||||
|
/// gets a fast entry taking and returning its stack items as WASM values,
|
||||||
|
/// and its `() -> ()` wrapper keeps the table slot. Nothing else covers that
|
||||||
|
/// path for correctness, so run the suite on top of it.
|
||||||
|
#[test]
|
||||||
|
fn compliance_core_after_consolidate() {
|
||||||
|
let mut vm = ForthVM::<NativeRuntime>::new().expect("Failed to create ForthVM");
|
||||||
|
let tester_path = format!("{SUITE_DIR}/tester.fr");
|
||||||
|
let f1 = load_file(&mut vm, &tester_path);
|
||||||
|
assert_load_fails_within_baseline(&tester_path, f1);
|
||||||
|
|
||||||
|
vm.evaluate("CONSOLIDATE").expect("CONSOLIDATE failed");
|
||||||
|
vm.take_output();
|
||||||
|
|
||||||
|
let core_path = format!("{SUITE_DIR}/core.fr");
|
||||||
|
let f2 = load_file(&mut vm, &core_path);
|
||||||
|
assert_load_fails_within_baseline(&core_path, f2);
|
||||||
|
|
||||||
|
let _ = vm.evaluate("DECIMAL #ERRORS @");
|
||||||
|
let errors = vm.data_stack().first().copied().unwrap_or(-1);
|
||||||
|
assert_eq!(
|
||||||
|
errors, 0,
|
||||||
|
"Core word set after CONSOLIDATE: {errors} failures"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
//! End-to-end tests for the `SHA1` / `SHA256` / `SHA512` Forth host words.
|
//! End-to-end tests for the `SHA1` / `SHA256` / `SHA512` Forth host words.
|
||||||
//!
|
//!
|
||||||
//! These run inside a real WAFER VM (NativeRuntime). The Forth program writes
|
//! These run inside a real WAFER VM (`NativeRuntime`). The Forth program writes
|
||||||
//! a counted string into `PAD`, calls the hash word, then the test reads the
|
//! a counted string into `PAD`, calls the hash word, then the test reads the
|
||||||
//! digest out of WAFER linear memory and compares it to the RFC-3174 / FIPS-180
|
//! digest out of WAFER linear memory and compares it to the RFC-3174 / FIPS-180
|
||||||
//! reference vectors.
|
//! reference vectors.
|
||||||
@@ -26,10 +26,16 @@ fn hash_via_forth(word: &str, input: &[u8]) -> Vec<u8> {
|
|||||||
|
|
||||||
// Stack now: ( c-addr2 u2 ). Read u2 then c-addr2 from data stack.
|
// Stack now: ( c-addr2 u2 ). Read u2 then c-addr2 from data stack.
|
||||||
let stack = vm.data_stack();
|
let stack = vm.data_stack();
|
||||||
assert!(stack.len() >= 2, "expected (addr len) on stack, got {stack:?}");
|
assert!(
|
||||||
|
stack.len() >= 2,
|
||||||
|
"expected (addr len) on stack, got {stack:?}"
|
||||||
|
);
|
||||||
let u2 = stack[0] as usize;
|
let u2 = stack[0] as usize;
|
||||||
let addr2 = stack[1] as u32;
|
let addr2 = stack[1] as u32;
|
||||||
assert_eq!(addr2, HASH_SCRATCH_BASE, "digest should land in HASH_SCRATCH");
|
assert_eq!(
|
||||||
|
addr2, HASH_SCRATCH_BASE,
|
||||||
|
"digest should land in HASH_SCRATCH"
|
||||||
|
);
|
||||||
|
|
||||||
// Read the digest out of WAFER linear memory.
|
// Read the digest out of WAFER linear memory.
|
||||||
let mut bytes = Vec::with_capacity(u2);
|
let mut bytes = Vec::with_capacity(u2);
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ workspace = true
|
|||||||
crate-type = ["cdylib", "rlib"]
|
crate-type = ["cdylib", "rlib"]
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
wafer-core = { path = "../core", version = "0.1.0", default-features = false, features = ["crypto"] }
|
wafer-core = { path = "../core", version = "0.2.7", 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 }
|
||||||
|
|||||||
@@ -6,8 +6,9 @@ use send_wrapper::SendWrapper;
|
|||||||
use wasm_bindgen::prelude::*;
|
use wasm_bindgen::prelude::*;
|
||||||
|
|
||||||
use wafer_core::config::WaferConfig;
|
use wafer_core::config::WaferConfig;
|
||||||
use wafer_core::memory::{CELL_SIZE, PAD_BASE, PAD_SIZE};
|
use wafer_core::memory::{CELL_SIZE, PAD_BASE, PAD_SIZE, SYSVAR_BASE_VAR};
|
||||||
use wafer_core::outer::ForthVM;
|
use wafer_core::outer::ForthVM;
|
||||||
|
use wafer_core::runtime::Runtime;
|
||||||
use wafer_core::runtime::{HostAccess, HostFn};
|
use wafer_core::runtime::{HostAccess, HostFn};
|
||||||
|
|
||||||
use crate::runtime_web::WebRuntime;
|
use crate::runtime_web::WebRuntime;
|
||||||
@@ -53,9 +54,12 @@ impl WaferRepl {
|
|||||||
|
|
||||||
/// Get the current number base (10 = decimal, 16 = hex).
|
/// Get the current number base (10 = decimal, 16 = hex).
|
||||||
pub fn base(&mut self) -> u32 {
|
pub fn base(&mut self) -> u32 {
|
||||||
// BASE is stored at SYSVAR_BASE_VAR in WASM memory
|
self.vm.runtime_mut().mem_read_i32(SYSVAR_BASE_VAR) as u32
|
||||||
self.vm.take_output(); // no-op side effect; just return base
|
}
|
||||||
10 // TODO: read from memory once we have a getter
|
|
||||||
|
/// Names of all user-facing words (visible, non-internal), newest first.
|
||||||
|
pub fn words(&self) -> Vec<String> {
|
||||||
|
self.vm.word_names()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Reset the VM to initial state.
|
/// Reset the VM to initial state.
|
||||||
|
|||||||
@@ -38,6 +38,23 @@ impl WebHostAccess {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// An exception on its way back out of compiled code. Host words rethrow the
|
||||||
|
/// Forth message (`Stack underflow`, an `ABORT"` text, a `THROW` description),
|
||||||
|
/// so surface exactly that and nothing else — the JS `Error` carries the whole
|
||||||
|
/// engine stack in its message, which is noise to a Forth programmer. Anything
|
||||||
|
/// without a message is a genuine runtime fault and keeps the call context.
|
||||||
|
fn call_error(fn_index: u32, e: &JsValue) -> anyhow::Error {
|
||||||
|
match Reflect::get(e, &"message".into())
|
||||||
|
.ok()
|
||||||
|
.and_then(|m| m.as_string())
|
||||||
|
.and_then(|m| m.lines().next().map(str::trim).map(str::to_string))
|
||||||
|
.filter(|m| !m.is_empty())
|
||||||
|
{
|
||||||
|
Some(msg) => anyhow::anyhow!("{msg}"),
|
||||||
|
None => anyhow::anyhow!("call_func({fn_index}) failed: {e:?}"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl HostAccess for WebHostAccess {
|
impl HostAccess for WebHostAccess {
|
||||||
fn mem_read_i32(&mut self, addr: u32) -> i32 {
|
fn mem_read_i32(&mut self, addr: u32) -> i32 {
|
||||||
let view = js_sys::Int32Array::new(&self.buffer());
|
let view = js_sys::Int32Array::new(&self.buffer());
|
||||||
@@ -134,7 +151,7 @@ impl HostAccess for WebHostAccess {
|
|||||||
.dyn_into()
|
.dyn_into()
|
||||||
.map_err(|_| anyhow::anyhow!("table entry {fn_index} is not a function"))?;
|
.map_err(|_| anyhow::anyhow!("table entry {fn_index} is not a function"))?;
|
||||||
func.call0(&JsValue::NULL)
|
func.call0(&JsValue::NULL)
|
||||||
.map_err(|e| anyhow::anyhow!("call_func({fn_index}) failed: {e:?}"))?;
|
.map_err(|e| call_error(fn_index, &e))?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -406,7 +423,7 @@ impl Runtime for WebRuntime {
|
|||||||
.dyn_into()
|
.dyn_into()
|
||||||
.map_err(|_| anyhow::anyhow!("table entry {fn_index} is not callable"))?;
|
.map_err(|_| anyhow::anyhow!("table entry {fn_index} is not callable"))?;
|
||||||
func.call0(&JsValue::NULL)
|
func.call0(&JsValue::NULL)
|
||||||
.map_err(|e| anyhow::anyhow!("call_func({fn_index}) failed: {e:?}"))?;
|
.map_err(|e| call_error(fn_index, &e))?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+41
-22
@@ -1,8 +1,11 @@
|
|||||||
import init, { WaferRepl } from './pkg/wafer_web.js';
|
import init, { WaferRepl } from './pkg/wafer_web.js';
|
||||||
|
|
||||||
let repl = null;
|
let repl = null;
|
||||||
const history = [];
|
const HISTORY_KEY = 'wafer-history';
|
||||||
let historyIdx = -1;
|
const HISTORY_MAX = 200;
|
||||||
|
const history = JSON.parse(localStorage.getItem(HISTORY_KEY) || '[]');
|
||||||
|
let historyIdx = history.length;
|
||||||
|
let builtinWords = null;
|
||||||
|
|
||||||
const WORD_CATEGORIES = {
|
const WORD_CATEGORIES = {
|
||||||
'Stack': 'DUP DROP SWAP OVER ROT NIP TUCK 2DUP 2DROP 2SWAP 2OVER PICK ROLL DEPTH .S'.split(' '),
|
'Stack': 'DUP DROP SWAP OVER ROT NIP TUCK 2DUP 2DROP 2SWAP 2OVER PICK ROLL DEPTH .S'.split(' '),
|
||||||
@@ -39,10 +42,12 @@ function updateStack() {
|
|||||||
if (!repl) return;
|
if (!repl) return;
|
||||||
try {
|
try {
|
||||||
const stack = repl.data_stack();
|
const stack = repl.data_stack();
|
||||||
|
const base = repl.base();
|
||||||
|
const suffix = base !== 10 ? ` [base ${base}]` : '';
|
||||||
if (stack.length === 0) {
|
if (stack.length === 0) {
|
||||||
stackBar.textContent = 'Stack: (empty)';
|
stackBar.textContent = `Stack: (empty)${suffix}`;
|
||||||
} else {
|
} else {
|
||||||
stackBar.textContent = `Stack <${stack.length}> ${stack.join(' ')}`;
|
stackBar.textContent = `Stack <${stack.length}> ${stack.join(' ')}${suffix}`;
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
stackBar.textContent = 'Stack: (error)';
|
stackBar.textContent = 'Stack: (error)';
|
||||||
@@ -50,19 +55,25 @@ function updateStack() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function updateUserWords() {
|
function updateUserWords() {
|
||||||
const cat = document.getElementById('cat-user');
|
const list = document.getElementById('user-word-list');
|
||||||
if (!cat) return;
|
if (!list || !repl || !builtinWords) return;
|
||||||
// We'll track user words by checking what the REPL evaluates
|
list.innerHTML = '';
|
||||||
// For now, just show the category
|
for (const w of repl.words()) {
|
||||||
|
if (!builtinWords.has(w)) list.appendChild(wordChip(w));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function evaluate(line) {
|
function evaluate(line, record = true) {
|
||||||
if (!repl) return;
|
if (!repl) return;
|
||||||
const trimmed = line.trim();
|
const trimmed = line.trim();
|
||||||
if (!trimmed) return;
|
if (!trimmed) return;
|
||||||
|
|
||||||
// Add to history
|
// Add to history (user-typed lines only; skip consecutive duplicates)
|
||||||
|
if (record && history[history.length - 1] !== trimmed) {
|
||||||
history.push(trimmed);
|
history.push(trimmed);
|
||||||
|
if (history.length > HISTORY_MAX) history.splice(0, history.length - HISTORY_MAX);
|
||||||
|
localStorage.setItem(HISTORY_KEY, JSON.stringify(history));
|
||||||
|
}
|
||||||
historyIdx = history.length;
|
historyIdx = history.length;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -83,6 +94,7 @@ function evaluate(line) {
|
|||||||
|
|
||||||
updatePrompt();
|
updatePrompt();
|
||||||
updateStack();
|
updateStack();
|
||||||
|
updateUserWords();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Input handling
|
// Input handling
|
||||||
@@ -116,6 +128,18 @@ document.getElementById('btn-toggle-words').addEventListener('click', () => {
|
|||||||
document.getElementById('word-panel').classList.toggle('collapsed');
|
document.getElementById('word-panel').classList.toggle('collapsed');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
function wordChip(w) {
|
||||||
|
const chip = document.createElement('span');
|
||||||
|
chip.className = 'word-chip';
|
||||||
|
chip.textContent = w;
|
||||||
|
chip.title = w;
|
||||||
|
chip.addEventListener('click', () => {
|
||||||
|
input.value += (input.value.length > 0 ? ' ' : '') + w;
|
||||||
|
input.focus();
|
||||||
|
});
|
||||||
|
return chip;
|
||||||
|
}
|
||||||
|
|
||||||
function buildWordPanel() {
|
function buildWordPanel() {
|
||||||
const container = document.getElementById('word-categories');
|
const container = document.getElementById('word-categories');
|
||||||
container.innerHTML = '';
|
container.innerHTML = '';
|
||||||
@@ -129,15 +153,7 @@ function buildWordPanel() {
|
|||||||
const list = document.createElement('div');
|
const list = document.createElement('div');
|
||||||
list.className = 'word-list';
|
list.className = 'word-list';
|
||||||
for (const w of words) {
|
for (const w of words) {
|
||||||
const chip = document.createElement('span');
|
list.appendChild(wordChip(w));
|
||||||
chip.className = 'word-chip';
|
|
||||||
chip.textContent = w;
|
|
||||||
chip.title = w;
|
|
||||||
chip.addEventListener('click', () => {
|
|
||||||
input.value += (input.value.length > 0 ? ' ' : '') + w;
|
|
||||||
input.focus();
|
|
||||||
});
|
|
||||||
list.appendChild(chip);
|
|
||||||
}
|
}
|
||||||
cat.appendChild(list);
|
cat.appendChild(list);
|
||||||
container.appendChild(cat);
|
container.appendChild(cat);
|
||||||
@@ -179,7 +195,7 @@ document.getElementById('btn-run-init').addEventListener('click', () => {
|
|||||||
if (code.trim()) {
|
if (code.trim()) {
|
||||||
// Run each line separately
|
// Run each line separately
|
||||||
for (const line of code.split('\n')) {
|
for (const line of code.split('\n')) {
|
||||||
if (line.trim()) evaluate(line);
|
if (line.trim()) evaluate(line, false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
localStorage.setItem('wafer-init-code', code);
|
localStorage.setItem('wafer-init-code', code);
|
||||||
@@ -214,6 +230,7 @@ document.getElementById('btn-reset').addEventListener('click', () => {
|
|||||||
appendLine('WAFER reset.', 'line-ok');
|
appendLine('WAFER reset.', 'line-ok');
|
||||||
updatePrompt();
|
updatePrompt();
|
||||||
updateStack();
|
updateStack();
|
||||||
|
updateUserWords();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
appendLine(`Reset error: ${e.message}`, 'line-error');
|
appendLine(`Reset error: ${e.message}`, 'line-error');
|
||||||
}
|
}
|
||||||
@@ -225,6 +242,8 @@ async function boot() {
|
|||||||
try {
|
try {
|
||||||
await init();
|
await init();
|
||||||
repl = new WaferRepl();
|
repl = new WaferRepl();
|
||||||
|
// Everything defined at boot is "builtin"; later definitions are user words
|
||||||
|
builtinWords = new Set(repl.words());
|
||||||
output.innerHTML = '';
|
output.innerHTML = '';
|
||||||
appendLine('WAFER — WebAssembly Forth Engine in Rust', 'line-output');
|
appendLine('WAFER — WebAssembly Forth Engine in Rust', 'line-output');
|
||||||
appendLine(`Type Forth at the > prompt. Press ? for help.`, 'line-output');
|
appendLine(`Type Forth at the > prompt. Press ? for help.`, 'line-output');
|
||||||
@@ -241,7 +260,7 @@ async function boot() {
|
|||||||
const initCode = document.getElementById('init-code').value;
|
const initCode = document.getElementById('init-code').value;
|
||||||
if (initCode.trim()) {
|
if (initCode.trim()) {
|
||||||
for (const line of initCode.split('\n')) {
|
for (const line of initCode.split('\n')) {
|
||||||
if (line.trim()) evaluate(line);
|
if (line.trim()) evaluate(line, false);
|
||||||
}
|
}
|
||||||
localStorage.setItem('wafer-init-code', initCode);
|
localStorage.setItem('wafer-init-code', initCode);
|
||||||
}
|
}
|
||||||
@@ -252,7 +271,7 @@ async function boot() {
|
|||||||
const code = atob(location.hash.slice(1));
|
const code = atob(location.hash.slice(1));
|
||||||
document.getElementById('init-code').value = code;
|
document.getElementById('init-code').value = code;
|
||||||
for (const line of code.split('\n')) {
|
for (const line of code.split('\n')) {
|
||||||
if (line.trim()) evaluate(line);
|
if (line.trim()) evaluate(line, false);
|
||||||
}
|
}
|
||||||
} catch { /* ignore bad hash */ }
|
} catch { /* ignore bad hash */ }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,11 +18,11 @@ confidence-threshold = 0.8
|
|||||||
[bans]
|
[bans]
|
||||||
multiple-versions = "deny"
|
multiple-versions = "deny"
|
||||||
wildcards = "deny"
|
wildcards = "deny"
|
||||||
# Transitive duplicates from wasmtime v31 -- will resolve when upgrading
|
# Transitive duplicates from wasmtime v47 dependencies
|
||||||
skip = [
|
skip = [
|
||||||
"getrandom",
|
"getrandom",
|
||||||
|
"syn",
|
||||||
"hashbrown",
|
"hashbrown",
|
||||||
"r-efi",
|
|
||||||
"thiserror",
|
"thiserror",
|
||||||
"thiserror-impl",
|
"thiserror-impl",
|
||||||
"wasm-encoder",
|
"wasm-encoder",
|
||||||
|
|||||||
+45
-13
@@ -14,7 +14,7 @@ This document describes every optimization that makes sense for WAFER, why it ma
|
|||||||
|
|
||||||
| # | Optimization | Level | Status | Impact |
|
| # | Optimization | Level | Status | Impact |
|
||||||
| -- | -------------------------- | ------------ | ----------- | ------- |
|
| -- | -------------------------- | ------------ | ----------- | ------- |
|
||||||
| 1 | Stack-to-Local Promotion | Codegen | Phase 2 | Highest |
|
| 1 | Stack-to-Local Promotion | Codegen | Phase 4 | Highest |
|
||||||
| 2 | Peephole Optimization | IR pass | Done | High |
|
| 2 | Peephole Optimization | IR pass | Done | High |
|
||||||
| 3 | Constant Folding | IR pass | Done | High |
|
| 3 | Constant Folding | IR pass | Done | High |
|
||||||
| 4 | Inlining | IR pass | Done | High |
|
| 4 | Inlining | IR pass | Done | High |
|
||||||
@@ -29,12 +29,18 @@ This document describes every optimization that makes sense for WAFER, why it ma
|
|||||||
| 13 | Startup Batching | Architecture | Done | Low |
|
| 13 | Startup Batching | Architecture | Done | Low |
|
||||||
| 14 | Self-Recursive Direct Call | Codegen | Done | High |
|
| 14 | Self-Recursive Direct Call | Codegen | Done | High |
|
||||||
| 15 | Float / Double-Cell | Codegen | Not started | Future |
|
| 15 | Float / Double-Cell | Codegen | Not started | Future |
|
||||||
|
| 16 | Typed Calling Convention | Codegen | Done | Highest |
|
||||||
|
|
||||||
## 1. Stack-to-Local Promotion
|
## 1. Stack-to-Local Promotion
|
||||||
|
|
||||||
**Status: Phase 2 done.** Words with straight-line code, DO/LOOP, and IF/ELSE use WASM locals instead of memory stack. Stack manipulation ops (Swap, Rot, Nip, Tuck, Dup, Drop) emit zero WASM instructions. Loop index/limit kept in WASM locals (zero return stack traffic). Switchable via `WaferConfig::codegen.stack_to_local_promotion`.
|
**Status: Phase 4 done.** Straight-line code, DO/LOOP, IF/ELSE and the BEGIN loop family use WASM locals instead of the memory stack, per region rather than per word. Stack manipulation ops (Swap, Rot, Nip, Tuck, Dup, Drop) emit zero WASM instructions. Loop index/limit stay in WASM locals (zero return stack traffic). Switchable via `WaferConfig::codegen.stack_to_local_promotion`.
|
||||||
|
|
||||||
Phase 1 covered straight-line code only. Phase 2 extends to DO/LOOP (with stack-neutrality check) and IF/ELSE/THEN (with equal-branch-effect check). BEGIN loops and BeginDoubleWhileRepeat are not yet promoted.
|
- **Phase 1** — straight-line code.
|
||||||
|
- **Phase 2** — DO/LOOP (stack-neutrality check) and IF/ELSE/THEN (equal-branch-effect check).
|
||||||
|
- **Phase 3** — _per region instead of per word_. Promotion used to be all-or-nothing: one `.`, `CR`, `>R` or host call anywhere in a definition put the entire body on the memory stack, hot loops included, which costs 2.2 ns per loop-carried add instead of 0.31 — the accumulator round-trips through store-to-load forwarding rather than staying in a register. `emit_body` now partitions a body into maximal promotable stretches and runs the simulator over each, loading what a region reads and writing back what it leaves. A region may only use `I` / `J` when the DO loops naming them are inside it, and a straight-line region needs at least three operations to pay for its prologue and epilogue; a loop always does.
|
||||||
|
- **Phase 4** — `BEGIN..UNTIL`, `BEGIN..AGAIN` and `BEGIN..WHILE..REPEAT`, when the construct is provably stack-neutral: UNTIL's body nets +1 (the flag it consumes), AGAIN's nets 0, and for WHILE..REPEAT the test and the body must balance _separately_, because WHILE leaves the loop between them and a net that only added up over the pair would give the two exits different stack shapes. Bodies containing an `EXIT` stay out, the same rule DO/LOOP follows.
|
||||||
|
|
||||||
|
Still not promoted: `BeginDoubleWhileRepeat`, `>R`/`R>`, floats, `{: :}` locals, `SP@`/`DEPTH`/`EXECUTE`, and the flat forward-block IR ops.
|
||||||
|
|
||||||
### The Problem
|
### The Problem
|
||||||
|
|
||||||
@@ -452,33 +458,59 @@ Fibonacci(25) with ~243K recursive calls:
|
|||||||
|
|
||||||
The optimization is implemented in `emit_op` for `IrOp::Call`: when `ctx.self_word_id == Some(word_id)`, emit `call WORD_FUNC` (function index 1 in the word's own module). The `self_word_id` is derived from `CodegenConfig::base_fn_index`.
|
The optimization is implemented in `emit_op` for `IrOp::Call`: when `ctx.self_word_id == Some(word_id)`, emit `call WORD_FUNC` (function index 1 in the word's own module). The `self_word_id` is derived from `CodegenConfig::base_fn_index`.
|
||||||
|
|
||||||
|
The numbers above are the state before section 16: they measure the call instruction, and what dominated turned out to be the calling _convention_ around it. A self-recursive word that is also typed now calls its own fast entry instead, and Fibonacci(25) is 356 microseconds rather than 1.6 ms.
|
||||||
|
|
||||||
## 15. Float and Double-Cell Stack
|
## 15. Float and Double-Cell Stack
|
||||||
|
|
||||||
**Status: Not started.** `PushI64` and `PushF64` exist as IR ops but are stubs in codegen. Float stack operations are currently all host functions.
|
**Status: Not started.** `PushI64` and `PushF64` exist as IR ops but are stubs in codegen. Float stack operations are currently all host functions.
|
||||||
|
|
||||||
The float stack lives in its own memory region (0x2540--0x2D40). Float operations will have the same memory-based overhead as integer operations, but worse: `f64` values are 8 bytes, doubling the memory traffic per push/pop. Stack-to-local promotion (section 1) is even more impactful for floats because WASM has native `f64` locals and operand stack support.
|
The float stack lives in its own memory region (0x2540--0x2D40). Float operations will have the same memory-based overhead as integer operations, but worse: `f64` values are 8 bytes, doubling the memory traffic per push/pop. Stack-to-local promotion (section 1) is even more impactful for floats because WASM has native `f64` locals and operand stack support.
|
||||||
|
|
||||||
|
## 16. Typed Calling Convention
|
||||||
|
|
||||||
|
**Status: Done.** A word whose stack effect is statically known compiles to two entry points: a fast one with signature `(i32 x p) -> (i32 x q)`, carrying its stack items as WASM values, and the usual `( -- )` wrapper that moves those items on and off the memory data stack. The wrapper keeps the function-table slot, so `EXECUTE`, the outer interpreter, host words and `CATCH` see exactly the ABI they saw before; only direct calls inside a module take the fast entry. `WAFER_TYPED_CALLS=0` falls back.
|
||||||
|
|
||||||
|
### The Problem
|
||||||
|
|
||||||
|
This is what the SwiftForth gap was made of. sf64 keeps TOS in `RBX` and the stack pointer in `RBP`, and both survive a `CALL` untouched, so its `FIB` is 16 instructions and about 7 memory touches per node. WAFER kept the whole stack in linear memory and flushed its cached `$dsp` to an imported global before every call: about 36 touches. Section 1's simulator, which already promoted loop and `IF` bodies into locals, refused any body containing a call or an `EXIT` -- exactly the words where the convention cost the most.
|
||||||
|
|
||||||
|
### The Effect Fixpoint
|
||||||
|
|
||||||
|
Self-recursion makes the stack-effect equation circular (`d = k + m*d`), so the effect is solved by iterating a guess until it reproduces itself: `FIB` settles on `(1,1)` in two rounds, while `: F 1 RECURSE ;` never settles and stays untyped. `CONSOLIDATE` extends this across words, since it puts them all in one module: the effects are solved from the leaves outward, and 105 of 187 words in a booted dictionary end up typed.
|
||||||
|
|
||||||
|
### Impact
|
||||||
|
|
||||||
|
Fibonacci(25) went from 1035 to 366 microseconds, 4.3x slower than `sf64` to 1.2x. Stack guards became nearly free as a side effect -- they hang off the memory-stack push/pop choke points, and a typed word barely has any -- so the default guards-on configuration that the REPL and the web build use went from 1631 to 365 microseconds on the same benchmark.
|
||||||
|
|
||||||
|
Untyped by design: anything using `SP@`, `DEPTH`, `EXECUTE`, `>R`/`R>`, floats or locals; anything calling a word that is itself untyped, which in the JIT path means every call except `RECURSE`; mutually recursive words; and words whose effect is not static -- branches that disagree on depth, `EXIT` at the wrong depth, a non-neutral loop body, or a recursion that grows the stack per level.
|
||||||
|
|
||||||
## Current Performance vs Gforth
|
## Current Performance vs Gforth
|
||||||
|
|
||||||
All optimizations enabled, release mode, measured with UTIME:
|
All optimizations enabled, release mode, measured with UTIME:
|
||||||
|
|
||||||
```
|
```
|
||||||
Benchmark WAFER CONSOL gforth WAFER/gf
|
Benchmark WAFER CONSOL gforth sf64 WAFER/gf WAFER/sf
|
||||||
Fibonacci(25) 1629 1535 3422 0.45x
|
Fibonacci(25) 356 361 3389 287 0.11x 1.24x
|
||||||
Factorial(12)x10K 340 339 638 0.53x
|
Factorial(12)x100K 479 495 6249 1650 0.08x 0.29x
|
||||||
GCD-bench(500) 18 15 30 0.50x
|
GCD-bench(20K) 540 559 1801 801 0.30x 0.67x
|
||||||
NestedLoops(50) 84 73 720 0.10x
|
NestedLoops(50)x1K 509 501 7023 1887 0.07x 0.27x
|
||||||
Collatz(2K) 1212 1202 3914 0.31x
|
Collatz(2K) 185 213 3873 610 0.05x 0.30x
|
||||||
```
|
```
|
||||||
|
|
||||||
Times in microseconds. WAFER/gf < 1.0 means WAFER is faster.
|
Times in microseconds. WAFER/gf < 1.0 means WAFER is faster. `sf64` is SwiftForth,
|
||||||
|
which compiles to native code; two caveats on that column. The install here is an
|
||||||
|
x86-64 binary under Rosetta 2 while WAFER and gforth are native arm64, so it is a
|
||||||
|
native-vs-emulated comparison and a native SwiftForth would be faster than these
|
||||||
|
numbers; and sf64 uses 64-bit cells to WAFER's 32-bit. WAFER is ahead on the four
|
||||||
|
loop-heavy benchmarks and behind on Fibonacci, which is one call per node with no
|
||||||
|
loop to promote.
|
||||||
|
|
||||||
## Remaining Opportunities
|
## Remaining Opportunities
|
||||||
|
|
||||||
| Optimization | Status | Potential Impact |
|
| Optimization | Status | Potential Impact |
|
||||||
| -------------------------------- | ------------------- | ----------------------------------------------------- |
|
| -------------------------------- | ------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||||
| BEGIN loop promotion | Not started | Would speed up GCD-style tight loops further |
|
| Bounded self-inlining | Not started | Measured 1.33x on Fibonacci, the last benchmark behind sf64. Blocked on `EXIT`: the inliner refuses any body containing one, and a recursive Forth word is `... IF EXIT THEN ... RECURSE`. Needs either a scoped exit (compile an inlined `EXIT` as a branch to the end of a block) or guard-only expansion |
|
||||||
| BeginDoubleWhileRepeat promotion | Not started | Rare pattern, low priority |
|
| BeginDoubleWhileRepeat promotion | Not started | Rare pattern, low priority. Its promoted emitter exists but has no loop fixup and is unverified |
|
||||||
| LEAVE as IR primitive | Not started | Would enable fast-path for loops with LEAVE |
|
| LEAVE as IR primitive | Not started | Would enable fast-path for loops with LEAVE |
|
||||||
| Float stack-to-local | Not started | Eliminate float stack memory traffic |
|
| Float stack-to-local | Not started | Eliminate float stack memory traffic |
|
||||||
| WASM tail calls proposal | Waiting on wasmtime | Would eliminate stack growth for tail-recursive words |
|
| WASM tail calls proposal | Waiting on wasmtime | Would eliminate stack growth for tail-recursive words |
|
||||||
|
|||||||
+4
-2
@@ -282,11 +282,13 @@ When the compiler encounters a word reference during compilation, it emits:
|
|||||||
(call_indirect (type $void) (table 0)) ;; indirect call through the table
|
(call_indirect (type $void) (table 0)) ;; indirect call through the table
|
||||||
```
|
```
|
||||||
|
|
||||||
**Self-recursive optimization**: When a word calls itself (RECURSE), the codegen detects this and emits a direct `call` instead of `call_indirect`, eliminating the table lookup and signature check (~3x faster for recursive words like Fibonacci).
|
**Self-recursive optimization**: When a word calls itself (RECURSE), the codegen detects this and emits a direct `call` instead of `call_indirect`, eliminating the table lookup and signature check (~3x faster for recursive words like Fibonacci). When the word is also typed, that direct call goes to its fast entry -- see below.
|
||||||
|
|
||||||
**After CONSOLIDATE**: All `call_indirect` between words in the consolidated module are replaced with direct `call` instructions, giving similar benefits for cross-word calls.
|
**After CONSOLIDATE**: All `call_indirect` between words in the consolidated module are replaced with direct `call` instructions, giving similar benefits for cross-word calls.
|
||||||
|
|
||||||
At runtime, wasmtime resolves the table entry and calls the target function. Because all functions share the same memory, globals, and table, state passes between words through the data stack in linear memory. There are no function parameters or return values at the WASM level -- everything goes through the stack.
|
At runtime, wasmtime resolves the table entry and calls the target function. Because all functions share the same memory, globals, and table, state passes between words through the data stack in linear memory.
|
||||||
|
|
||||||
|
**Typed entry points**: that last sentence is the default, not the whole story. A word whose stack effect is statically known also gets a _fast_ entry with signature `(i32 x p) -> (i32 x q)`, which takes its arguments as WASM values and returns its results the same way, so they stay in registers across the call instead of round-tripping through linear memory. The `( -- )` function above is then a wrapper around it, and it is the wrapper that keeps the table slot -- so `EXECUTE`, the outer interpreter, host words and `CATCH` see the memory ABI unchanged. Only a direct call inside the same module takes the fast entry: `RECURSE` in the JIT path, and every resolvable call after `CONSOLIDATE`. See [OPTIMIZATIONS.md](OPTIMIZATIONS.md) section 16.
|
||||||
|
|
||||||
This is subroutine threading: each word is a subroutine, and calling a word is an indirect function call.
|
This is subroutine threading: each word is a subroutine, and calling a word is an indirect function call.
|
||||||
|
|
||||||
|
|||||||
+329
-157
@@ -1,6 +1,11 @@
|
|||||||
WAFER Architecture Reference (updated 2026-04-13)
|
WAFER Architecture Reference (updated 2026-04-16)
|
||||||
===================================================
|
===================================================
|
||||||
|
|
||||||
|
WAFER = WebAssembly Forth Engine in Rust. Optimizing Forth-2012 compiler that
|
||||||
|
emits WASM at run time. Each colon definition becomes its own WASM module that
|
||||||
|
shares memory, globals, and a function table with every other word.
|
||||||
|
|
||||||
|
|
||||||
1. COMPILATION PIPELINE
|
1. COMPILATION PIPELINE
|
||||||
-----------------------
|
-----------------------
|
||||||
|
|
||||||
@@ -11,96 +16,134 @@ WAFER Architecture Reference (updated 2026-04-13)
|
|||||||
+--------------------------------------------+
|
+--------------------------------------------+
|
||||||
| Tokenizer: whitespace-delimited words |
|
| Tokenizer: whitespace-delimited words |
|
||||||
| For each token: |
|
| For each token: |
|
||||||
| 1. Dictionary lookup (find) |
|
| 1. Dictionary lookup (HashMap + wordlist |
|
||||||
| 2. If found + interpret mode: EXECUTE |
|
| search order) |
|
||||||
| 3. If found + compile mode: |
|
| 2. Found + interpret mode: EXECUTE |
|
||||||
| - Immediate? Execute now |
|
| 3. Found + compile mode: |
|
||||||
|
| - IMMEDIATE? Execute now |
|
||||||
| - Normal? Append Call(WordId) to IR |
|
| - Normal? Append Call(WordId) to IR |
|
||||||
| 4. Not found: try parse as number |
|
| 4. Not found: try parse as number |
|
||||||
| - Interpret: push to data stack |
|
| - Interpret: push to data stack |
|
||||||
| - Compile: append PushI32(n) to IR |
|
| - Compile: append PushI32/64/F64 |
|
||||||
| 5. Neither: error "unknown word" |
|
| 5. Neither: error "unknown word" |
|
||||||
|
| Special cases handled here, not via IR: |
|
||||||
|
| defining words (CREATE, VARIABLE, :), |
|
||||||
|
| DOES> dispatch, S" / ." string parsing, |
|
||||||
|
| {: ... :} locals, [: ... ;] quotations. |
|
||||||
+--------------------------------------------+
|
+--------------------------------------------+
|
||||||
| On `;` (end of colon definition):
|
| On `;` (end of colon definition):
|
||||||
v
|
v
|
||||||
Optimizer (optimizer.rs)
|
Optimizer (optimizer.rs) — IR -> IR
|
||||||
+--------------------------------------------+
|
+--------------------------------------------+
|
||||||
| Phase 1: Simplify |
|
| Phase 1 simplify: |
|
||||||
| Peephole -> Constant Fold -> |
|
| peephole -> fold -> strength -> peephole |
|
||||||
| Strength Reduce -> Peephole |
|
| Phase 2 inline (max 8 ops) then re-simpl.: |
|
||||||
| Phase 2: Inline then re-simplify |
|
| inline -> peephole -> fold -> strength |
|
||||||
| Inline(max=8) -> Peephole -> |
|
| -> peephole |
|
||||||
| Constant Fold -> Strength Reduce -> |
|
| Phase 3 dead code: dce -> peephole |
|
||||||
| Peephole |
|
| Phase 4 tail calls (must be last) |
|
||||||
| Phase 3: Eliminate dead code |
|
| Total peephole passes: 5 |
|
||||||
| DCE -> Peephole |
|
|
||||||
| Phase 4: Tail calls (must be last) |
|
|
||||||
| Tail Call Detect |
|
|
||||||
+--------------------------------------------+
|
+--------------------------------------------+
|
||||||
|
|
|
|
||||||
v
|
v
|
||||||
Codegen (codegen.rs)
|
Codegen (codegen.rs) — IR -> WASM bytes
|
||||||
+--------------------------------------------+
|
+--------------------------------------------+
|
||||||
| IR -> WASM bytecode via wasm-encoder |
|
| wasm-encoder builds one module per word. |
|
||||||
| Each word = one WASM module with: |
|
| Function locals (laid out in order): |
|
||||||
| Imports: emit, memory, dsp, rsp, fsp, |
|
| 0 cached DSP (i32) |
|
||||||
| table |
|
| 1..s scratch i32 (or promoted |
|
||||||
| Types: void () -> (), i32 (i32) -> () |
|
| stack-to-local slots) |
|
||||||
| One defined function (the word body) |
|
| s..f Forth locals from {: ... :} |
|
||||||
| DSP cached in local 0, writeback before |
|
| (i32 then f64) |
|
||||||
| calls, reload after calls |
|
| f..l loop locals: 2 per nested |
|
||||||
| Scratch locals start at index 1 |
|
| DO/?DO (index, limit) |
|
||||||
|
| DSP write-back before every Call, |
|
||||||
|
| reload after — keeps host functions and |
|
||||||
|
| call_indirect targets coherent. |
|
||||||
|
| Stack-to-local promotion (codegen flag): |
|
||||||
|
| straight-line + simple control flow |
|
||||||
|
| words skip the linear-memory data stack |
|
||||||
|
| entirely; values stay in WASM locals. |
|
||||||
+--------------------------------------------+
|
+--------------------------------------------+
|
||||||
|
|
|
|
||||||
v
|
v
|
||||||
Runtime trait (runtime.rs)
|
Runtime trait (runtime.rs) — execution backend
|
||||||
+--------------------------------------------+
|
+--------------------------------------------+
|
||||||
| ForthVM<R: Runtime> — generic over backend |
|
| ForthVM<R: Runtime> generic over backend. |
|
||||||
| Runtime provides: |
|
| Runtime owns: |
|
||||||
| - Memory r/w (mem_read_i32, etc.) |
|
| - shared linear memory (16 pages init) |
|
||||||
| - Globals (get/set_dsp, rsp, fsp) |
|
| - shared funcref table (grows on demand) |
|
||||||
| - Table (ensure_table_size) |
|
| - 3 mutable i32 globals (dsp/rsp/fsp) |
|
||||||
| - instantiate_and_install(wasm_bytes) |
|
| - emit() import bound to output buffer |
|
||||||
| - call_func(fn_index) |
|
| Runtime methods: |
|
||||||
| - register_host_func(fn_index, HostFn) |
|
| mem_read/write_{i32,u8,slice} |
|
||||||
|
| get/set_{dsp,rsp,fsp} |
|
||||||
|
| ensure_table_size(n) |
|
||||||
|
| instantiate_and_install(wasm, fn_index) |
|
||||||
|
| call_func(fn_index) |
|
||||||
|
| register_host_func(fn_index, HostFn) |
|
||||||
| |
|
| |
|
||||||
| HostAccess trait — memory/global ops for |
|
| HostAccess trait — same memory/global ops |
|
||||||
| host function callbacks |
|
| exposed to host-fn callbacks; lets one |
|
||||||
| HostFn = Box<dyn Fn(&mut dyn HostAccess)> |
|
| HostFn closure run on either runtime. |
|
||||||
|
| HostFn = Box<dyn Fn(&mut dyn HostAccess) |
|
||||||
|
| -> Result<()> + Send + Sync> |
|
||||||
+--------------------------------------------+
|
+--------------------------------------------+
|
||||||
| |
|
| |
|
||||||
v v
|
v v
|
||||||
NativeRuntime WebRuntime
|
NativeRuntime WebRuntime
|
||||||
(runtime_native.rs) (crates/web/runtime_web.rs)
|
(runtime_native.rs, (crates/web/src/
|
||||||
|
feature = "native") runtime_web.rs)
|
||||||
+------------------+ +------------------+
|
+------------------+ +------------------+
|
||||||
| wasmtime Engine | | js_sys::WebAsm |
|
| wasmtime Engine, | | js_sys WebAsm |
|
||||||
| Store, Memory | | Memory, Table |
|
| Store, Memory, | | Memory, Table, |
|
||||||
| Table, Globals | | Global objects |
|
| Table, Globals, | | Global, JS |
|
||||||
| Func closures | | JS Closures |
|
| Func closures | | Closures |
|
||||||
+------------------+ +------------------+
|
+------------------+ +------------------+
|
||||||
|
|
||||||
|
|
||||||
2. MEMORY LAYOUT (Linear Memory)
|
2. MEMORY LAYOUT (linear memory, single shared instance)
|
||||||
--------------------------------
|
--------------------------------------------------------
|
||||||
|
|
||||||
Address Region Size Notes
|
Address Region Size Notes
|
||||||
-------- ------------------ ------- -------------------------
|
-------- ------------------ ------- --------------------------
|
||||||
0x0000 System Variables 64 B STATE, BASE, >IN, HERE,
|
0x0000 System Variables 64 B STATE, BASE, >IN, HERE,
|
||||||
LATEST, SOURCE-ID, #TIB,
|
LATEST, SOURCE-ID, #TIB,
|
||||||
HLD, LEAVE-FLAG
|
HLD, LEAVE-FLAG
|
||||||
0x0040 Input Buffer 1024 B Source parsing
|
0x0040 Input Buffer (TIB) 1024 B Source line being parsed
|
||||||
0x0440 PAD 256 B Scratch area
|
0x0440 PAD 256 B Scratch for string ops
|
||||||
0x0540 Pictured Output 128 B <# ... #> (grows down)
|
0x0540 Pictured Output 128 B <# ... #> (HLD grows down)
|
||||||
0x05C0 WORD Buffer 64 B Transient counted string
|
0x05C0 WORD Buffer 64 B Transient counted string
|
||||||
0x0600 Data Stack 4096 B 1024 cells, grows DOWN
|
0x0600 Data Stack 4096 B 1024 cells, grows DOWN
|
||||||
0x1600 (Data Stack Top) DSP starts here
|
^ DSP starts at top = 0x1600
|
||||||
0x1540 Return Stack 4096 B Grows DOWN
|
0x1600 Return Stack 4096 B Grows DOWN
|
||||||
0x2540 Float Stack 2048 B 256 doubles, grows DOWN
|
^ RSP starts at top = 0x2600
|
||||||
0x2D40 Dictionary grows UP Linked list of word entries
|
0x2600 Float Stack 2048 B 256 doubles, grows DOWN
|
||||||
|
^ FSP starts at top = 0x2E00
|
||||||
|
0x2E00 Hash Scratch 128 B SHA1/256/512 output
|
||||||
|
0x2E80 Dictionary grows UP Linked list of entries
|
||||||
|
|
||||||
Total initial memory: 16 pages = 1 MiB (max 256 pages = 16 MiB)
|
Constants from crates/core/src/memory.rs (authoritative):
|
||||||
Cell size: 4 bytes (i32)
|
SYSVAR_BASE 0x0000 size 64
|
||||||
Float size: 8 bytes (f64)
|
INPUT_BUFFER_BASE 0x0040 size 1024
|
||||||
|
PAD_BASE 0x0440 size 256
|
||||||
|
PICT_BUF_BASE 0x0540 size 128
|
||||||
|
WORD_BUF_BASE 0x05C0 size 64
|
||||||
|
DATA_STACK_BASE 0x0600 size 4096 (DATA_STACK_TOP = 0x1600)
|
||||||
|
RETURN_STACK_BASE 0x1600 size 4096 (RETURN_STACK_TOP = 0x2600)
|
||||||
|
FLOAT_STACK_BASE 0x2600 size 2048 (FLOAT_STACK_TOP = 0x2E00)
|
||||||
|
HASH_SCRATCH_BASE 0x2E00 size 128
|
||||||
|
DICTIONARY_BASE 0x2E80 grows up to memory.len()
|
||||||
|
(Some inline `// 0x...` comments in memory.rs are stale — the
|
||||||
|
computed values above are correct; the consts are derived.)
|
||||||
|
|
||||||
|
Total initial memory: 16 pages = 1 MiB (max 256 pages = 16 MiB).
|
||||||
|
Cell size: 4 bytes (i32). Float size: 8 bytes (f64).
|
||||||
|
|
||||||
|
Stack layout note: linear-memory data and float stacks are the
|
||||||
|
fallback used whenever the optimizer can't keep values in WASM
|
||||||
|
locals. After stack-to-local promotion, many words touch DSP
|
||||||
|
only on entry/exit.
|
||||||
|
|
||||||
|
|
||||||
3. SYSTEM VARIABLES (offsets from 0x0000)
|
3. SYSTEM VARIABLES (offsets from 0x0000)
|
||||||
@@ -113,60 +156,86 @@ WAFER Architecture Reference (updated 2026-04-13)
|
|||||||
8 >IN Parse offset into input buffer
|
8 >IN Parse offset into input buffer
|
||||||
12 HERE Next free dictionary address
|
12 HERE Next free dictionary address
|
||||||
16 LATEST Most recent dictionary entry addr
|
16 LATEST Most recent dictionary entry addr
|
||||||
20 SOURCE-ID 0=user input, -1=string
|
20 SOURCE-ID 0=user input, -1=string, fileid>0
|
||||||
24 #TIB Length of current input
|
24 #TIB Length of current input
|
||||||
28 HLD Pictured numeric output pointer
|
28 HLD Pictured numeric output pointer
|
||||||
32 LEAVE-FLAG Nonzero when LEAVE called in loop
|
32 LEAVE-FLAG Nonzero when LEAVE called in loop
|
||||||
|
|
||||||
|
|
||||||
4. DICTIONARY ENTRY FORMAT
|
4. DICTIONARY (dictionary.rs)
|
||||||
--------------------------
|
-----------------------------
|
||||||
|
|
||||||
+--------+-------+----------+---------+-----------+
|
Entry layout in linear memory:
|
||||||
| Link | Flags | Name | Padding | Code |
|
|
||||||
| 4 bytes| 1 byte| N bytes | 0-3 B | 4 bytes |
|
+--------+-------+----------+---------+-----------+----------+
|
||||||
+--------+-------+----------+---------+-----------+
|
| Link | Flags | Name | Padding | Code | Param |
|
||||||
|
| 4 B | 1 B | N B | 0-3 B | 4 B | optional |
|
||||||
|
+--------+-------+----------+---------+-----------+----------+
|
||||||
^ ^
|
^ ^
|
||||||
entry_addr code field (fn table index)
|
entry_addr code field (fn-table idx)
|
||||||
|
|
||||||
Flags byte:
|
Flags byte:
|
||||||
Bit 7 (0x80): IMMEDIATE
|
Bit 7 (0x80): IMMEDIATE
|
||||||
Bit 6 (0x40): HIDDEN (during compilation)
|
Bit 6 (0x40): HIDDEN (during compilation)
|
||||||
Bits 0-4 (0x1F): name length (max 31)
|
Bits 0-4 : name length (max 31)
|
||||||
|
|
||||||
Link points to previous entry (0 = end of list).
|
Link points to previous entry (0 = end of list).
|
||||||
Name stored uppercase, padded to 4-byte alignment.
|
Name stored uppercase, padded to 4-byte alignment.
|
||||||
Code field: index into WASM function table.
|
Code field: index into shared WASM function table.
|
||||||
Parameter field (if any) follows immediately after code field.
|
Parameter field follows the code field for CREATE'd /
|
||||||
|
DOES> / VARIABLE / CONSTANT bodies.
|
||||||
|
|
||||||
|
Lookup is NOT linear: dictionary.rs maintains a HashMap
|
||||||
|
index from name -> Vec<(wid, addr, fn_index, immediate)>.
|
||||||
|
Each entry is tagged with its wordlist id; resolution
|
||||||
|
walks the current search order.
|
||||||
|
|
||||||
|
Wordlists / Search-Order:
|
||||||
|
wordlist ids are u32; the FORTH wordlist is id 1.
|
||||||
|
`current_wid` selects where new definitions land;
|
||||||
|
`search_order` is the lookup chain (top first).
|
||||||
|
Implements the Forth-2012 Search-Order word set.
|
||||||
|
|
||||||
|
|
||||||
5. THREE TYPES OF WORDS
|
5. WORD CATEGORIES
|
||||||
-----------------------
|
------------------
|
||||||
|
|
||||||
a) IR Primitives (compiled to WASM)
|
a) IR Primitives — register_primitive("DUP", false, vec![IrOp::Dup])
|
||||||
register_primitive("DUP", false, vec![IrOp::Dup])
|
|
||||||
- Body stored as Vec<IrOp>
|
- Body stored as Vec<IrOp>
|
||||||
- Optimized, then compiled to WASM module
|
- Optimized, then compiled to WASM
|
||||||
- Inlineable by optimizer
|
- Inlineable by optimizer
|
||||||
- FAST: no function call overhead when inlined
|
- Batched at boot: ~110 primitive registrations compiled
|
||||||
|
into a single WASM module to amortize instantiation cost
|
||||||
|
|
||||||
b) Host Functions (HostFn closures)
|
b) Host Functions — register_host_primitive(".", false, func)
|
||||||
register_host_primitive(".", false, func)
|
- HostFn = Box<dyn Fn(&mut dyn HostAccess)
|
||||||
- HostFn = Box<dyn Fn(&mut dyn HostAccess) -> Result<()>>
|
-> Result<()> + Send + Sync>
|
||||||
- Access memory/globals via HostAccess trait (runtime-agnostic)
|
- Access memory/globals via HostAccess trait
|
||||||
- NOT inlineable
|
- NOT inlineable
|
||||||
- Used for: I/O, dictionary manipulation, complex logic
|
- Used for I/O, dictionary manipulation, complex stack ops
|
||||||
- Same closure works on NativeRuntime and WebRuntime
|
- Same closure runs on NativeRuntime and WebRuntime
|
||||||
|
|
||||||
c) Forth-defined words
|
c) Forth-defined words — `: SQUARE DUP * ;`
|
||||||
: SQUARE DUP * ;
|
- Compiled by the outer interpreter
|
||||||
- Compiled by outer interpreter
|
- Goes through the full optimize -> codegen pipeline
|
||||||
- Goes through full optimize -> codegen pipeline
|
- Stored in `ir_bodies` for future inlining
|
||||||
- Stored in ir_bodies for future inlining
|
|
||||||
|
d) Special interpreter tokens (immediate, with custom parsing)
|
||||||
|
- Defining words: CREATE, VARIABLE, CONSTANT, :, ;, DOES>
|
||||||
|
- String literals: S", ."
|
||||||
|
- Control structures: IF/ELSE/THEN, BEGIN/UNTIL/WHILE/REPEAT,
|
||||||
|
DO/?DO/LOOP/+LOOP, [: ... ;] quotations, {: ... :} locals
|
||||||
|
- CONSOLIDATE
|
||||||
|
Their body-collection / dictionary-side-effect logic lives
|
||||||
|
directly in compile_token / interpret_token_immediate.
|
||||||
|
They still emit IR ops (e.g. IrOp::If, IrOp::DoLoop,
|
||||||
|
IrOp::ForthLocalGet) — the difference is that they are NOT
|
||||||
|
registered via register_primitive; the outer interpreter
|
||||||
|
handles them as special syntax.
|
||||||
|
|
||||||
|
|
||||||
6. WASM MODULE STRUCTURE (per word)
|
6. WASM MODULE STRUCTURE (per JIT-compiled word)
|
||||||
-----------------------------------
|
------------------------------------------------
|
||||||
|
|
||||||
Imports (6) — provided by Runtime impl:
|
Imports (6) — provided by Runtime impl:
|
||||||
0. emit (func: i32 -> void) Character output callback
|
0. emit (func: i32 -> void) Character output callback
|
||||||
@@ -176,25 +245,59 @@ WAFER Architecture Reference (updated 2026-04-13)
|
|||||||
4. fsp (global: mut i32) Float stack pointer
|
4. fsp (global: mut i32) Float stack pointer
|
||||||
5. table (table: funcref) Shared function table
|
5. table (table: funcref) Shared function table
|
||||||
|
|
||||||
Types (2):
|
Types: () -> () for word bodies; (i32) -> () for emit.
|
||||||
0. void: () -> ()
|
|
||||||
1. i32: (i32) -> ()
|
|
||||||
|
|
||||||
Functions (1):
|
Functions (1):
|
||||||
The compiled word body
|
The compiled word body, typed () -> ().
|
||||||
|
|
||||||
Element section:
|
Element section:
|
||||||
table[base_fn_index] = function 1
|
table[base_fn_index] = function 1
|
||||||
|
|
||||||
Runtime::instantiate_and_install(wasm_bytes, fn_index):
|
Runtime::instantiate_and_install(wasm_bytes, fn_index):
|
||||||
- NativeRuntime: Module::new + Instance::new with 6 wasmtime imports
|
- NativeRuntime: wasmtime Module::new + Instance::new
|
||||||
- WebRuntime: WebAssembly.instantiate with JS import objects
|
with the 6 imports above
|
||||||
|
- WebRuntime: WebAssembly.instantiate with JS import
|
||||||
|
objects pulled from the shared WaferRepl state
|
||||||
|
|
||||||
|
|
||||||
7. OPTIMIZATION PASSES (detail)
|
7. IR OPS (ir.rs — IrOp enum)
|
||||||
|
-----------------------------
|
||||||
|
|
||||||
|
Stack: Drop, Dup, Swap, Over, Rot, Nip, Tuck,
|
||||||
|
TwoDup, TwoDrop
|
||||||
|
Literals: PushI32, PushI64, PushF64
|
||||||
|
Arithmetic: Add, Sub, Mul, DivMod, Negate, Abs
|
||||||
|
Compare: Eq, NotEq, Lt, Gt, LtUnsigned,
|
||||||
|
ZeroEq, ZeroLt
|
||||||
|
Logic: And, Or, Xor, Invert,
|
||||||
|
Lshift, Rshift, ArithRshift
|
||||||
|
Memory: Fetch, Store, CFetch, CStore, PlusStore
|
||||||
|
Control: Call, TailCall, Exit,
|
||||||
|
If{then, else?},
|
||||||
|
DoLoop{body, is_plus_loop},
|
||||||
|
BeginUntil, BeginAgain,
|
||||||
|
BeginWhileRepeat,
|
||||||
|
BeginDoubleWhileRepeat,
|
||||||
|
LoopRestartIfFalse,
|
||||||
|
Block(label), BranchIfFalse(label),
|
||||||
|
EndBlock(label) -- for CS-ROLL'd patterns
|
||||||
|
Return stack: ToR, FromR, RFetch, LoopJ
|
||||||
|
Forth locals: ForthLocalGet/Set,
|
||||||
|
ForthFLocalGet/Set
|
||||||
|
I/O: Emit, Dot, Cr, Type
|
||||||
|
System: Execute, SpFetch
|
||||||
|
Float stack: FDup, FDrop, FSwap, FOver
|
||||||
|
Float math: FAdd, FSub, FMul, FDiv, FNegate, FAbs,
|
||||||
|
FSqrt, FMin, FMax, FFloor, FRound
|
||||||
|
Float compare:FZeroEq, FZeroLt, FEq, FLt
|
||||||
|
Float memory: FetchFloat, StoreFloat
|
||||||
|
Conversion: StoF, FtoS
|
||||||
|
|
||||||
|
|
||||||
|
8. OPTIMIZATION PASSES (detail)
|
||||||
-------------------------------
|
-------------------------------
|
||||||
|
|
||||||
PEEPHOLE (runs 5x across full pipeline):
|
PEEPHOLE (5x across pipeline):
|
||||||
PushI32(n), Drop -> (removed) Unused literal
|
PushI32(n), Drop -> (removed) Unused literal
|
||||||
Dup, Drop -> (removed) Redundant copy
|
Dup, Drop -> (removed) Redundant copy
|
||||||
Swap, Swap -> (removed) Self-inverse
|
Swap, Swap -> (removed) Self-inverse
|
||||||
@@ -205,16 +308,17 @@ WAFER Architecture Reference (updated 2026-04-13)
|
|||||||
PushI32(1), Mul -> (removed) Identity
|
PushI32(1), Mul -> (removed) Identity
|
||||||
Over, Over -> TwoDup Combine
|
Over, Over -> TwoDup Combine
|
||||||
Drop, Drop -> TwoDrop Combine
|
Drop, Drop -> TwoDrop Combine
|
||||||
(+ float variants: PushF64/FDrop, FDup/FDrop, FSwap/FSwap, FNegate/FNegate)
|
Float variants:
|
||||||
|
PushF64(_), FDrop / FDup, FDrop /
|
||||||
|
FSwap, FSwap / FNegate, FNegate
|
||||||
|
|
||||||
CONSTANT FOLD:
|
CONSTANT FOLD:
|
||||||
Binary: PushI32(a), PushI32(b), <op> -> PushI32(result)
|
Binary i32: PushI32(a), PushI32(b), <op> -> PushI32(r)
|
||||||
Supports: Add, Sub, Mul, And, Or, Xor, Lshift, Rshift, ArithRshift,
|
Add, Sub, Mul, And, Or, Xor,
|
||||||
|
Lshift, Rshift, ArithRshift,
|
||||||
Eq, NotEq, Lt, Gt, LtUnsigned
|
Eq, NotEq, Lt, Gt, LtUnsigned
|
||||||
Unary: PushI32(n), <op> -> PushI32(result)
|
Unary i32: Negate, Abs, Invert, ZeroEq, ZeroLt
|
||||||
Supports: Negate, Abs, Invert, ZeroEq, ZeroLt
|
Float binary/unary equivalents on PushF64.
|
||||||
Float binary: PushF64(a), PushF64(b), <op> -> PushF64(result)
|
|
||||||
Float unary: PushF64(n), <op> -> PushF64(result)
|
|
||||||
|
|
||||||
STRENGTH REDUCE:
|
STRENGTH REDUCE:
|
||||||
PushI32(2^n), Mul -> PushI32(n), Lshift
|
PushI32(2^n), Mul -> PushI32(n), Lshift
|
||||||
@@ -226,81 +330,149 @@ WAFER Architecture Reference (updated 2026-04-13)
|
|||||||
PushI32(0), If{then,else} -> else_body only
|
PushI32(0), If{then,else} -> else_body only
|
||||||
Everything after Exit -> removed
|
Everything after Exit -> removed
|
||||||
|
|
||||||
INLINE (max_size=8, single pass):
|
INLINE (max 8 ops, single pass):
|
||||||
Call(id) -> inline body if:
|
Call(id) -> body if all of:
|
||||||
- Body length <= 8 ops
|
- body length <= 8 ops
|
||||||
- No self-recursion
|
- no self-recursion
|
||||||
- No Exit (would return from caller)
|
- no Exit (would return from caller)
|
||||||
- No ForthLocalGet/Set (would collide with caller's locals)
|
- no ForthLocalGet/Set (would collide with caller locals)
|
||||||
TailCall -> Call when inlined (no longer tail position)
|
TailCall -> Call when inlined (no longer tail position)
|
||||||
|
|
||||||
TAIL CALL (last pass):
|
TAIL CALL (last pass, must be last):
|
||||||
Last Call(id) -> TailCall(id) if:
|
trailing Call(id) -> TailCall(id) if return stack balanced
|
||||||
- Return stack balanced (equal ToR and FromR)
|
(equal ToR / FromR pairs).
|
||||||
Recurses into If branches for conditional tail calls
|
Recurses into If branches for conditional tail calls.
|
||||||
|
|
||||||
|
STACK-TO-LOCAL PROMOTION (codegen pass, not optimizer):
|
||||||
|
Words whose effects on the data stack can be statically
|
||||||
|
tracked are compiled to use WASM locals 1..s instead of
|
||||||
|
DSP loads/stores. Triggered by `is_promotable(body)`.
|
||||||
|
DSP is still written back before any Call so callees and
|
||||||
|
host functions see a consistent stack.
|
||||||
|
|
||||||
|
|
||||||
8. CONSOLIDATION
|
9. CONSOLIDATION (consolidate.rs + codegen.rs)
|
||||||
----------------
|
----------------------------------------------
|
||||||
|
|
||||||
CONSOLIDATE word recompiles all JIT-compiled words into a
|
CONSOLIDATE recompiles every JIT-compiled word into ONE WASM
|
||||||
single WASM module:
|
module:
|
||||||
- All call_indirect -> direct call (for words in module)
|
- All call_indirect to consolidated words become direct
|
||||||
- External calls (host functions) remain call_indirect
|
`call` (single-module direct calls)
|
||||||
- Maximum performance for final program
|
- External calls (host functions) stay call_indirect
|
||||||
|
- Removes per-word instantiation overhead and lets the
|
||||||
|
WASM engine inline / specialize across word boundaries
|
||||||
|
|
||||||
Two-part implementation:
|
Two parts:
|
||||||
codegen::compile_consolidated_module() - builds multi-function module
|
codegen::compile_consolidated_module()
|
||||||
outer::ForthVM::consolidate() - orchestrates collection + table update
|
Builds the multi-function module.
|
||||||
|
outer::ForthVM::consolidate()
|
||||||
|
Collects ir_bodies, computes table layout, compiles,
|
||||||
|
instantiates, and patches the shared function table.
|
||||||
|
|
||||||
|
|
||||||
9. EXPORT PIPELINE (wafer build)
|
10. EXPORT PIPELINE (`wafer build`)
|
||||||
--------------------------------
|
----------------------------------
|
||||||
|
|
||||||
1. Evaluate source file with recording_toplevel=true
|
export.rs::export_module() steps:
|
||||||
2. Collect all IR words + top-level IR
|
1. Evaluate the source file with recording_toplevel = true
|
||||||
3. Determine entry: --entry flag > MAIN word > top-level execution
|
2. Collect every IR word + recorded top-level IR
|
||||||
4. Build consolidated module with data section (memory snapshot)
|
3. Resolve entry point (priority):
|
||||||
5. Embed metadata in "wafer" custom section (JSON)
|
--entry <name> > MAIN > synthetic _start from the
|
||||||
6. Optional: --js generates JS loader + HTML page
|
recorded top-level
|
||||||
7. Optional: --native AOT-compiles and appends to wafer binary
|
4. Snapshot WASM linear memory (system vars + dictionary +
|
||||||
Format: [wafer binary][precompiled WASM][metadata][trailer]
|
any user data)
|
||||||
Trailer: payload_len(8) + metadata_len(8) + "WAFEREXE"(8)
|
5. Walk the IR, find every Call/TailCall to a host word
|
||||||
|
not in the consolidated set: those become required
|
||||||
|
imports of the exported module
|
||||||
|
6. Build metadata (JSON, custom "wafer" section):
|
||||||
|
version, entry_table_index, host_functions,
|
||||||
|
memory_size, dsp/rsp/fsp_init
|
||||||
|
7. compile_exportable_module() emits the final WASM with
|
||||||
|
a passive data section seeded from the memory snapshot
|
||||||
|
8. Optional --js: also emit a JS loader + minimal HTML
|
||||||
|
9. Optional --native: AOT-compile and append to the wafer
|
||||||
|
binary itself, in this layout:
|
||||||
|
[wafer ELF/Mach-O][precompiled WASM][metadata]
|
||||||
|
[trailer: payload_len(8) | metadata_len(8) | "WAFEREXE"]
|
||||||
|
The CLI detects the trailer at startup and runs the
|
||||||
|
embedded payload directly (single-file distribution).
|
||||||
|
|
||||||
|
|
||||||
10. CRATE STRUCTURE
|
11. CRATE STRUCTURE
|
||||||
-------------------
|
-------------------
|
||||||
|
|
||||||
crates/
|
crates/
|
||||||
core/ wafer-core: compiler, optimizer, codegen, dictionary, Runtime trait
|
core/ wafer-core: compiler, optimizer, codegen,
|
||||||
Feature flags: default=["native"], "native" enables wasmtime
|
dictionary, runtime trait, outer interpreter.
|
||||||
Without features: pure Rust (dictionary, IR, optimizer, codegen, outer)
|
Largest file: codegen.rs (~4.3k LOC).
|
||||||
cli/ wafer: CLI REPL (rustyline), wafer build/run commands
|
Feature flags:
|
||||||
web/ wafer-web: browser REPL (wasm-bindgen + WebRuntime + HTML/CSS/JS)
|
default = ["native"]
|
||||||
|
"native" pulls in wasmtime + NativeRuntime +
|
||||||
|
runner.rs (CLI executor) + export.rs
|
||||||
|
"crypto" enables SHA1/256/512 host words
|
||||||
|
No features: pure-Rust core for wafer-web
|
||||||
|
(dictionary, IR, optimizer, codegen,
|
||||||
|
outer interpreter only)
|
||||||
|
cli/ wafer: rustyline REPL + `wafer build` / `wafer run`
|
||||||
|
web/ wafer-web: browser REPL.
|
||||||
|
|
||||||
Key web files:
|
Key web files:
|
||||||
crates/web/src/lib.rs WaferRepl wasm-bindgen entry point
|
crates/web/src/lib.rs WaferRepl wasm-bindgen entry
|
||||||
crates/web/src/runtime_web.rs WebRuntime: js_sys WebAssembly API
|
crates/web/src/runtime_web.rs WebRuntime: js_sys WebAssembly
|
||||||
crates/web/www/app.js Frontend JS (terminal emulation)
|
crates/web/www/app.js Frontend (terminal emulation)
|
||||||
crates/web/www/index.html HTML shell
|
crates/web/www/index.html HTML shell
|
||||||
crates/web/www/style.css Styling
|
crates/web/www/style.css Styling
|
||||||
|
crates/web/www/pkg/ wasm-pack output (gitignored)
|
||||||
|
|
||||||
|
|
||||||
11. BOOT SEQUENCE
|
12. BOOT SEQUENCE
|
||||||
-----------------
|
-----------------
|
||||||
|
|
||||||
ForthVM::<R>::new() ->
|
ForthVM::<R>::new() ->
|
||||||
1. R::new() — create runtime (wasmtime or browser WASM)
|
1. R::new() — create runtime (wasmtime or browser WASM)
|
||||||
2. register_primitives() in batch_mode:
|
2. register_primitives() in batch_mode = true:
|
||||||
- ~40 IR primitives (DUP, +, @, etc.)
|
- ~110 IR primitive registrations (DUP, +, @, ...)
|
||||||
- ~60 host functions (., .S, M*, ACCEPT, etc.)
|
- ~87 host primitive registrations (., .S, M*, ACCEPT, ...)
|
||||||
- ~30 special words (IF, DO, :, VARIABLE, etc.)
|
- special interpreter tokens (IF, DO, :, VARIABLE, S",
|
||||||
3. compile_batch() - single WASM module for all IR primitives
|
{: :}, [: ;], CONSOLIDATE, ...) handled directly in
|
||||||
4. Load boot.fth - Forth replaces Rust host functions:
|
interpret_token_immediate / compile_token, no IR op
|
||||||
Phase 1: Stack/memory (DEPTH, PICK, 2OVER, FILL, MOVE)
|
3. Word-set registrations:
|
||||||
Phase 2: Double-cell arithmetic (D+, DNEGATE, D<)
|
core, double, exception, facility, file (subset),
|
||||||
Phase 3: Mixed arithmetic (SM/REM, FM/MOD, */, */MOD)
|
floating-point, locals, memory, search-order,
|
||||||
Phase 4: HERE, ALLOT, comma, ALIGN
|
programming-tools, string, optional crypto
|
||||||
Phase 5: I/O, pictured numeric output (., U., TYPE, <# # #>)
|
4. batch_compile_deferred() — single WASM module for all
|
||||||
Phase 6: DEFER support
|
deferred IR primitives
|
||||||
Phase 7: String operations (COMPARE, SOURCE, FALIGNED)
|
5. Load boot.fth (include_str!), evaluated line by line so
|
||||||
|
`\` comments terminate at end-of-line:
|
||||||
|
Phase 1: stack/memory (DEPTH, PICK, 2OVER, FILL, MOVE,
|
||||||
|
CMOVE, /STRING, -TRAILING)
|
||||||
|
Phase 2: double-cell arithmetic (D+, DNEGATE, D<, D=)
|
||||||
|
Phase 3: mixed arithmetic (SM/REM, FM/MOD, */, */MOD)
|
||||||
|
Phase 4: HERE, ALLOT, comma, ALIGN, ALIGNED
|
||||||
|
Phase 5: I/O + pictured output (., U., TYPE, <# # #>,
|
||||||
|
SIGN, HOLD)
|
||||||
|
Phase 6: DEFER support (DEFER, IS, ACTION-OF)
|
||||||
|
Phase 7: more replacements (COMPARE, SOURCE, FALIGNED,
|
||||||
|
DFALIGN, structures, S" hint, ...)
|
||||||
|
|
||||||
|
|
||||||
|
13. RUNTIME-VS-EXPORT NOTE
|
||||||
|
--------------------------
|
||||||
|
|
||||||
|
Two separate codegen entry points produce multi-function
|
||||||
|
WASM modules from the same IR:
|
||||||
|
|
||||||
|
compile_consolidated_module() used by CONSOLIDATE
|
||||||
|
- Targets the live runtime
|
||||||
|
- Re-uses the shared globals/table/memory imports
|
||||||
|
- External calls remain call_indirect
|
||||||
|
|
||||||
|
compile_exportable_module() used by `wafer build`
|
||||||
|
- Targets a standalone module
|
||||||
|
- Carries its own memory (passive data section seeded
|
||||||
|
from the snapshot) and embeds metadata
|
||||||
|
- Required host functions become imports the runner
|
||||||
|
(or AOT loader) must satisfy
|
||||||
|
|
||||||
|
Both share the same per-IrOp lowering helpers; the
|
||||||
|
difference is in module-level wiring.
|
||||||
|
|||||||
@@ -0,0 +1,47 @@
|
|||||||
|
# Editor support for WAFER
|
||||||
|
|
||||||
|
Syntax highlighting assets for editors and pagers.
|
||||||
|
|
||||||
|
## bat (and other Sublime-Text-compatible tools)
|
||||||
|
|
||||||
|
`bat/WAFER.sublime-syntax` is a Sublime Text grammar covering Forth 2012 plus
|
||||||
|
WAFER-specific words (`CONSOLIDATE`, `RANDOM`, `RND-SEED`, `UTIME`).
|
||||||
|
|
||||||
|
### Install
|
||||||
|
|
||||||
|
```
|
||||||
|
just install-syntax
|
||||||
|
```
|
||||||
|
|
||||||
|
or manually:
|
||||||
|
|
||||||
|
```
|
||||||
|
mkdir -p ~/.config/bat/syntaxes
|
||||||
|
cp tools/editor-support/bat/WAFER.sublime-syntax ~/.config/bat/syntaxes/
|
||||||
|
bat cache --build
|
||||||
|
```
|
||||||
|
|
||||||
|
### Verify
|
||||||
|
|
||||||
|
```
|
||||||
|
bat --list-languages | grep -i forth # should list Forth
|
||||||
|
bat --language forth crates/core/boot.fth # should render with colour
|
||||||
|
```
|
||||||
|
|
||||||
|
### Use with `oked`
|
||||||
|
|
||||||
|
`oked` auto-detects `.fth` / `.4th` / `.forth` files and invokes `bat` with
|
||||||
|
`--language forth`. After the install step above, opening any WAFER source in
|
||||||
|
`oked` and toggling highlight (`H` command, or `oked -S forth`) will use this
|
||||||
|
syntax.
|
||||||
|
|
||||||
|
### Updating the keyword list
|
||||||
|
|
||||||
|
Primitives live in `crates/core/src/outer.rs` (`register_primitive` and
|
||||||
|
`register_host_primitive` calls). When a new **user-facing, non-standard** word
|
||||||
|
is added, append it to the `wafer_extras` context in
|
||||||
|
`bat/WAFER.sublime-syntax`. Standard Forth 2012 words are already covered by
|
||||||
|
the main contexts.
|
||||||
|
|
||||||
|
Internal symbols (names that start with `_`) should not be added — they are
|
||||||
|
implementation details that user code never types.
|
||||||
@@ -0,0 +1,211 @@
|
|||||||
|
%YAML 1.2
|
||||||
|
---
|
||||||
|
# WAFER / Forth 2012 syntax for `bat` (and any Sublime Text compatible highlighter).
|
||||||
|
#
|
||||||
|
# Keyword list is derived from the primitives registered in
|
||||||
|
# crates/core/src/outer.rs plus the Forth 2012 core-ext wordset and the boot.fth
|
||||||
|
# definitions in crates/core/boot.fth. WAFER-specific additions are tagged below.
|
||||||
|
#
|
||||||
|
# Install: see tools/editor-support/README.md.
|
||||||
|
name: Forth
|
||||||
|
file_extensions:
|
||||||
|
- fth
|
||||||
|
- 4th
|
||||||
|
- forth
|
||||||
|
scope: source.forth
|
||||||
|
|
||||||
|
variables:
|
||||||
|
ident_break: '(?=\s|$)'
|
||||||
|
|
||||||
|
contexts:
|
||||||
|
main:
|
||||||
|
- include: comments
|
||||||
|
- include: strings
|
||||||
|
- include: numbers
|
||||||
|
- include: definitions
|
||||||
|
- include: locals
|
||||||
|
- include: structures
|
||||||
|
- include: control
|
||||||
|
- include: stack_ops
|
||||||
|
- include: return_stack
|
||||||
|
- include: arithmetic
|
||||||
|
- include: logic
|
||||||
|
- include: compare
|
||||||
|
- include: memory
|
||||||
|
- include: io
|
||||||
|
- include: pictured
|
||||||
|
- include: string_ops
|
||||||
|
- include: float
|
||||||
|
- include: tools
|
||||||
|
- include: dictionary
|
||||||
|
- include: exception
|
||||||
|
- include: parsing
|
||||||
|
- include: literals
|
||||||
|
- include: hashing
|
||||||
|
- include: wafer_extras
|
||||||
|
|
||||||
|
comments:
|
||||||
|
# Line comment: backslash to end of line, must be followed by whitespace or EOL.
|
||||||
|
- match: '(?i)(?:^|(?<=\s))\\(?=\s|$).*$'
|
||||||
|
scope: comment.line.backslash.forth
|
||||||
|
# Stack-effect / block comment: ( ... ) — the `(` must be followed by whitespace.
|
||||||
|
- match: '(?i)(?:^|(?<=\s))\((?=\s|$)'
|
||||||
|
scope: punctuation.definition.comment.forth
|
||||||
|
push:
|
||||||
|
- meta_scope: comment.block.paren.forth
|
||||||
|
- match: '\)'
|
||||||
|
scope: punctuation.definition.comment.forth
|
||||||
|
pop: true
|
||||||
|
# Immediate print comment: .( ... )
|
||||||
|
- match: '(?i)(?:^|(?<=\s))\.\((?=\s|$)'
|
||||||
|
scope: punctuation.definition.comment.forth
|
||||||
|
push:
|
||||||
|
- meta_scope: comment.block.dot-paren.forth
|
||||||
|
- match: '\)'
|
||||||
|
scope: punctuation.definition.comment.forth
|
||||||
|
pop: true
|
||||||
|
|
||||||
|
strings:
|
||||||
|
# Standard Forth strings: leading word followed by space then body, closed with ".
|
||||||
|
- match: '(?i)(?:^|(?<=\s))(S\\"|S"|C"|\."|ABORT")(\s)'
|
||||||
|
captures:
|
||||||
|
1: keyword.other.string-prefix.forth
|
||||||
|
push:
|
||||||
|
- meta_scope: string.quoted.double.forth
|
||||||
|
- match: '"'
|
||||||
|
pop: true
|
||||||
|
|
||||||
|
numbers:
|
||||||
|
# Hex / binary / decimal / char literals / negatives; all whitespace-delimited.
|
||||||
|
- match: '(?i)(?:^|(?<=\s))\$[0-9A-F]+{{ident_break}}'
|
||||||
|
scope: constant.numeric.hex.forth
|
||||||
|
- match: '(?i)(?:^|(?<=\s))#-?[0-9]+{{ident_break}}'
|
||||||
|
scope: constant.numeric.decimal.forth
|
||||||
|
- match: '(?i)(?:^|(?<=\s))%[01]+{{ident_break}}'
|
||||||
|
scope: constant.numeric.binary.forth
|
||||||
|
- match: "(?i)(?:^|(?<=\\s))'.'{{ident_break}}"
|
||||||
|
scope: constant.character.forth
|
||||||
|
- match: '(?i)(?:^|(?<=\s))-?[0-9]+(?:\.[0-9]*)?(?:[eE]-?[0-9]+)?{{ident_break}}'
|
||||||
|
scope: constant.numeric.forth
|
||||||
|
|
||||||
|
definitions:
|
||||||
|
- match: '(?i)(?:^|(?<=\s))(:|:NONAME)(\s+)(\S+)?'
|
||||||
|
captures:
|
||||||
|
1: keyword.other.definition.forth
|
||||||
|
3: entity.name.function.forth
|
||||||
|
- match: '(?i)(?:^|(?<=\s));{{ident_break}}'
|
||||||
|
scope: keyword.other.definition.forth
|
||||||
|
# Quotations (Core-Ext 6.2.0455): [: ... ;] compiles an anonymous word.
|
||||||
|
- match: '(?i)(?:^|(?<=\s))(\[:|;\]){{ident_break}}'
|
||||||
|
scope: keyword.other.definition.forth
|
||||||
|
- match: '(?i)(?:^|(?<=\s))(VARIABLE|2VARIABLE|CONSTANT|2CONSTANT|VALUE|CREATE|DEFER|MARKER|REMEMBER|BUFFER:|FCONSTANT|FVARIABLE)(\s+)(\S+)?'
|
||||||
|
captures:
|
||||||
|
1: keyword.other.defining.forth
|
||||||
|
3: entity.name.constant.forth
|
||||||
|
- match: '(?i)(?:^|(?<=\s))(DOES>|IMMEDIATE|RECURSE|POSTPONE|COMPILE,|LITERAL|2LITERAL|FLITERAL|SLITERAL|DEFER!|DEFER@){{ident_break}}'
|
||||||
|
scope: keyword.other.defining.forth
|
||||||
|
|
||||||
|
control:
|
||||||
|
- match: '(?i)(?:^|(?<=\s))(IF|THEN|ELSE|BEGIN|UNTIL|WHILE|REPEAT|AGAIN|DO|\?DO|LOOP|\+LOOP|LEAVE|UNLOOP|EXIT|CASE|OF|ENDOF|ENDCASE|QUIT){{ident_break}}'
|
||||||
|
scope: keyword.control.forth
|
||||||
|
# Conditional compilation (Tools-ext 15.6.2).
|
||||||
|
- match: '(?i)(?:^|(?<=\s))(\[IF\]|\[ELSE\]|\[THEN\]|\[DEFINED\]|\[UNDEFINED\]){{ident_break}}'
|
||||||
|
scope: keyword.control.conditional-compilation.forth
|
||||||
|
|
||||||
|
stack_ops:
|
||||||
|
- match: '(?i)(?:^|(?<=\s))(DUP|\?DUP|DROP|SWAP|OVER|ROT|-ROT|NIP|TUCK|PICK|ROLL|2DUP|2DROP|2SWAP|2OVER|2ROT|DEPTH|SP@){{ident_break}}'
|
||||||
|
scope: support.function.stack.forth
|
||||||
|
|
||||||
|
return_stack:
|
||||||
|
# RP@ / RDEPTH are WAFER extensions (gforth-style return-stack access).
|
||||||
|
- match: '(?i)(?:^|(?<=\s))(>R|R>|R@|2>R|2R>|2R@|N>R|NR>|I|J|CS-PICK|CS-ROLL|RP@|RDEPTH){{ident_break}}'
|
||||||
|
scope: support.function.return-stack.forth
|
||||||
|
|
||||||
|
arithmetic:
|
||||||
|
- match: '(?i)(?:^|(?<=\s))(\+|-|\*|/|MOD|/MOD|\*/|\*/MOD|NEGATE|ABS|MIN|MAX|1\+|1-|2\*|2/|M\*|M\+|M\*/|UM\*|UM/MOD|FM/MOD|SM/REM|S>D|D>S|D\+|D-|DNEGATE|DABS|DMAX|DMIN|D2\*|D2/){{ident_break}}'
|
||||||
|
scope: keyword.operator.arithmetic.forth
|
||||||
|
|
||||||
|
logic:
|
||||||
|
- match: '(?i)(?:^|(?<=\s))(AND|OR|XOR|INVERT|LSHIFT|RSHIFT){{ident_break}}'
|
||||||
|
scope: keyword.operator.logical.forth
|
||||||
|
|
||||||
|
compare:
|
||||||
|
- match: '(?i)(?:^|(?<=\s))(=|<>|<|>|<=|>=|U<|U>|0=|0<>|0<|0>|D<|D=|D0<|D0=|DU<|WITHIN){{ident_break}}'
|
||||||
|
scope: keyword.operator.comparison.forth
|
||||||
|
|
||||||
|
memory:
|
||||||
|
- match: '(?i)(?:^|(?<=\s))(@|!|C@|C!|\+!|2@|2!|C,|ALLOT|HERE|ALIGN|ALIGNED|CELL\+|CELLS|CHAR\+|CHARS|UNUSED|MOVE|CMOVE|CMOVE>|FILL|ERASE|BLANK|ALLOCATE|FREE|RESIZE|PAD){{ident_break}}'
|
||||||
|
scope: support.function.memory.forth
|
||||||
|
|
||||||
|
io:
|
||||||
|
- match: '(?i)(?:^|(?<=\s))(EMIT|CR|SPACE|SPACES|TYPE|\.|U\.|\.R|U\.R|D\.|D\.R|\?|KEY|KEY\?|PAGE|AT-XY|ACCEPT|EXPECT|\.S|F\.S|\.RS){{ident_break}}'
|
||||||
|
scope: support.function.io.forth
|
||||||
|
|
||||||
|
# Pictured numeric output (6.1: <# # #S #> HOLD SIGN; HOLDS is Core-Ext).
|
||||||
|
pictured:
|
||||||
|
- match: '(?i)(?:^|(?<=\s))(<#|#>|#S|#|HOLD|HOLDS|SIGN){{ident_break}}'
|
||||||
|
scope: support.function.pictured.forth
|
||||||
|
|
||||||
|
# String word set (17.6).
|
||||||
|
string_ops:
|
||||||
|
- match: '(?i)(?:^|(?<=\s))(COUNT|COMPARE|-TRAILING|/STRING){{ident_break}}'
|
||||||
|
scope: support.function.string.forth
|
||||||
|
|
||||||
|
float:
|
||||||
|
- match: '(?i)(?:^|(?<=\s))(F\+|F-|F\*\*|F\*|F/|FNEGATE|FABS|FMAX|FMIN|FSQRT|FFLOOR|FROUND|FLOOR|FSINCOS|FSINH|FSIN|FCOSH|FCOS|FTANH|FTAN|FASINH|FASIN|FACOSH|FACOS|FATANH|FATAN2|FATAN|FEXPM1|FEXP|FLNP1|FLN|FLOG|FALOG|F=|F<|F0=|F0<|F~|FDUP|FDROP|FSWAP|FOVER|FROT|FNIP|FTUCK|FDEPTH|F@|F!|FE\.|FS\.|F\.|F>D|D>F|F>S|S>F|>FLOAT|REPRESENT|PRECISION|SET-PRECISION|FALIGN|FALIGNED|DFALIGN|DFALIGNED|SFALIGN|SFALIGNED|FLOAT\+|FLOATS|DFLOAT\+|DFLOATS|SFLOAT\+|SFLOATS|DF@|DF!|SF@|SF!){{ident_break}}'
|
||||||
|
scope: support.function.float.forth
|
||||||
|
|
||||||
|
# Interactive/debug tools (Tools word set + WAFER REPL additions).
|
||||||
|
tools:
|
||||||
|
- match: '(?i)(?:^|(?<=\s))(SEE-IR|SEE|DUMP|BYE|HELP){{ident_break}}'
|
||||||
|
scope: support.function.tools.forth
|
||||||
|
|
||||||
|
dictionary:
|
||||||
|
- match: "(?i)(?:^|(?<=\\s))('|\\[']|,|>BODY|FIND|WORDS|ONLY|ALSO|PREVIOUS|DEFINITIONS|FORTH|GET-ORDER|SET-ORDER|GET-CURRENT|SET-CURRENT|WORDLIST|SEARCH-WORDLIST|FORTH-WORDLIST|ENVIRONMENT\\?|EXECUTE){{ident_break}}"
|
||||||
|
scope: support.function.dictionary.forth
|
||||||
|
|
||||||
|
exception:
|
||||||
|
- match: '(?i)(?:^|(?<=\s))(CATCH|THROW|ABORT){{ident_break}}'
|
||||||
|
scope: keyword.control.exception.forth
|
||||||
|
|
||||||
|
parsing:
|
||||||
|
- match: '(?i)(?:^|(?<=\s))(PARSE|PARSE-NAME|WORD|REFILL|EVALUATE|INCLUDE|INCLUDED|SOURCE|SOURCE-ID|>IN|BASE|DECIMAL|HEX|STATE|>NUMBER|SEARCH|SUBSTITUTE|UNESCAPE|REPLACES|S){{ident_break}}'
|
||||||
|
scope: support.function.parsing.forth
|
||||||
|
|
||||||
|
literals:
|
||||||
|
- match: '(?i)(?:^|(?<=\s))(TRUE|FALSE|BL|CHAR|\[CHAR\]|\[COMPILE\]){{ident_break}}'
|
||||||
|
scope: constant.language.forth
|
||||||
|
|
||||||
|
# Forth 2012 §13 Locals. `{: ... :}` is the user-facing form; `{F:` is the
|
||||||
|
# float-locals variant (gforth/SwiftForth-style). `(LOCAL)` is the low-level
|
||||||
|
# primitive from §13.6.1.0086; user code typically builds `LOCAL` /
|
||||||
|
# `END-LOCALS` on top of it. `TO` rebinds a VALUE or local; `LOCALS|` is the
|
||||||
|
# §13 legacy (Forth-94) form.
|
||||||
|
locals:
|
||||||
|
- match: '(?i)(?:^|(?<=\s))(\{:|:\}|\{F:|LOCALS\|){{ident_break}}'
|
||||||
|
scope: keyword.other.locals.forth
|
||||||
|
- match: '(?i)(?:^|(?<=\s))(TO|END-LOCALS){{ident_break}}'
|
||||||
|
scope: keyword.other.locals.forth
|
||||||
|
- match: '(?i)(?:^|(?<=\s))\(LOCAL\){{ident_break}}'
|
||||||
|
scope: support.function.locals.forth
|
||||||
|
|
||||||
|
# Structure words — Facility-ext 10.6.2.0935 (defined in boot.fth).
|
||||||
|
structures:
|
||||||
|
- match: '(?i)(?:^|(?<=\s))(BEGIN-STRUCTURE)(\s+)(\S+)?'
|
||||||
|
captures:
|
||||||
|
1: keyword.other.struct.forth
|
||||||
|
3: entity.name.struct.forth
|
||||||
|
- match: '(?i)(?:^|(?<=\s))(END-STRUCTURE|\+FIELD|FIELD:|CFIELD:|FFIELD:|SFFIELD:|DFFIELD:){{ident_break}}'
|
||||||
|
scope: keyword.other.struct.forth
|
||||||
|
|
||||||
|
# Hash primitives — mirrors the registry in crates/core/src/crypto.rs. When
|
||||||
|
# new algorithms are added to `crypto::ALGOS`, extend this alternation.
|
||||||
|
hashing:
|
||||||
|
- match: '(?i)(?:^|(?<=\s))(SHA1|SHA256|SHA512){{ident_break}}'
|
||||||
|
scope: support.function.hash.forth
|
||||||
|
|
||||||
|
wafer_extras:
|
||||||
|
# WAFER-specific extensions beyond the Forth 2012 standard.
|
||||||
|
# When the language grows new user-facing non-standard words, add them here.
|
||||||
|
- match: '(?i)(?:^|(?<=\s))(CONSOLIDATE|RANDOM|RND-SEED|UTIME|READ-PASSWORD|EMPTY|GILD){{ident_break}}'
|
||||||
|
scope: support.function.wafer-extra.forth
|
||||||
Reference in New Issue
Block a user