diff --git a/.gitignore b/.gitignore index 5f85b5d..7eace5a 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,6 @@ *.swp .DS_Store *.bk + +# Local planning notes — never tracked +/plans/ diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..0e08d01 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,90 @@ +# 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.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.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 diff --git a/CLAUDE.md b/CLAUDE.md index a9f96df..e938fb9 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -79,7 +79,7 @@ Handle in `interpret_token_immediate()` or `compile_token()` as a special case. ## Testing -- Run `cargo test --workspace` before committing (currently 431 unit + 1 benchmark + 11 compliance + 9 comparison) +- Run `cargo test --workspace` before committing (currently 542 unit + 1 benchmark + 11 compliance + 9 comparison + 5 crypto) - Forth 2012 compliance: `cargo test -p wafer-core --test compliance` - 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` diff --git a/Cargo.lock b/Cargo.lock index 96ddf27..8ed5922 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -69,9 +69,9 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.102" +version = "1.0.104" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" +checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470" [[package]] name = "arbitrary" @@ -81,20 +81,20 @@ checksum = "c3d036a3c4ab069c7b410a2ce876bd74808d2d0888a82667669f8e783a898bf1" [[package]] name = "async-trait" -version = "0.1.89" +version = "0.1.91" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" +checksum = "ae36dc4177970ef04fde5178d3e2429882def40e57a451f919c098f72baa6cec" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 3.0.3", ] [[package]] name = "autocfg" -version = "1.5.0" +version = "1.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" +checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" [[package]] name = "base64" @@ -119,18 +119,9 @@ checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7" [[package]] name = "bitflags" -version = "2.11.0" +version = "2.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af" - -[[package]] -name = "bitmaps" -version = "2.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "031043d04099746d8db04daf1fa424b2bc8bd69d92b25962dcde24da39ab64a2" -dependencies = [ - "typenum", -] +checksum = "b588b76d00fde79687d7646a9b5bdf3cc0f655e0bbd080335a95d7e96f3587da" [[package]] name = "block-buffer" @@ -143,18 +134,18 @@ dependencies = [ [[package]] name = "block-buffer" -version = "0.12.0" +version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cdd35008169921d80bc60d3d0ab416eecb028c4cd653352907921d95084790be" +checksum = "d2f6c7dbe95a6ed67ad9f18e57daf93a2f034c524b99fd2b76d18fdfeb6660aa" dependencies = [ "hybrid-array", ] [[package]] name = "bumpalo" -version = "3.20.2" +version = "3.20.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb" +checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" dependencies = [ "allocator-api2", ] @@ -167,9 +158,9 @@ checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5" [[package]] name = "cc" -version = "1.2.60" +version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43c5703da9466b66a946814e1adf53ea2c90f10063b86290cc9eb67ce3478a20" +checksum = "5add81bb678e6cb321aff7fa0dc7689ad82b112dbc032cea19f91d6b8e3582b9" dependencies = [ "find-msvc-tools", "jobserver", @@ -185,15 +176,15 @@ checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" [[package]] name = "cfg_aliases" -version = "0.2.1" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" +checksum = "f079e83a288787bcd14a6aea84cee5c87a67c5a3e660c30f557a3d24761b3527" [[package]] name = "clap" -version = "4.6.0" +version = "4.6.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b193af5b67834b676abd72466a96c1024e6a6ad978a1f484bd90b85c94041351" +checksum = "301b56658598e48f3648647ac6fc887be7e7108eddfa4e9b63fcf3ec58c0cadf" dependencies = [ "clap_builder", "clap_derive", @@ -201,9 +192,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.6.0" +version = "4.6.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "714a53001bf66416adb0e2ef5ac857140e7dc3a0c48fb28b2f10762fc4b5069f" +checksum = "94a65403d1a1bd28f7dc68eb8506e8874808ee5eecb59298de588e2e1407a078" dependencies = [ "anstream", "anstyle", @@ -213,14 +204,14 @@ dependencies = [ [[package]] name = "clap_derive" -version = "4.6.0" +version = "4.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1110bd8a634a1ab8cb04345d8d878267d57c3cf1b38d91b71af6686408bbca6a" +checksum = "d012d2b9d65aca7f18f4d9878a045bc17899bba951561ba5ec3c2ba1eed9a061" dependencies = [ "heck", "proc-macro2", "quote", - "syn", + "syn 3.0.3", ] [[package]] @@ -244,7 +235,7 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0fa961b519f0b462e3a3b4a34b64d119eeaca1d59af726fe450bbba07a9fc0a1" dependencies = [ - "thiserror 2.0.18", + "thiserror 2.0.19", ] [[package]] @@ -255,9 +246,9 @@ checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570" [[package]] name = "console" -version = "0.16.3" +version = "0.16.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d64e8af5551369d19cf50138de61f1c42074ab970f74e99be916646777f8fc87" +checksum = "4fe5f465a4f6fee88fad41b85d990f84c835335e85b5d9e6e63e0d06d28cba7c" dependencies = [ "encode_unicode", "libc", @@ -272,9 +263,9 @@ checksum = "a6ef517f0926dd24a1582492c791b6a4818a4d94e789a334894aa15b0d12f55c" [[package]] name = "cpp_demangle" -version = "0.4.5" +version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2bb79cb74d735044c972aae58ed0aaa9a837e85b01106a54c39e42e97f62253" +checksum = "0667304c32ea56cb4cd6d2d7c0cfe9a2f8041229db8c033af7f8d69492429def" dependencies = [ "cfg-if", ] @@ -299,27 +290,27 @@ dependencies = [ [[package]] name = "cranelift-assembler-x64" -version = "0.130.1" +version = "0.134.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "046d4b584c3bb9b5eb500c8f29549bec36be11000f1ba2a927cef3d1a9875691" +checksum = "d552bd33b7a56dc70aeb1e1c960e51a218fa0db50f23873b500a310379450b2d" dependencies = [ "cranelift-assembler-x64-meta", ] [[package]] name = "cranelift-assembler-x64-meta" -version = "0.130.1" +version = "0.134.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9b194a7870becb1490366fc0ae392ccd188065ff35f8391e77ac659db6fb977" +checksum = "078e80e4c222279e3330f6aa1a256ca77ddf156d4453166a9f09defedc4594dd" dependencies = [ "cranelift-srcgen", ] [[package]] name = "cranelift-bforest" -version = "0.130.1" +version = "0.134.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb6a4ab44c6b371e661846b97dab687387a60ac4e2f864e2d4257284aad9e889" +checksum = "820ce15d4ad3562d613c31a67b6d0434d403e7091a68d1349903842f7d31737e" dependencies = [ "cranelift-entity", "wasmtime-internal-core", @@ -327,9 +318,9 @@ dependencies = [ [[package]] name = "cranelift-bitset" -version = "0.130.1" +version = "0.134.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8b7a44150c2f471a94023482bda1902710746e4bed9f9973d60c5a94319b06d" +checksum = "61bca563d4b86d285928d9e27f97f27039bb33a0fc524fa130d7d0c106bf8ab3" dependencies = [ "serde", "serde_derive", @@ -338,9 +329,9 @@ dependencies = [ [[package]] name = "cranelift-codegen" -version = "0.130.1" +version = "0.134.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "01b06598133b1dd76758b8b95f8d6747c124124aade50cea96a3d88b962da9fa" +checksum = "709f4b7c0fb57d952658d5b5c07fbdc4149acd7b7f0de9678ae754b9c981949a" dependencies = [ "bumpalo", "cranelift-assembler-x64", @@ -352,13 +343,16 @@ dependencies = [ "cranelift-entity", "cranelift-isle", "gimli", - "hashbrown 0.16.1", + "hashbrown 0.17.1", "libm", "log", + "postcard", "pulley-interpreter", "regalloc2", "rustc-hash", "serde", + "serde_derive", + "sha2 0.10.9", "smallvec", "target-lexicon", "wasmtime-internal-core", @@ -366,9 +360,9 @@ dependencies = [ [[package]] name = "cranelift-codegen-meta" -version = "0.130.1" +version = "0.134.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6190e2e7bcf0a678da2f715363d34ed530fedf7a2f0ab75edaefef72a70465ff" +checksum = "903dc8915af62aad1d9d0f39a5968d33fa80b9aa899ed6f56e47f40ca4512e1e" dependencies = [ "cranelift-assembler-x64-meta", "cranelift-codegen-shared", @@ -379,24 +373,24 @@ dependencies = [ [[package]] name = "cranelift-codegen-shared" -version = "0.130.1" +version = "0.134.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f583cf203d1aa8b79560e3b01f929bdacf9070b015eec4ea9c46e22a3f83e4a0" +checksum = "0522d74c227e49f3fd49ab055311486ae4f09083262b66705bed676952491469" [[package]] name = "cranelift-control" -version = "0.130.1" +version = "0.134.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "803159df35cc398ae54473c150b16d6c77e92ab2948be638488de126a3328fbc" +checksum = "66560ea1c5cef72e170b18e46d263dba2d3169c9d39e8cafdab2173c6362cc1a" dependencies = [ "arbitrary", ] [[package]] name = "cranelift-entity" -version = "0.130.1" +version = "0.134.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3109e417257082d88087f5bcce677525bdaa8322b88dd7f175ed1a1fd41d546c" +checksum = "b62ef5b17cc814d27e96b66a5b46da0e4ce2b8ac55a6d478048bb99d04b05526" dependencies = [ "cranelift-bitset", "serde", @@ -406,11 +400,12 @@ dependencies = [ [[package]] name = "cranelift-frontend" -version = "0.130.1" +version = "0.134.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "14db6b0e0e4994c581092df78d837be2072578f7cb2528f96a6cf895e56dee63" +checksum = "a87e0aaa39dbf70693b348a221e45904111704ee8f9fef140498471005f9842d" dependencies = [ "cranelift-codegen", + "hashbrown 0.17.1", "log", "smallvec", "target-lexicon", @@ -418,15 +413,15 @@ dependencies = [ [[package]] name = "cranelift-isle" -version = "0.130.1" +version = "0.134.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec66ea5025c7317383699778282ac98741d68444f956e3b1d7b62f12b7216e67" +checksum = "cf79003ebfa1eed5e87f3b84446ad5236f540268289960e14f387dc9b28e40b7" [[package]] name = "cranelift-native" -version = "0.130.1" +version = "0.134.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "373ade56438e6232619d85678477d0a88a31b3581936e0503e61e96b546b0800" +checksum = "05bf4f235743c81e67ee4db617c5a4a0b65d58d3f0cfc575ee0f1a4e0cd58273" dependencies = [ "cranelift-codegen", "libc", @@ -435,9 +430,9 @@ dependencies = [ [[package]] name = "cranelift-srcgen" -version = "0.130.1" +version = "0.134.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef53619d3cd5c78fd998c6d9420547af26b72e6456f94c2a8a2334cb76b42baa" +checksum = "f6977c2a71ab1e0d1e62f966b411a498aa04c4dce47d93d52f8a360a06058922" [[package]] name = "crc32fast" @@ -450,9 +445,9 @@ dependencies = [ [[package]] name = "crossbeam-deque" -version = "0.8.6" +version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51" +checksum = "5181e0de7b61eb03a81e347d6dd8797bae9da5146707b51077e2d71a54ec0ceb" dependencies = [ "crossbeam-epoch", "crossbeam-utils", @@ -460,18 +455,18 @@ dependencies = [ [[package]] name = "crossbeam-epoch" -version = "0.9.18" +version = "0.9.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" +checksum = "2d6914041f254d6e9176c01941b21115dcfb7089e55135a35411081bd106ef3f" dependencies = [ "crossbeam-utils", ] [[package]] name = "crossbeam-utils" -version = "0.8.21" +version = "0.8.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" +checksum = "61803da095bee82a81bb1a452ecc25d3b2f1416d1897eb86430c6159ef717c17" [[package]] name = "crypto-common" @@ -485,9 +480,9 @@ dependencies = [ [[package]] name = "crypto-common" -version = "0.2.1" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77727bb15fa921304124b128af125e7e3b968275d1b108b379190264f4423710" +checksum = "ce6e4c961d6cd6c9a86db418387425e8bdeaf05b3c8bc1411e6dca4c252f1453" dependencies = [ "hybrid-array", ] @@ -513,13 +508,13 @@ dependencies = [ [[package]] name = "digest" -version = "0.11.2" +version = "0.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4850db49bf08e663084f7fb5c87d202ef91a3907271aff24a94eb97ff039153c" +checksum = "f1dd6dbb5841937940781866fa1281a1ff7bd3bf827091440879f9994983d5c2" dependencies = [ - "block-buffer 0.12.0", + "block-buffer 0.12.1", "const-oid", - "crypto-common 0.2.1", + "crypto-common 0.2.2", ] [[package]] @@ -545,9 +540,9 @@ dependencies = [ [[package]] name = "either" -version = "1.15.0" +version = "1.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" +checksum = "9e5e8f6c15a24b9a3ee5efec809ccd006d3b30e8b3bb63c39af737c7f87daa1d" [[package]] name = "embedded-io" @@ -606,9 +601,9 @@ checksum = "dea2df4cf52843e0452895c455a1a2cfbb842a1e7329671acf418fdc53ed4c59" [[package]] name = "fastrand" -version = "2.4.1" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f1f227452a390804cdb637b74a86990f2a7d7ba4b7d5693aac9b4dd6defd8d6" +checksum = "da7c62ceae207dd37ea5b845da6a0696c799f85e97da1ab5b7910be3c1c80223" [[package]] name = "find-msvc-tools" @@ -628,12 +623,6 @@ version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" -[[package]] -name = "foldhash" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" - [[package]] name = "foldhash" version = "0.2.0" @@ -642,9 +631,9 @@ checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" [[package]] name = "futures" -version = "0.3.32" +version = "0.3.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b147ee9d1f6d097cef9ce628cd2ee62288d963e16fb287bd9286455b241382d" +checksum = "a88cf1f829d945f548cf8fec32c61b1f202b6d93b45848602fc02af4b12ad218" dependencies = [ "futures-channel", "futures-core", @@ -656,9 +645,9 @@ dependencies = [ [[package]] name = "futures-channel" -version = "0.3.32" +version = "0.3.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d" +checksum = "262590f4fe6afeb0bc83be1daa64e52657fe185690a958af7f3ad0e92085c5ae" dependencies = [ "futures-core", "futures-sink", @@ -666,33 +655,33 @@ dependencies = [ [[package]] name = "futures-core" -version = "0.3.32" +version = "0.3.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" +checksum = "2cd50c473c80f6d7c3670a752354b8e569b1a7cbfdc0419ec88e5edad85e0dc7" [[package]] name = "futures-io" -version = "0.3.32" +version = "0.3.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718" +checksum = "4577ecaa3c4f96589d473f679a71b596316f6641bc350038b962a5daf0085d7a" [[package]] name = "futures-sink" -version = "0.3.32" +version = "0.3.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893" +checksum = "e34418ac499d6305c2fb5ad0ed2f6ac998c5f8ca209b4510f7f94242c647e307" [[package]] name = "futures-task" -version = "0.3.32" +version = "0.3.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" +checksum = "b231ed28831efb4a61a08580c4bc233ec56bc009f4cd8f52da2c3cb97df0c109" [[package]] name = "futures-util" -version = "0.3.32" +version = "0.3.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" +checksum = "a77a90a256fce34da66415271e30f94ee91c57b04b8a2c042d9cf3220179deaa" dependencies = [ "futures-channel", "futures-core", @@ -753,22 +742,20 @@ dependencies = [ [[package]] name = "getrandom" -version = "0.4.2" +version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555" +checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099" dependencies = [ "cfg-if", "libc", "r-efi 6.0.0", - "wasip2", - "wasip3", ] [[package]] name = "gimli" -version = "0.33.1" +version = "0.33.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19e16c5073773ccf057c282be832a59ee53ef5ff98db3aeff7f8314f52ffc196" +checksum = "0bf7f043f89559805f8c7cacc432749b2fa0d0a0a9ee46ce47164ed5ba7f126c" dependencies = [ "fnv", "hashbrown 0.16.1", @@ -776,31 +763,22 @@ dependencies = [ "stable_deref_trait", ] -[[package]] -name = "hashbrown" -version = "0.15.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" -dependencies = [ - "foldhash 0.1.5", -] - [[package]] name = "hashbrown" version = "0.16.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" -dependencies = [ - "foldhash 0.2.0", - "serde", - "serde_core", -] [[package]] name = "hashbrown" -version = "0.17.0" +version = "0.17.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4f467dd6dccf739c208452f8014c75c18bb8301b050ad1cfb27153803edb0f51" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" +dependencies = [ + "foldhash", + "serde", + "serde_core", +] [[package]] name = "heck" @@ -819,9 +797,9 @@ dependencies = [ [[package]] name = "hybrid-array" -version = "0.4.10" +version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3944cf8cf766b40e2a1a333ee5e9b563f854d5fa49d6a8ca2764e97c6eddb214" +checksum = "707114b52a152fa7bdb290cd7cd5912d9467273b6d74e21b8d81aca1f8533f6b" dependencies = [ "typenum", ] @@ -832,20 +810,6 @@ version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" -[[package]] -name = "im-rc" -version = "15.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af1955a75fa080c677d3972822ec4bad316169ab1cfc6c257a942c2265dbe5fe" -dependencies = [ - "bitmaps", - "rand_core 0.6.4", - "rand_xoshiro", - "sized-chunks", - "typenum", - "version_check", -] - [[package]] name = "indexmap" version = "2.14.0" @@ -853,16 +817,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" dependencies = [ "equivalent", - "hashbrown 0.17.0", + "hashbrown 0.17.1", "serde", "serde_core", ] [[package]] name = "insta" -version = "1.47.2" +version = "1.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b4a6248eb93a4401ed2f37dfe8ea592d3cf05b7cf4f8efa867b6895af7e094e" +checksum = "86f0f8fee8c926415c58d6ae43a08523a26faccb2323f5e6b644fe7dd4ef6b82" dependencies = [ "console", "once_cell", @@ -913,23 +877,22 @@ dependencies = [ [[package]] name = "jobserver" -version = "0.1.34" +version = "0.1.35" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33" +checksum = "1c00acbd29eabad4a2392fa0e921c874934dbbf4194312ad20f04a0ed67a3cb3" dependencies = [ - "getrandom 0.3.4", + "getrandom 0.4.3", "libc", ] [[package]] name = "js-sys" -version = "0.3.95" +version = "0.3.103" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2964e92d1d9dc3364cae4d718d93f227e3abb088e747d92e0395bfdedf1c12ca" +checksum = "53b44bfcdb3f8d5837a46dae1ca9660a837176eee74a28b229bc626816589102" dependencies = [ "cfg-if", "futures-util", - "once_cell", "wasm-bindgen", ] @@ -941,9 +904,9 @@ checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" [[package]] name = "libc" -version = "0.2.184" +version = "0.2.189" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48f5d2a454e16a5ea0f4ced81bd44e4cfc7bd3a507b61887c99fd3538b28e4af" +checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2" [[package]] name = "libm" @@ -953,9 +916,9 @@ checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" [[package]] name = "libredox" -version = "0.1.16" +version = "0.1.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e02f3bb43d335493c96bf3fd3a321600bf6bd07ed34bc64118e9293bdffea46c" +checksum = "2026a5056764a10b2bf5d56488cba40da507f5493a6a429340e2004d9ed085fa" dependencies = [ "libc", ] @@ -968,24 +931,21 @@ checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" [[package]] name = "log" -version = "0.4.29" +version = "0.4.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" +checksum = "0ceec5bc11778974d1bcb055b18002eba7f4b3518b6a0081b3af5f21666da9ad" [[package]] name = "mach2" -version = "0.4.3" +version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d640282b302c0bb0a2a8e0233ead9035e3bed871f0b7e81fe4a1ec829765db44" -dependencies = [ - "libc", -] +checksum = "dae608c151f68243f2b000364e1f7b186d9c29845f7d2d85bd31b9ad77ad552b" [[package]] name = "memchr" -version = "2.8.0" +version = "2.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" +checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" [[package]] name = "memfd" @@ -1017,9 +977,9 @@ dependencies = [ [[package]] name = "nix" -version = "0.31.2" +version = "0.31.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d6d0705320c1e6ba1d912b5e37cf18071b6c2e9b7fa8215a1e8a7651966f5d3" +checksum = "cf20d2fde8ff38632c426f1165ed7436270b44f199fc55284c38276f9db47c3d" dependencies = [ "bitflags", "cfg-if", @@ -1048,12 +1008,12 @@ dependencies = [ [[package]] name = "object" -version = "0.38.1" +version = "0.39.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "271638cd5fa9cca89c4c304675ca658efc4e64a66c716b7cfe1afb4b9611dbbc" +checksum = "2e5a6c098c7a3b6547378093f5cc30bc54fd361ce711e05293a5cc589562739b" dependencies = [ "crc32fast", - "hashbrown 0.16.1", + "hashbrown 0.17.1", "indexmap", "memchr", ] @@ -1119,21 +1079,11 @@ dependencies = [ "zerocopy", ] -[[package]] -name = "prettyplease" -version = "0.2.37" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" -dependencies = [ - "proc-macro2", - "syn", -] - [[package]] name = "proc-macro2" -version = "1.0.106" +version = "1.0.107" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9" dependencies = [ "unicode-ident", ] @@ -1159,9 +1109,9 @@ dependencies = [ [[package]] name = "pulley-interpreter" -version = "43.0.1" +version = "47.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "010dec3755eb61b2f1051ecb3611b718460b7a74c131e474de2af20a845938af" +checksum = "bc5c8c21ea032e4efbdf2d067dc45171779dbe0c8ecf20ef4a57efa7474f2b0a" dependencies = [ "cranelift-bitset", "log", @@ -1171,13 +1121,13 @@ dependencies = [ [[package]] name = "pulley-macros" -version = "43.0.1" +version = "47.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ad360c32e85ca4b083ac0e2b6856e8f11c3d5060dafa7d5dc57b370857fa3018" +checksum = "9f10925455d5dde962e3604eade797ba5488644c8ee14a44191e2f2b58980574" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.119", ] [[package]] @@ -1188,9 +1138,9 @@ checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0" [[package]] name = "quote" -version = "1.0.45" +version = "1.0.47" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" +checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001" dependencies = [ "proc-macro2", ] @@ -1219,12 +1169,12 @@ dependencies = [ [[package]] name = "rand" -version = "0.9.3" +version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ec095654a25171c2124e9e3393a930bddbffdc939556c914957a4c3e0a87166" +checksum = "b9ef1d0d795eb7d84685bca4f72f3649f064e6641543d3a8c415898726a57b41" dependencies = [ "rand_chacha", - "rand_core 0.9.5", + "rand_core", ] [[package]] @@ -1234,15 +1184,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" dependencies = [ "ppv-lite86", - "rand_core 0.9.5", + "rand_core", ] -[[package]] -name = "rand_core" -version = "0.6.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" - [[package]] name = "rand_core" version = "0.9.5" @@ -1258,23 +1202,14 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "513962919efc330f829edb2535844d1b912b0fbe2ca165d613e4e8788bb05a5a" dependencies = [ - "rand_core 0.9.5", -] - -[[package]] -name = "rand_xoshiro" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6f97cdb2a36ed4183de61b2f824cc45c9f1037f28afe0a322e9fff4c108b5aaa" -dependencies = [ - "rand_core 0.6.4", + "rand_core", ] [[package]] name = "rayon" -version = "1.11.0" +version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "368f01d005bf8fd9b1206fb6fa653e6c4a81ceb1466406b81792d87c5677a58f" +checksum = "fb39b166781f92d482534ef4b4b1b2568f42613b53e5b6c160e24cfbfa30926d" dependencies = [ "either", "rayon-core", @@ -1303,35 +1238,36 @@ dependencies = [ [[package]] name = "regalloc2" -version = "0.15.0" +version = "0.15.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "952ddbfc6f9f64d006c3efd8c9851a6ba2f2b944ba94730db255d55006e0ffda" +checksum = "757712e8e61590d6d4f5d563483755538b5aa13467837a3b41cd9832509a7f85" dependencies = [ "allocator-api2", "bumpalo", - "hashbrown 0.15.5", + "hashbrown 0.17.1", "log", "rustc-hash", + "serde", "smallvec", ] [[package]] name = "regex-syntax" -version = "0.8.10" +version = "0.8.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" +checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4" [[package]] name = "rustc-demangle" -version = "0.1.27" +version = "0.1.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b50b8869d9fc858ce7266cce0194bd74df58b9d0e3f6df3a9fc8eb470d95c09d" +checksum = "b74b56ffa8bb2830709a538c2cbcae9aa062db0d2a42563bfb09bdaae44020eb" [[package]] name = "rustc-hash" -version = "2.1.2" +version = "2.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94300abf3f1ae2e2b8ffb7b58043de3d399c73fa6f4b73826402a5c457614dbe" +checksum = "6b1e7f9a428571be2dc5bc0505c13fb6bf936822b894ec87abf8a08a4e51742d" [[package]] name = "rustix" @@ -1348,9 +1284,9 @@ dependencies = [ [[package]] name = "rustversion" -version = "1.0.22" +version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" +checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f" [[package]] name = "rusty-fork" @@ -1366,9 +1302,9 @@ dependencies = [ [[package]] name = "rustyline" -version = "18.0.0" +version = "18.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a990b25f351b25139ddc7f21ee3f6f56f86d6846b74ac8fad3a719a287cd4a0" +checksum = "53f6a737db68eb1a8ccff86b584b2fc13eca6a7bb6f78ebc7c529547e3ab9684" dependencies = [ "bitflags", "cfg-if", @@ -1385,12 +1321,6 @@ dependencies = [ "windows-sys", ] -[[package]] -name = "ryu" -version = "1.0.23" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" - [[package]] name = "same-file" version = "1.0.6" @@ -1418,9 +1348,9 @@ checksum = "cd0b0ec5f1c1ca621c432a25813d8d60c88abe6d3e08a3eb9cf37d97a0fe3d73" [[package]] name = "serde" -version = "1.0.228" +version = "1.0.229" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba" dependencies = [ "serde_core", "serde_derive", @@ -1428,29 +1358,29 @@ dependencies = [ [[package]] name = "serde_core" -version = "1.0.228" +version = "1.0.229" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.228" +version = "1.0.229" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 3.0.3", ] [[package]] name = "serde_json" -version = "1.0.149" +version = "1.0.151" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" +checksum = "c841b55ecdae098c80dcae9cf767f6f8a0c2cdb3416bbef72181df4d0fe73f14" dependencies = [ "itoa", "memchr", @@ -1468,19 +1398,6 @@ dependencies = [ "serde_core", ] -[[package]] -name = "serde_yaml" -version = "0.9.34+deprecated" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a8b1a1a2ebf674015cc02edccce75287f1a0130d394307b36743c2f5d504b47" -dependencies = [ - "indexmap", - "itoa", - "ryu", - "serde", - "unsafe-libyaml", -] - [[package]] name = "sha1" version = "0.11.0" @@ -1489,7 +1406,7 @@ checksum = "aacc4cc499359472b4abe1bf11d0b12e688af9a805fa5e3016f9a386dc2d0214" dependencies = [ "cfg-if", "cpufeatures 0.3.0", - "digest 0.11.2", + "digest 0.11.3", ] [[package]] @@ -1511,14 +1428,14 @@ checksum = "446ba717509524cb3f22f17ecc096f10f4822d76ab5c0b9822c5f9c284e825f4" dependencies = [ "cfg-if", "cpufeatures 0.3.0", - "digest 0.11.2", + "digest 0.11.3", ] [[package]] name = "shlex" -version = "1.3.0" +version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" +checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" [[package]] name = "similar" @@ -1526,16 +1443,6 @@ version = "2.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbbb5d9659141646ae647b42fe094daf6c6192d1620870b449d9557f748b2daa" -[[package]] -name = "sized-chunks" -version = "0.6.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "16d69225bde7a69b235da73377861095455d298f2b970996eec25ddbb42b3d1e" -dependencies = [ - "bitmaps", - "typenum", -] - [[package]] name = "slab" version = "0.4.12" @@ -1544,9 +1451,9 @@ checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" [[package]] name = "smallvec" -version = "1.15.1" +version = "1.15.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" +checksum = "8ed6a63f02c8539c91a8685a86f4099661ba3da017932f6ebbea6de3f0fa7c90" dependencies = [ "serde", ] @@ -1565,9 +1472,20 @@ checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" [[package]] name = "syn" -version = "2.0.117" +version = "2.0.119" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" +checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "3.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53e9bae58849f64dfa4f5d5ae372c8341f7305f82a3868709269343628b659a3" dependencies = [ "proc-macro2", "quote", @@ -1587,7 +1505,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" dependencies = [ "fastrand", - "getrandom 0.4.2", + "getrandom 0.4.3", "once_cell", "rustix", "windows-sys", @@ -1613,11 +1531,11 @@ dependencies = [ [[package]] name = "thiserror" -version = "2.0.18" +version = "2.0.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" +checksum = "09a43598840e33d5b0331f38c5e30d13bb11c11210a4b58f0d9b18a5a5eefcd9" dependencies = [ - "thiserror-impl 2.0.18", + "thiserror-impl 2.0.19", ] [[package]] @@ -1628,18 +1546,18 @@ checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.119", ] [[package]] name = "thiserror-impl" -version = "2.0.18" +version = "2.0.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" +checksum = "43cbfe0cf76104d42a574802844187e84a305e531ed54455f11fbde0f10541cd" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 3.0.3", ] [[package]] @@ -1668,24 +1586,24 @@ dependencies = [ [[package]] name = "toml_parser" -version = "1.1.2+spec-1.1.0" +version = "1.1.3+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2abe9b86193656635d2411dc43050282ca48aa31c2451210f4202550afb7526" +checksum = "1d38ac1cf9b95face32296c0a3ede1fdc270627c9d9c02a7274dd6d960dc4d56" dependencies = [ - "winnow 1.0.1", + "winnow 1.0.4", ] [[package]] name = "toml_writer" -version = "1.1.1+spec-1.1.0" +version = "1.1.2+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "756daf9b1013ebe47a8776667b466417e2d4c5679d441c26230efd9ef78692db" +checksum = "7d56353a2a665ad0f41a421187180aab746c8c325620617ad883a99a1cbe66d2" [[package]] name = "typenum" -version = "1.19.0" +version = "1.20.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" +checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20" [[package]] name = "unarray" @@ -1701,9 +1619,9 @@ checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" [[package]] name = "unicode-segmentation" -version = "1.13.2" +version = "1.13.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9629274872b2bfaf8d66f5f15725007f635594914870f65218920345aa11aa8c" +checksum = "c6f5d3c3b1bf09027a88a6bc961fc00497d651009560b5463668dc81b0fa87a8" [[package]] name = "unicode-width" @@ -1711,18 +1629,6 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254" -[[package]] -name = "unicode-xid" -version = "0.2.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" - -[[package]] -name = "unsafe-libyaml" -version = "0.2.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861" - [[package]] name = "utf8parse" version = "0.2.2" @@ -1731,9 +1637,9 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" [[package]] name = "uuid" -version = "1.23.0" +version = "1.24.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ac8b6f42ead25368cf5b098aeb3dc8a1a2c05a3eee8a9a1a68c640edbfc79d9" +checksum = "bf3923a6f5c4c6382e0b653c4117f48d631ea17f38ed86e2a828e6f7412f5239" dependencies = [ "js-sys", "wasm-bindgen", @@ -1747,7 +1653,7 @@ checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" [[package]] name = "wafer" -version = "0.1.0" +version = "0.2.0" dependencies = [ "anyhow", "clap", @@ -1758,22 +1664,22 @@ dependencies = [ [[package]] name = "wafer-core" -version = "0.1.0" +version = "0.2.0" dependencies = [ "anyhow", "insta", "proptest", "sha1", "sha2 0.11.0", - "thiserror 2.0.18", - "wasm-encoder 0.246.2", - "wasmparser 0.246.2", + "thiserror 2.0.19", + "wasm-encoder 0.255.0", + "wasmparser 0.255.0", "wasmtime", ] [[package]] name = "wafer-web" -version = "0.1.0" +version = "0.2.0" dependencies = [ "anyhow", "js-sys", @@ -1781,7 +1687,7 @@ dependencies = [ "wafer-core", "wasm-bindgen", "wasm-bindgen-test", - "wasm-encoder 0.246.2", + "wasm-encoder 0.255.0", ] [[package]] @@ -1811,27 +1717,18 @@ checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" [[package]] name = "wasip2" -version = "1.0.2+wasi-0.2.9" +version = "1.0.4+wasi-0.2.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9517f9239f02c069db75e65f174b3da828fe5f5b945c4dd26bd25d89c03ebcf5" -dependencies = [ - "wit-bindgen", -] - -[[package]] -name = "wasip3" -version = "0.4.0+wasi-0.3.0-rc-2026-01-06" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5" +checksum = "b67efb37e106e55ce722a510d6b5f9c17f083e5fc79afc2badeb12cc313d9487" dependencies = [ "wit-bindgen", ] [[package]] name = "wasm-bindgen" -version = "0.2.118" +version = "0.2.126" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0bf938a0bacb0469e83c1e148908bd7d5a6010354cf4fb73279b7447422e3a89" +checksum = "4b067c0c11094aef6b7a801c1e34a26affafdf3d051dba08456b868789aaf9a4" dependencies = [ "cfg-if", "once_cell", @@ -1842,9 +1739,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-futures" -version = "0.4.68" +version = "0.4.76" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f371d383f2fb139252e0bfac3b81b265689bf45b6874af544ffa4c975ac1ebf8" +checksum = "c62df1340f32221cb9c54d6a27b030e3dba64361d4a95bed55f9aacb44da291d" dependencies = [ "js-sys", "wasm-bindgen", @@ -1852,9 +1749,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.118" +version = "0.2.126" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eeff24f84126c0ec2db7a449f0c2ec963c6a49efe0698c4242929da037ca28ed" +checksum = "167ce5e579f6bcf889c4f7175a8a5a585de84e8ff93976ce393efa5f2837aab1" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -1862,31 +1759,31 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.118" +version = "0.2.126" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d08065faf983b2b80a79fd87d8254c409281cf7de75fc4b773019824196c904" +checksum = "f3997c7839262f4ef12cf90b818d6340c18e80f263f1a94bf157d0ec4420380e" dependencies = [ "bumpalo", "proc-macro2", "quote", - "syn", + "syn 2.0.119", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" -version = "0.2.118" +version = "0.2.126" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5fd04d9e306f1907bd13c6361b5c6bfc7b3b3c095ed3f8a9246390f8dbdee129" +checksum = "dc1b4cb0cc549fcf58d7dfc081778139b3d283a081644e833e84682ad71cea24" dependencies = [ "unicode-ident", ] [[package]] name = "wasm-bindgen-test" -version = "0.3.68" +version = "0.3.76" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6bb55e2540ad1c56eec35fd63e2aea15f83b11ce487fd2de9ad11578dfc047ea" +checksum = "2a0d555ca874445df8d314f94f5c948a4e74e5418f332c89f660a3d8310a96f4" dependencies = [ "async-trait", "cast", @@ -1906,104 +1803,66 @@ dependencies = [ [[package]] name = "wasm-bindgen-test-macro" -version = "0.3.68" +version = "0.3.76" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "caf0ca1bd612b988616bac1ab34c4e4290ef18f7148a1d8b7f31c150080e9295" +checksum = "94eb68555b95bcea5e8cf4abe280b529049479fa995bfc23734af96a6aedc120" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.119", ] [[package]] name = "wasm-bindgen-test-shared" -version = "0.2.118" +version = "0.2.126" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23cda5ecc67248c48d3e705d3e03e00af905769b78b9d2a1678b663b8b9d4472" +checksum = "c31d56021e873866c968588ed85ccdf56db5c426e44afdb4618c39895104b920" [[package]] name = "wasm-compose" -version = "0.245.1" +version = "0.252.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5fd23d12cc95c451c1306db5bc63075fbebb612bb70c53b4237b1ce5bc178343" +checksum = "d59b710751a35d54732a63851cdfacbfb2266b7160ce174faf269d3f4e84e31b" dependencies = [ "anyhow", "heck", - "im-rc", "indexmap", "log", "petgraph", - "serde", - "serde_derive", - "serde_yaml", "smallvec", - "wasm-encoder 0.245.1", - "wasmparser 0.245.1", + "wasm-encoder 0.252.0", + "wasmparser 0.252.0", "wat", ] [[package]] name = "wasm-encoder" -version = "0.244.0" +version = "0.252.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319" +checksum = "8185ae345fa5687c054626ff9a50e7089797a343d9904d1dc9820eb4c4d3196f" dependencies = [ "leb128fmt", - "wasmparser 0.244.0", + "wasmparser 0.252.0", ] [[package]] name = "wasm-encoder" -version = "0.245.1" +version = "0.255.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f9dca005e69bf015e45577e415b9af8c67e8ee3c0e38b5b0add5aa92581ed5c" +checksum = "9b524283fb5df62eec102ed0574838961bdd7ba5ac9c50d38e2756c51c971a42" dependencies = [ "leb128fmt", - "wasmparser 0.245.1", -] - -[[package]] -name = "wasm-encoder" -version = "0.246.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61fb705ce81adde29d2a8e99d87995e39a6e927358c91398f374474746070ef7" -dependencies = [ - "leb128fmt", - "wasmparser 0.246.2", -] - -[[package]] -name = "wasm-metadata" -version = "0.244.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909" -dependencies = [ - "anyhow", - "indexmap", - "wasm-encoder 0.244.0", - "wasmparser 0.244.0", + "wasmparser 0.255.0", ] [[package]] name = "wasmparser" -version = "0.244.0" +version = "0.252.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" +checksum = "d3eb099dcadcde5be9eef55e3a337128efd4e44b4c93122487e4d2e4e1c6627c" dependencies = [ "bitflags", - "hashbrown 0.15.5", - "indexmap", - "semver", -] - -[[package]] -name = "wasmparser" -version = "0.245.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4f08c9adee0428b7bddf3890fc27e015ac4b761cc608c822667102b8bfd6995e" -dependencies = [ - "bitflags", - "hashbrown 0.16.1", + "hashbrown 0.17.1", "indexmap", "semver", "serde", @@ -2011,12 +1870,12 @@ dependencies = [ [[package]] name = "wasmparser" -version = "0.246.2" +version = "0.255.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "71cde4757396defafd25417cfb36aa3161027d06d865b0c24baaae229aac005d" +checksum = "e8e329ef4b5d46e73b91d3ac6924417cad55a8cbbf869c199283383427c3320b" dependencies = [ "bitflags", - "hashbrown 0.16.1", + "hashbrown 0.17.1", "indexmap", "semver", "serde", @@ -2024,20 +1883,20 @@ dependencies = [ [[package]] name = "wasmprinter" -version = "0.245.1" +version = "0.252.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f41517a3716fbb8ccf46daa9c1325f760fcbff5168e75c7392288e410b91ac8" +checksum = "7142797de29b35ab8dbf15c00f55fda75d409da4c423a8ab8bd6b667a785824b" dependencies = [ "anyhow", "termcolor", - "wasmparser 0.245.1", + "wasmparser 0.252.0", ] [[package]] name = "wasmtime" -version = "43.0.1" +version = "47.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce205cd643d661b5ba5ba4717e13730262e8cdbc8f2eacbc7b906d45c1a74026" +checksum = "c80ca6098e0d4d06886d91d7f2cc3cb6623eb583c4c0ab3c89cbfb6098c8586c" dependencies = [ "addr2line", "async-trait", @@ -2068,8 +1927,8 @@ dependencies = [ "target-lexicon", "tempfile", "wasm-compose", - "wasm-encoder 0.245.1", - "wasmparser 0.245.1", + "wasm-encoder 0.252.0", + "wasmparser 0.252.0", "wasmtime-environ", "wasmtime-internal-cache", "wasmtime-internal-component-macro", @@ -2081,16 +1940,16 @@ dependencies = [ "wasmtime-internal-jit-icache-coherence", "wasmtime-internal-unwinder", "wasmtime-internal-versioned-export-macros", - "wasmtime-internal-winch", "wat", "windows-sys", + "wit-parser", ] [[package]] name = "wasmtime-environ" -version = "43.0.1" +version = "47.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b8b78abf3677d4a0a5db82e5015b4d085ff3a1b8b472cbb8c70d4b769f019ce" +checksum = "134f9d136d29c76f6c1b4c9b468e97a2efc38c7d49fd188e39b64870b2fea701" dependencies = [ "anyhow", "cpp_demangle", @@ -2098,7 +1957,7 @@ dependencies = [ "cranelift-bitset", "cranelift-entity", "gimli", - "hashbrown 0.16.1", + "hashbrown 0.17.1", "indexmap", "log", "object", @@ -2110,8 +1969,8 @@ dependencies = [ "sha2 0.10.9", "smallvec", "target-lexicon", - "wasm-encoder 0.245.1", - "wasmparser 0.245.1", + "wasm-encoder 0.252.0", + "wasmparser 0.252.0", "wasmprinter", "wasmtime-internal-component-util", "wasmtime-internal-core", @@ -2119,9 +1978,9 @@ dependencies = [ [[package]] name = "wasmtime-internal-cache" -version = "43.0.1" +version = "47.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e4fd4103ba413c0da2e636f73490c6c8e446d708cbde7573703941bc3d6a448" +checksum = "65db2eb1bfc5371a3b4107dbf539d0b93d05016fc29625b1648146b47db02071" dependencies = [ "base64", "directories-next", @@ -2139,42 +1998,42 @@ dependencies = [ [[package]] name = "wasmtime-internal-component-macro" -version = "43.0.1" +version = "47.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d3d6914f34be2f9d78d8ee9f422e834dfc204e71ccce697205fae95fed87892" +checksum = "2bf7b91fed3fc34781d57f9c6654ea2513bf0a99f7b0b0523c00de1e6efebe1c" dependencies = [ "anyhow", "proc-macro2", "quote", - "syn", + "syn 2.0.119", "wasmtime-internal-component-util", "wasmtime-internal-wit-bindgen", - "wit-parser 0.245.1", + "wit-parser", ] [[package]] name = "wasmtime-internal-component-util" -version = "43.0.1" +version = "47.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3751b0616b914fdd87fe1bf804694a078f321b000338e6476bc48a4d6e454f21" +checksum = "fc8a678149885cae00289f806fbe74c7863084cf74cace0b8dc73602279400e1" [[package]] name = "wasmtime-internal-core" -version = "43.0.1" +version = "47.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22632b187e1b0716f1b9ac57ad29013bed33175fcb19e10bb6896126f82fac67" +checksum = "8a0092c4b9d070ac5e278b6d0db10f5e71214190f0347ca57502b4692f796321" dependencies = [ "anyhow", - "hashbrown 0.16.1", + "hashbrown 0.17.1", "libm", "serde", ] [[package]] name = "wasmtime-internal-cranelift" -version = "43.0.1" +version = "47.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b3ca07b3e0bb3429674b173b5800577719d600774dd81bff58f775c0aaa64ee" +checksum = "6851ebc9e03cab23d9821d2b2505d380bdfe38f35ccec945247b05274d85c98b" dependencies = [ "cfg-if", "cranelift-codegen", @@ -2189,8 +2048,8 @@ dependencies = [ "pulley-interpreter", "smallvec", "target-lexicon", - "thiserror 2.0.18", - "wasmparser 0.245.1", + "thiserror 2.0.19", + "wasmparser 0.252.0", "wasmtime-environ", "wasmtime-internal-core", "wasmtime-internal-unwinder", @@ -2199,9 +2058,9 @@ dependencies = [ [[package]] name = "wasmtime-internal-fiber" -version = "43.0.1" +version = "47.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "20c8b2c9704eb1f33ead025ec16038277ccb63d0a14c31e99d5b765d7c36da55" +checksum = "9b26da6d5f60d4c438da70bba3553fe810a840533a64156be287dca2081c6991" dependencies = [ "cc", "cfg-if", @@ -2214,9 +2073,9 @@ dependencies = [ [[package]] name = "wasmtime-internal-jit-debug" -version = "43.0.1" +version = "47.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d950310d07391d34369f62c48336ebb14eacbd4d6f772bb5f349c24e838e0664" +checksum = "ed621ba25d7bf78b7edd7b4749abbb27c2e5cdba836c9504a424ee74b6c23149" dependencies = [ "cc", "object", @@ -2226,9 +2085,9 @@ dependencies = [ [[package]] name = "wasmtime-internal-jit-icache-coherence" -version = "43.0.1" +version = "47.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3606662c156962d096be3127b8b8ae8ee2f8be3f896dad29259ff01ddb64abfd" +checksum = "5684ba160951baad06a725696f3c590e2fb0e8067c2aebee27bf7f9259058e85" dependencies = [ "cfg-if", "libc", @@ -2238,9 +2097,9 @@ dependencies = [ [[package]] name = "wasmtime-internal-unwinder" -version = "43.0.1" +version = "47.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75eef0747e52dc545b075f64fd0e0cc237ae738e641266b1970e07e2d744bc32" +checksum = "112eead527bffa8ff0646a11fb4339a9d52ddd1da2b9a6fce4aff84c815dd94f" dependencies = [ "cfg-if", "cranelift-codegen", @@ -2251,63 +2110,46 @@ dependencies = [ [[package]] name = "wasmtime-internal-versioned-export-macros" -version = "43.0.1" +version = "47.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d8b0a5dab02a8fb527f547855ecc0e05f9fdc3d5bd57b8b080349408f9a6cece" +checksum = "153592e0bed824fc13c6696203fa1bd7bd20eb355316475e39a55f081ef80eca" dependencies = [ "proc-macro2", "quote", - "syn", -] - -[[package]] -name = "wasmtime-internal-winch" -version = "43.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8007342bd12ff400293a817973f7ecd6f1d9a8549a53369a9c1af357166f1f1e" -dependencies = [ - "cranelift-codegen", - "gimli", - "log", - "object", - "target-lexicon", - "wasmparser 0.245.1", - "wasmtime-environ", - "wasmtime-internal-cranelift", - "winch-codegen", + "syn 2.0.119", ] [[package]] name = "wasmtime-internal-wit-bindgen" -version = "43.0.1" +version = "47.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7900c3e3c1d6e475bc225d73b02d6d5484815f260022e6964dca9558e50dd01a" +checksum = "c456ad6e81e0f46abfeca43687d18ea15e289c395b262ea6e0023e2682e088be" dependencies = [ "anyhow", "bitflags", "heck", "indexmap", - "wit-parser 0.245.1", + "wit-parser", ] [[package]] name = "wast" -version = "246.0.2" +version = "255.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fe3fe8e3bf88ad96d031b4181ddbd64634b17cb0d06dfc3de589ef43591a9a62" +checksum = "55ffec530f199bd3d553ac442c13dd108353cad533cad8514bc41e1f1f0fe686" dependencies = [ "bumpalo", "leb128fmt", "memchr", "unicode-width", - "wasm-encoder 0.246.2", + "wasm-encoder 0.255.0", ] [[package]] name = "wat" -version = "1.246.2" +version = "1.255.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4bd7fda1199b94fff395c2d19a153f05dbe7807630316fa9673367666fd2ad8c" +checksum = "dda82c82e1486c7eed42a0465e544d80fff37abc3b39482e0d34dbaabe2fe5b1" dependencies = [ "wast", ] @@ -2343,25 +2185,6 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" -[[package]] -name = "winch-codegen" -version = "43.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eb9f45f7172a2628c8317766e427babc0a400f9d10b1c0f0b0617c5ed5b79de6" -dependencies = [ - "cranelift-assembler-x64", - "cranelift-codegen", - "gimli", - "regalloc2", - "smallvec", - "target-lexicon", - "thiserror 2.0.18", - "wasmparser 0.245.1", - "wasmtime-environ", - "wasmtime-internal-core", - "wasmtime-internal-cranelift", -] - [[package]] name = "windows-link" version = "0.2.1" @@ -2385,87 +2208,24 @@ checksum = "df79d97927682d2fd8adb29682d1140b343be4ac0f08fd68b7765d9c059d3945" [[package]] name = "winnow" -version = "1.0.1" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09dac053f1cd375980747450bfc7250c264eaae0583872e845c0c7cd578872b5" +checksum = "23b97319f7b8343df12cc98938e5c3eb436064524c8d2b4e30a1d3a36eecdf81" [[package]] name = "wit-bindgen" -version = "0.51.0" +version = "0.57.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" -dependencies = [ - "wit-bindgen-rust-macro", -] - -[[package]] -name = "wit-bindgen-core" -version = "0.51.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc" -dependencies = [ - "anyhow", - "heck", - "wit-parser 0.244.0", -] - -[[package]] -name = "wit-bindgen-rust" -version = "0.51.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21" -dependencies = [ - "anyhow", - "heck", - "indexmap", - "prettyplease", - "syn", - "wasm-metadata", - "wit-bindgen-core", - "wit-component", -] - -[[package]] -name = "wit-bindgen-rust-macro" -version = "0.51.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a" -dependencies = [ - "anyhow", - "prettyplease", - "proc-macro2", - "quote", - "syn", - "wit-bindgen-core", - "wit-bindgen-rust", -] - -[[package]] -name = "wit-component" -version = "0.244.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" -dependencies = [ - "anyhow", - "bitflags", - "indexmap", - "log", - "serde", - "serde_derive", - "serde_json", - "wasm-encoder 0.244.0", - "wasm-metadata", - "wasmparser 0.244.0", - "wit-parser 0.244.0", -] +checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" [[package]] name = "wit-parser" -version = "0.244.0" +version = "0.252.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736" +checksum = "4266bea110371c620ccf3201c5023676046bc4556e5c7cfb5d500bda5ebc162d" dependencies = [ "anyhow", + "hashbrown 0.17.1", "id-arena", "indexmap", "log", @@ -2473,54 +2233,35 @@ dependencies = [ "serde", "serde_derive", "serde_json", - "unicode-xid", - "wasmparser 0.244.0", -] - -[[package]] -name = "wit-parser" -version = "0.245.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "330698718e82983499419494dd1e3d7811a457a9bf9f69734e8c5f07a2547929" -dependencies = [ - "anyhow", - "hashbrown 0.16.1", - "id-arena", - "indexmap", - "log", - "semver", - "serde", - "serde_derive", - "serde_json", - "unicode-xid", - "wasmparser 0.245.1", + "unicode-ident", + "wasmparser 0.252.0", ] [[package]] name = "zerocopy" -version = "0.8.48" +version = "0.8.55" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eed437bf9d6692032087e337407a86f04cd8d6a16a37199ed57949d415bd68e9" +checksum = "b5a105cd7b140f6eeec8acff2ea38135d3cab283ada58540f629fe51e46696eb" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.8.48" +version = "0.8.55" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70e3cd084b1788766f53af483dd21f93881ff30d7320490ec3ef7526d203bad4" +checksum = "0fe976fb70c78cd64cccfe3a6fc142244e8a77b70959b30faf9d0ac37ee228eb" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.119", ] [[package]] name = "zmij" -version = "1.0.21" +version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" +checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b" [[package]] name = "zstd" diff --git a/Cargo.toml b/Cargo.toml index 459aa29..864eec7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,7 +3,7 @@ members = ["crates/*"] resolver = "2" [workspace.package] -version = "0.1.0" +version = "0.2.0" edition = "2024" license = "MIT OR Apache-2.0" repository = "https://github.com/ok2/wafer" @@ -41,9 +41,9 @@ needless_collect = "warn" or_fun_call = "warn" [workspace.dependencies] -wasm-encoder = "0.246" -wasmparser = "0.246" -wasmtime = "43" +wasm-encoder = "0.255" +wasmparser = "0.255" +wasmtime = "47" anyhow = "1" thiserror = "2" proptest = "1" diff --git a/Justfile b/Justfile index 1845471..ee98547 100644 --- a/Justfile +++ b/Justfile @@ -47,6 +47,10 @@ bench-opts: 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 deny: cargo deny check @@ -62,6 +66,13 @@ ci: fmt clippy deny test check: 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 diff --git a/README.md b/README.md index 6f120d1..0574cf7 100644 --- a/README.md +++ b/README.md @@ -95,7 +95,7 @@ Times in microseconds. WAFER/gf < 1.0 means WAFER is faster. CONSOL = after `CON ## Testing ```bash -# All tests (~450 currently passing) +# All tests (~570 currently passing) cargo test --workspace # Forth 2012 compliance suite @@ -185,6 +185,7 @@ Over 200 words are implemented across the following categories: | 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 | | Case | `CASE OF ENDOF ENDCASE` | +| Tools | `WORDS SEE SEE-IR HELP INCLUDE INCLUDED .S F.S ? DUMP MARKER REMEMBER EMPTY GILD BYE` | ## Web REPL diff --git a/crates/cli/Cargo.toml b/crates/cli/Cargo.toml index a099458..e29bc0f 100644 --- a/crates/cli/Cargo.toml +++ b/crates/cli/Cargo.toml @@ -9,7 +9,7 @@ license.workspace = true workspace = true [dependencies] -wafer-core = { path = "../core", version = "0.1.0" } +wafer-core = { path = "../core", version = "0.2.0" } wasmtime = { workspace = true } anyhow = { workspace = true } clap = { version = "4", features = ["derive"] } diff --git a/crates/cli/src/main.rs b/crates/cli/src/main.rs index ba17205..96d6840 100644 --- a/crates/cli/src/main.rs +++ b/crates/cli/src/main.rs @@ -137,7 +137,9 @@ fn cmd_build( ) -> anyhow::Result<()> { let source = std::fs::read_to_string(file)?; - let mut vm = ForthVM::::new()?; + // Exported modules are production artifacts: no stack guards by default + let mut vm = ForthVM::::new_with_config(vm_config(false))?; + vm.set_source_loader(fs_loader()); vm.set_recording(true); vm.evaluate(&source)?; @@ -260,18 +262,38 @@ fn cmd_run(file: &str) -> anyhow::Result<()> { Ok(()) } +/// `WaferConfig` for CLI-created VMs. `WAFER_STACK_GUARDS=0|1` overrides +/// the per-command default (REPL/file execution on, build off). +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 +} + +/// Filesystem source loader for INCLUDE/INCLUDED. +fn fs_loader() -> Box anyhow::Result + Send + Sync> { + Box::new(|path| Ok(std::fs::read_to_string(path)?)) +} + /// `wafer` (REPL) or `wafer program.fth` (evaluate and exit) fn cmd_eval_or_repl(file: Option<&str>) -> anyhow::Result<()> { - let mut vm = ForthVM::::new()?; + let mut vm = ForthVM::::new_with_config(vm_config(true))?; + vm.set_source_loader(fs_loader()); match file { Some(file) => { - let source = std::fs::read_to_string(file)?; - vm.evaluate(&source)?; + // Through the include machinery: file:line error context and a + // base directory for nested INCLUDEs. + let result = vm.include(file); let output = vm.take_output(); if !output.is_empty() { print!("{output}"); } + result?; } None => { if !stdin_is_tty() { @@ -285,66 +307,17 @@ fn cmd_eval_or_repl(file: Option<&str>) -> anyhow::Result<()> { if !output.is_empty() { print!("{output}"); } + if vm.bye_requested() { + break; + } } Err(e) => { - eprintln!("Error: {e}"); + eprintln!("Error: {e:#}"); } } } } else { - // Interactive REPL - 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; - } - } - } + run_repl(&mut vm)?; } } } @@ -357,3 +330,162 @@ fn stdin_is_tty() -> bool { use std::io::IsTerminal; std::io::stdin().is_terminal() } + +/// Completes the token under the cursor against the live dictionary. +struct WaferHelper { + words: Vec, +} + +impl rustyline::completion::Completer for WaferHelper { + type Candidate = String; + + fn complete( + &self, + line: &str, + pos: usize, + _ctx: &rustyline::Context<'_>, + ) -> rustyline::Result<(usize, Vec)> { + 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 = 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 { + 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) -> 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 = + 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| { + 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(()) +} diff --git a/crates/core/boot.fth b/crates/core/boot.fth index 83162ea..6a04449 100644 --- a/crates/core/boot.fth +++ b/crates/core/boot.fth @@ -197,8 +197,8 @@ \ TYPE ( c-addr u -- ) output u characters : TYPE 0 ?DO DUP C@ EMIT 1+ LOOP DROP ; -\ SPACES ( n -- ) output n spaces -: SPACES 0 ?DO SPACE LOOP ; +\ SPACES ( n -- ) output n spaces (nothing for n <= 0, per 6.1.2230) +: SPACES 0 MAX 0 ?DO SPACE LOOP ; \ Pictured numeric output constants \ PICT_BUF_TOP = 0x05C0 = 1472, SYSVAR_HLD = 28 @@ -243,6 +243,9 @@ \ U. ( u -- ) print unsigned number and space : U. 0 <# #S #> TYPE SPACE ; +\ ? ( a-addr -- ) fetch and print +: ? @ . ; + \ .R ( n width -- ) print right-justified signed number : .R >R DUP ABS 0 <# #S ROT SIGN #> R> OVER - SPACES TYPE ; @@ -255,6 +258,21 @@ \ D.R ( d width -- ) print right-justified signed double : 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 \ --------------------------------------------------------------- diff --git a/crates/core/src/codegen.rs b/crates/core/src/codegen.rs index 46d26cc..f9dd20d 100644 --- a/crates/core/src/codegen.rs +++ b/crates/core/src/codegen.rs @@ -19,7 +19,10 @@ use wasm_encoder::{ use crate::dictionary::WordId; use crate::error::{WaferError, WaferResult}; use crate::ir::IrOp; -use crate::memory::{CELL_SIZE, SYSVAR_LEAVE_FLAG}; +use crate::memory::{ + CELL_SIZE, DATA_STACK_BASE, DATA_STACK_TOP, FLOAT_STACK_BASE, FLOAT_STACK_TOP, + RETURN_STACK_BASE, RETURN_STACK_TOP, SYSVAR_FAULT_CODE, SYSVAR_LEAVE_FLAG, +}; // --------------------------------------------------------------------------- // Import indices (order matters: imports numbered sequentially by kind) @@ -94,6 +97,9 @@ pub struct CodegenConfig { pub table_size: u32, /// Enable stack-to-local promotion for straight-line words. pub stack_to_local_promotion: bool, + /// Table index of the `_STACK_FAULT_` host word; `Some` enables + /// stack under/overflow guards in the emitted code. + pub stack_guards: Option, } /// Result of compiling a word to WASM. @@ -109,16 +115,73 @@ pub struct CompiledModule { // Instruction-level helpers (free functions that take &mut Function) // --------------------------------------------------------------------------- -/// Decrement the cached `$dsp` local by `CELL_SIZE`. +// Stack-guard emission. The fault word's table index is stashed in a +// thread-local by `compile_word` (None = guards off) so the low-level +// push/pop helpers can stay plain `&mut Function` free functions +// without threading config through every emitter. +thread_local! { + static GUARD_FAULT: std::cell::Cell> = const { std::cell::Cell::new(None) }; +} + +/// With guards on: emit `if { mem[SYSVAR_FAULT_CODE] = code; +/// call _STACK_FAULT_ }`. `cond` must leave an i32 boolean on the +/// operand stack. The fault host word throws, so the `if` never falls +/// through on the failure path. +fn emit_guard(f: &mut Function, code: i32, cond: impl FnOnce(&mut Function)) { + let Some(fault_idx) = GUARD_FAULT.get() else { + return; + }; + cond(f); + f.instruction(&Instruction::If(BlockType::Empty)) + .instruction(&Instruction::I32Const(SYSVAR_FAULT_CODE as i32)) + .instruction(&Instruction::I32Const(code)) + .instruction(&Instruction::I32Store(MEM4)) + .instruction(&Instruction::I32Const(fault_idx as i32)) + .instruction(&Instruction::CallIndirect { + type_index: TYPE_VOID, + table_index: TABLE, + }) + .instruction(&Instruction::End); +} + +/// Guard: data stack has at least `n` cells (else throw -4). +fn guard_dsp_underflow(f: &mut Function, n: u32) { + emit_guard(f, -4, |f| { + f.instruction(&Instruction::LocalGet(CACHED_DSP_LOCAL)) + .instruction(&Instruction::I32Const((n * CELL_SIZE) as i32)) + .instruction(&Instruction::I32Add) + .instruction(&Instruction::I32Const(DATA_STACK_TOP as i32)) + .instruction(&Instruction::I32GtU); + }); +} + +/// Guard: data stack has room for `n` more cells (else throw -3). +fn guard_dsp_overflow(f: &mut Function, n: u32) { + emit_guard(f, -3, |f| { + f.instruction(&Instruction::LocalGet(CACHED_DSP_LOCAL)) + .instruction(&Instruction::I32Const( + (DATA_STACK_BASE + n * CELL_SIZE) as i32, + )) + .instruction(&Instruction::I32LtU); + }); +} + +/// Decrement the cached `$dsp` local by `CELL_SIZE` (allocate one cell). +/// This is the single choke point for data-stack pushes, so the +/// overflow guard lives here. fn dsp_dec(f: &mut Function) { + guard_dsp_overflow(f, 1); f.instruction(&Instruction::LocalGet(CACHED_DSP_LOCAL)) .instruction(&Instruction::I32Const(CELL_SIZE as i32)) .instruction(&Instruction::I32Sub) .instruction(&Instruction::LocalSet(CACHED_DSP_LOCAL)); } -/// Increment the cached `$dsp` local by `CELL_SIZE`. +/// Increment the cached `$dsp` local by `CELL_SIZE` (free one cell). +/// Single choke point for data-stack pops (`DROP` never loads the +/// value, so the underflow guard must sit here, not in `pop`). fn dsp_inc(f: &mut Function) { + guard_dsp_underflow(f, 1); f.instruction(&Instruction::LocalGet(CACHED_DSP_LOCAL)) .instruction(&Instruction::I32Const(CELL_SIZE as i32)) .instruction(&Instruction::I32Add) @@ -160,6 +223,7 @@ fn pop_to(f: &mut Function, local: u32) { /// Read the top of the data stack without popping (value on operand stack). fn peek(f: &mut Function) { + guard_dsp_underflow(f, 1); f.instruction(&Instruction::LocalGet(CACHED_DSP_LOCAL)) .instruction(&Instruction::I32Load(MEM4)); } @@ -182,6 +246,13 @@ fn dsp_reload(f: &mut Function) { /// Push a value from the WASM operand stack onto the return stack via `tmp`. fn rpush_via_local(f: &mut Function, tmp: u32) { + emit_guard(f, -5, |f| { + f.instruction(&Instruction::GlobalGet(RSP)) + .instruction(&Instruction::I32Const( + (RETURN_STACK_BASE + CELL_SIZE) as i32, + )) + .instruction(&Instruction::I32LtU); + }); f.instruction(&Instruction::LocalSet(tmp)); // rsp -= CELL_SIZE f.instruction(&Instruction::GlobalGet(RSP)) @@ -194,8 +265,18 @@ fn rpush_via_local(f: &mut Function, tmp: u32) { .instruction(&Instruction::I32Store(MEM4)); } +/// Guard: return stack is non-empty (else throw -6). +fn guard_rsp_underflow(f: &mut Function) { + emit_guard(f, -6, |f| { + f.instruction(&Instruction::GlobalGet(RSP)) + .instruction(&Instruction::I32Const(RETURN_STACK_TOP as i32)) + .instruction(&Instruction::I32GeU); + }); +} + /// Pop the return stack onto the WASM operand stack. fn rpop(f: &mut Function) { + guard_rsp_underflow(f); f.instruction(&Instruction::GlobalGet(RSP)) .instruction(&Instruction::I32Load(MEM4)); // rsp += CELL_SIZE @@ -207,6 +288,7 @@ fn rpop(f: &mut Function) { /// Peek at the top of the return stack (no pop). fn rpeek(f: &mut Function) { + guard_rsp_underflow(f); f.instruction(&Instruction::GlobalGet(RSP)) .instruction(&Instruction::I32Load(MEM4)); } @@ -253,7 +335,9 @@ struct EmitCtx { } /// Decrement the FSP global by 8 (allocate space for one f64). +/// Single choke point for float pushes: overflow guard lives here. fn fsp_dec(f: &mut Function) { + guard_fsp_overflow(f); f.instruction(&Instruction::GlobalGet(FSP)) .instruction(&Instruction::I32Const(8)) .instruction(&Instruction::I32Sub) @@ -261,7 +345,10 @@ fn fsp_dec(f: &mut Function) { } /// Increment the FSP global by 8 (free space for one f64). +/// Single choke point for float pops (`FDROP` never loads the value): +/// underflow guard lives here. fn fsp_inc(f: &mut Function) { + guard_fsp_underflow(f); f.instruction(&Instruction::GlobalGet(FSP)) .instruction(&Instruction::I32Const(8)) .instruction(&Instruction::I32Add) @@ -278,6 +365,24 @@ fn fpush_via_local(f: &mut Function, tmp: u32) { .instruction(&Instruction::F64Store(MEM8)); } +/// Guard: float stack has room for one more f64 (else throw -44). +fn guard_fsp_overflow(f: &mut Function) { + emit_guard(f, -44, |f| { + f.instruction(&Instruction::GlobalGet(FSP)) + .instruction(&Instruction::I32Const((FLOAT_STACK_BASE + 8) as i32)) + .instruction(&Instruction::I32LtU); + }); +} + +/// Guard: float stack is non-empty (else throw -45). +fn guard_fsp_underflow(f: &mut Function) { + emit_guard(f, -45, |f| { + f.instruction(&Instruction::GlobalGet(FSP)) + .instruction(&Instruction::I32Const(FLOAT_STACK_TOP as i32)) + .instruction(&Instruction::I32GeU); + }); +} + /// Decrement FSP, then store the f64 from local `src` at [FSP]. fn fpush_from_local(f: &mut Function, src: u32) { fsp_dec(f); @@ -295,6 +400,7 @@ fn fpop(f: &mut Function) { /// Load f64 from [FSP] onto the WASM operand stack without popping. fn fpeek(f: &mut Function) { + guard_fsp_underflow(f); f.instruction(&Instruction::GlobalGet(FSP)) .instruction(&Instruction::F64Load(MEM8)); } @@ -793,9 +899,20 @@ fn emit_op(f: &mut Function, op: &IrOp, ctx: &mut EmitCtx) { .instruction(&Instruction::I32Store(MEM4)); } + IrOp::RpFetch => { + // Push the current return-stack pointer onto the data stack. + // `$rsp` lives in a global (not cached), so no writeback needed. + dsp_dec(f); + f.instruction(&Instruction::LocalGet(CACHED_DSP_LOCAL)) + .instruction(&Instruction::GlobalGet(RSP)) + .instruction(&Instruction::I32Store(MEM4)); + } + // -- Compound operations ----------------------------------------------- IrOp::TwoDup => { // ( a b -- a b a b ) + guard_dsp_underflow(f, 2); + guard_dsp_overflow(f, 2); f.instruction(&Instruction::LocalGet(CACHED_DSP_LOCAL)) .instruction(&Instruction::I32Load(MEM4)); // b f.instruction(&Instruction::LocalSet(SCRATCH_BASE)); @@ -822,6 +939,7 @@ fn emit_op(f: &mut Function, op: &IrOp, ctx: &mut EmitCtx) { IrOp::TwoDrop => { // ( a b -- ) + guard_dsp_underflow(f, 2); f.instruction(&Instruction::LocalGet(CACHED_DSP_LOCAL)) .instruction(&Instruction::I32Const((CELL_SIZE * 2) as i32)) .instruction(&Instruction::I32Add) @@ -1133,7 +1251,9 @@ fn is_promotable(ops: &[IrOp]) -> bool { fn is_promotable_body(ops: &[IrOp]) -> bool { for op in ops { match op { - IrOp::Call(_) | IrOp::TailCall(_) | IrOp::Execute | IrOp::SpFetch => return false, + IrOp::Call(_) | IrOp::TailCall(_) | IrOp::Execute | IrOp::SpFetch | IrOp::RpFetch => { + return false; + } IrOp::ToR | IrOp::FromR | IrOp::Exit => return false, IrOp::ForthLocalGet(_) | IrOp::ForthLocalSet(_) => return false, IrOp::ForthFLocalGet(_) | IrOp::ForthFLocalSet(_) => return false, @@ -1545,6 +1665,11 @@ impl StackSim { /// Emit the promoted prologue: load `preload` items from the memory stack /// into WASM locals. fn emit_promoted_prologue(f: &mut Function, preload: u32, sim: &mut StackSim) { + // One entry check covers the whole promoted word: the caller must + // have at least `preload` cells on the data stack. + if preload > 0 { + guard_dsp_underflow(f, preload); + } // Load items: mem[dsp] = top of stack, mem[dsp+4] = second, etc. // We load them top-first, then reverse the sim stack so that // sim.stack[0] = deepest loaded, sim.stack[last] = top. @@ -1575,6 +1700,7 @@ fn emit_promoted_prologue(f: &mut Function, preload: u32, sim: &mut StackSim) { fn emit_promoted_epilogue(f: &mut Function, sim: &mut StackSim) { let remaining = sim.stack.len() as u32; if remaining > 0 { + guard_dsp_overflow(f, remaining); // Decrement cached DSP for the items we're pushing back f.instruction(&Instruction::LocalGet(CACHED_DSP_LOCAL)); f.instruction(&Instruction::I32Const((remaining * CELL_SIZE) as i32)); @@ -2191,6 +2317,9 @@ fn body_needs_return_stack(ops: &[IrOp]) -> bool { match op { IrOp::Call(_) | IrOp::TailCall(_) | IrOp::Execute => return true, IrOp::ToR | IrOp::FromR => return true, + // RP@ observes the return stack, so loop params must be there + // (otherwise inlined RDEPTH/.RS would report an empty stack). + IrOp::RpFetch => return true, // RFetch (I) is handled by loop locals in the fast path — not a problem. // LoopJ is also handled by loop locals. // Only explicit >R / R> / calls force the slow path. @@ -2403,10 +2532,13 @@ fn count_forth_f_locals(ops: &[IrOp]) -> u32 { /// This is the JIT path: each word gets its own module that imports /// shared memory, globals, and function table from the host. pub fn compile_word( - _name: &str, + name: &str, body: &[IrOp], config: &CodegenConfig, ) -> WaferResult { + // Arm (or disarm) stack-guard emission for this compilation. + GUARD_FAULT.set(config.stack_guards); + let mut module = Module::new(); // -- Type section -- @@ -2567,6 +2699,16 @@ pub fn compile_word( code.function(&func); module.section(&code); + // -- Name section: carries the Forth word name into wasmtime trap + // backtraces (best-effort symbolication, WS-008). + let mut names = wasm_encoder::NameSection::new(); + names.module(name); + let mut fn_names = wasm_encoder::NameMap::new(); + fn_names.append(0, "emit"); + fn_names.append(WORD_FUNC, name); + names.functions(&fn_names); + module.section(&names); + let bytes = module.finish(); // Validate @@ -2871,8 +3013,9 @@ pub fn compile_consolidated_module( words: &[(WordId, Vec)], local_fn_map: &HashMap, table_size: u32, + stack_guards: Option, ) -> WaferResult> { - compile_multi_word_module(words, local_fn_map, table_size, None) + compile_multi_word_module(words, local_fn_map, table_size, None, stack_guards) } /// Compile an exportable WASM module with embedded memory and metadata. @@ -2885,8 +3028,9 @@ pub fn compile_exportable_module( local_fn_map: &HashMap, table_size: u32, export: &ExportSections<'_>, + stack_guards: Option, ) -> WaferResult> { - compile_multi_word_module(words, local_fn_map, table_size, Some(export)) + compile_multi_word_module(words, local_fn_map, table_size, Some(export), stack_guards) } /// Internal: build a multi-word WASM module. When `export` is `Some`, adds @@ -2896,7 +3040,11 @@ fn compile_multi_word_module( local_fn_map: &HashMap, table_size: u32, export: Option<&ExportSections<'_>>, + stack_guards: Option, ) -> WaferResult> { + // Arm (or disarm) stack-guard emission for this module. + GUARD_FAULT.set(stack_guards); + let has_data = export.is_some_and(|e| !e.memory_snapshot.is_empty()); let mut module = Module::new(); @@ -3125,6 +3273,7 @@ mod tests { base_fn_index: 0, table_size: 16, stack_to_local_promotion: true, + stack_guards: None, } } @@ -3349,6 +3498,7 @@ mod tests { base_fn_index: 7, table_size: 16, stack_to_local_promotion: true, + stack_guards: None, }; let m = compile_word("t", &[IrOp::PushI32(1)], &cfg).unwrap(); assert_eq!(m.fn_index, 7); diff --git a/crates/core/src/config.rs b/crates/core/src/config.rs index ac270ac..a80e71c 100644 --- a/crates/core/src/config.rs +++ b/crates/core/src/config.rs @@ -7,6 +7,11 @@ use crate::optimizer::OptConfig; pub struct CodegenOpts { /// Enable stack-to-local promotion for straight-line words. 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, } /// Master configuration for all WAFER optimizations. @@ -32,6 +37,7 @@ impl WaferConfig { }, codegen: CodegenOpts { stack_to_local_promotion: true, + stack_guards: true, }, } } @@ -49,6 +55,7 @@ impl WaferConfig { }, codegen: CodegenOpts { stack_to_local_promotion: false, + stack_guards: false, }, } } diff --git a/crates/core/src/consolidate.rs b/crates/core/src/consolidate.rs index 39477e5..9c340bb 100644 --- a/crates/core/src/consolidate.rs +++ b/crates/core/src/consolidate.rs @@ -21,7 +21,7 @@ mod tests { // Empty word list should produce nothing (but we guard against this at call site) let words = vec![]; let map = HashMap::new(); - let result = compile_consolidated_module(&words, &map, 16); + let result = compile_consolidated_module(&words, &map, 16, None); // Empty is valid -- should produce a valid module with no functions assert!(result.is_ok()); } @@ -31,7 +31,7 @@ mod tests { let words = vec![(WordId(1), vec![IrOp::PushI32(42)])]; let mut map = HashMap::new(); 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); assert!(result.is_ok()); } @@ -49,7 +49,7 @@ mod tests { map.insert(WordId(1), 1u32); map.insert(WordId(2), 2u32); map.insert(WordId(3), 3u32); - let result = compile_consolidated_module(&words, &map, 16); + let result = compile_consolidated_module(&words, &map, 16, None); assert!(result.is_ok()); } @@ -59,7 +59,7 @@ mod tests { let words = vec![(WordId(3), vec![IrOp::Call(WordId(99))])]; let mut map = HashMap::new(); map.insert(WordId(3), 1u32); - let result = compile_consolidated_module(&words, &map, 256); + let result = compile_consolidated_module(&words, &map, 256, None); assert!(result.is_ok()); } @@ -72,7 +72,7 @@ mod tests { let mut map = HashMap::new(); map.insert(WordId(1), 1u32); map.insert(WordId(2), 2u32); - let result = compile_consolidated_module(&words, &map, 16); + let result = compile_consolidated_module(&words, &map, 16, None); assert!(result.is_ok()); } @@ -95,7 +95,7 @@ mod tests { let mut map = HashMap::new(); map.insert(WordId(1), 1u32); map.insert(WordId(2), 2u32); - let result = compile_consolidated_module(&words, &map, 16); + let result = compile_consolidated_module(&words, &map, 16, None); assert!(result.is_ok()); } @@ -120,7 +120,7 @@ mod tests { let mut map = HashMap::new(); map.insert(WordId(1), 1u32); map.insert(WordId(2), 2u32); - let result = compile_consolidated_module(&words, &map, 16); + let result = compile_consolidated_module(&words, &map, 16, None); assert!(result.is_ok()); } @@ -141,7 +141,7 @@ mod tests { let mut map = HashMap::new(); map.insert(WordId(1), 1u32); map.insert(WordId(2), 2u32); - let result = compile_consolidated_module(&words, &map, 16); + let result = compile_consolidated_module(&words, &map, 16, None); assert!(result.is_ok()); } @@ -163,7 +163,7 @@ mod tests { let mut map = HashMap::new(); map.insert(WordId(1), 1u32); map.insert(WordId(2), 2u32); - let result = compile_consolidated_module(&words, &map, 16); + let result = compile_consolidated_module(&words, &map, 16, None); assert!(result.is_ok()); } } diff --git a/crates/core/src/dictionary.rs b/crates/core/src/dictionary.rs index eeaaf50..fea0054 100644 --- a/crates/core/src/dictionary.rs +++ b/crates/core/src/dictionary.rs @@ -18,6 +18,8 @@ pub mod flags { pub const IMMEDIATE: u8 = 0x80; /// Word is hidden (being compiled, not yet findable). pub const HIDDEN: u8 = 0x40; + /// Word is an implementation detail: findable, but skipped by WORDS. + pub const INTERNAL: u8 = 0x20; /// Mask for the name length (lower 5 bits). pub const LENGTH_MASK: u8 = 0x1F; /// Maximum word name length. @@ -95,11 +97,17 @@ impl Dictionary { // Write link field (points to previous LATEST) self.write_u32_unchecked(entry_start, self.latest); - // Write flags byte: HIDDEN | length, optionally IMMEDIATE + // Write flags byte: HIDDEN | length, optionally IMMEDIATE. + // Underscore-prefixed names are implementation details by repo + // convention (see tools/editor-support): flag them INTERNAL so + // WORDS and completion skip them while FIND still works. let mut flag_byte = flags::HIDDEN | (name_len as u8 & flags::LENGTH_MASK); if immediate { flag_byte |= flags::IMMEDIATE; } + if name_bytes.first() == Some(&b'_') { + flag_byte |= flags::INTERNAL; + } self.memory[(entry_start + 4) as usize] = flag_byte; // Write name bytes @@ -410,8 +418,21 @@ impl Dictionary { } /// Return names of all visible (non-hidden) words, newest first. - pub fn visible_words(&self) -> Vec { - let mut names = Vec::new(); + /// With `include_internal` false, words flagged INTERNAL are skipped. + pub fn visible_words(&self, include_internal: bool) -> Vec { + 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; while addr != 0 { let flags_byte = self.memory[(addr + 4) as usize]; @@ -420,7 +441,12 @@ impl Dictionary { let name_start = (addr + 5) as usize; let name = String::from_utf8_lossy(&self.memory[name_start..name_start + name_len]) .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); if link == addr { @@ -428,7 +454,7 @@ impl Dictionary { } addr = link; } - names + entries } /// Get a reference to the raw memory buffer. diff --git a/crates/core/src/error.rs b/crates/core/src/error.rs index 7af7187..3227a21 100644 --- a/crates/core/src/error.rs +++ b/crates/core/src/error.rs @@ -61,6 +61,13 @@ pub enum WaferError { #[error("{0}")] 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. diff --git a/crates/core/src/export.rs b/crates/core/src/export.rs index d18a28a..02988e4 100644 --- a/crates/core/src/export.rs +++ b/crates/core/src/export.rs @@ -120,8 +120,14 @@ pub fn export_module( metadata_json: metadata_json.as_bytes(), }; - let wasm_bytes = compile_exportable_module(&words, &local_fn_map, table_size, &export_sections) - .map_err(|e| anyhow::anyhow!("export codegen error: {e}"))?; + let wasm_bytes = compile_exportable_module( + &words, + &local_fn_map, + table_size, + &export_sections, + vm.stack_guard_param(), + ) + .map_err(|e| anyhow::anyhow!("export codegen error: {e}"))?; Ok((wasm_bytes, metadata)) } diff --git a/crates/core/src/ir.rs b/crates/core/src/ir.rs index 18130a8..47aed57 100644 --- a/crates/core/src/ir.rs +++ b/crates/core/src/ir.rs @@ -159,6 +159,8 @@ pub enum IrOp { Execute, /// Push the current data-stack pointer: ( -- addr ) SpFetch, + /// Push the current return-stack pointer: ( -- addr ) + RpFetch, // -- Float stack manipulation -- /// Float duplicate: ( F: r -- r r ) diff --git a/crates/core/src/lib.rs b/crates/core/src/lib.rs index 364055e..bbd2b90 100644 --- a/crates/core/src/lib.rs +++ b/crates/core/src/lib.rs @@ -24,6 +24,8 @@ pub mod ir; pub mod memory; pub mod optimizer; pub mod runtime; +pub mod see; +pub mod wordhelp; // Outer interpreter: runtime-agnostic, works with any Runtime impl #[allow(trivial_numeric_casts, clippy::unnecessary_cast)] diff --git a/crates/core/src/memory.rs b/crates/core/src/memory.rs index 1a36b1b..952bfa7 100644 --- a/crates/core/src/memory.rs +++ b/crates/core/src/memory.rs @@ -106,6 +106,8 @@ pub const SYSVAR_NUM_TIB: u32 = SYSVAR_BASE + 24; pub const SYSVAR_HLD: u32 = SYSVAR_BASE + 28; /// LEAVE flag: nonzero when LEAVE has been called inside a DO loop. 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; #[cfg(test)] mod tests { diff --git a/crates/core/src/outer.rs b/crates/core/src/outer.rs index b9cdb5d..9d40a00 100644 --- a/crates/core/src/outer.rs +++ b/crates/core/src/outer.rs @@ -24,8 +24,8 @@ use crate::ir::IrOp; use crate::memory::HASH_SCRATCH_BASE; use crate::memory::{ CELL_SIZE, DATA_STACK_TOP, FLOAT_SIZE, FLOAT_STACK_BASE, FLOAT_STACK_TOP, INPUT_BUFFER_BASE, - INPUT_BUFFER_SIZE, RETURN_STACK_TOP, SYSVAR_BASE_VAR, SYSVAR_HERE, SYSVAR_LEAVE_FLAG, - SYSVAR_NUM_TIB, SYSVAR_STATE, SYSVAR_TO_IN, + INPUT_BUFFER_SIZE, RETURN_STACK_TOP, SYSVAR_BASE_VAR, SYSVAR_FAULT_CODE, SYSVAR_HERE, + SYSVAR_LEAVE_FLAG, SYSVAR_NUM_TIB, SYSVAR_STATE, SYSVAR_TO_IN, }; use crate::optimizer::optimize; @@ -161,7 +161,117 @@ struct DoesDefinition { has_create: bool, } +/// Tokens handled directly by the outer interpreter (`interpret_token`, +/// `interpret_token_immediate`, `compile_token` hardcoded match arms). +/// Several have no dictionary entry at all; SEE/SEE-IR/HELP explain them +/// instead of erroring. Keep in sync with those match arms. +pub(crate) const INTERPRETER_TOKENS: &[&str] = &[ + // Definition structure + ":", + ":NONAME", + ";", + "[:", + ";]", + "[", + "]", + "{:", + // Conditional compilation + "[IF]", + "[ELSE]", + "[THEN]", + "[DEFINED]", + "[UNDEFINED]", + // Strings + comments + ".\"", + ".(", + "S\"", + "S\\\"", + "C\"", + "S", + "(", + "\\", + "ABORT\"", + // Defining words + "VARIABLE", + "CONSTANT", + "CREATE", + "VALUE", + "DOES>", + "2CONSTANT", + "2VARIABLE", + "2VALUE", + "FVARIABLE", + "FCONSTANT", + "FVALUE", + "BUFFER:", + "MARKER", + "REMEMBER", + "GILD", + "EMPTY", + "SYNONYM", + "CONSOLIDATE", + // Parsing words + "'", + "[']", + "CHAR", + "[CHAR]", + "EVALUATE", + "WORD", + "TO", + "IS", + "ACTION-OF", + "PARSE", + "PARSE-NAME", + "REFILL", + "ORDER", + // Compile-mode control flow + "IF", + "ELSE", + "THEN", + "DO", + "?DO", + "LOOP", + "+LOOP", + "BEGIN", + "UNTIL", + "AGAIN", + "WHILE", + "REPEAT", + "AHEAD", + "CASE", + "OF", + "ENDOF", + "ENDCASE", + "RECURSE", + "EXIT", + "LITERAL", + "2LITERAL", + "FLITERAL", + "SLITERAL", + "POSTPONE", +]; + +/// Source loader injected for INCLUDE/INCLUDED: resolved path -> text. +pub type SourceLoader = Box anyhow::Result + Send + Sync>; + +/// Append names space-separated, wrapped at 78 columns, ending with a +/// newline (also when the list is empty -- WORDS' historic shape). +fn push_wrapped(out: &mut String, names: &[&str]) { + let mut col = 0usize; + for name in names { + if col + name.len() + 1 > 78 && col > 0 { + out.push('\n'); + col = 0; + } + out.push_str(name); + out.push(' '); + col += name.len() + 1; + } + out.push('\n'); +} + /// Saved VM state for a MARKER word. +#[derive(Clone)] struct MarkerState { dict_state: DictionaryState, user_here: u32, @@ -170,8 +280,19 @@ struct MarkerState { ir_bodies: HashMap>, does_definitions: HashMap, host_word_names: HashMap, + word_sources: HashMap, two_value_words: std::collections::HashSet, fvalue_words: std::collections::HashSet, + // Namespace + text state: search order, wordlist allocation, + // REPLACES table, ABORT" texts + search_order: Vec, + next_wid: u32, + current_wid: u32, + substitutions: HashMap>, + abort_messages_len: usize, + // REMEMBER-style marker: state was saved just AFTER the marker word + // was defined, and the entry survives its own execution (re-runnable) + keep: bool, } // --------------------------------------------------------------------------- @@ -193,6 +314,20 @@ pub struct ForthVM { compiling_ir: Vec, control_stack: Vec, compiling_word_id: Option, + // SEE source capture: verbatim text of the colon definition in progress + // (accumulated across evaluate() calls), the position in the CURRENT + // input buffer where capture (re)starts, the byte offset of the most + // recently read token, and completed sources by word id. + compiling_source: String, + source_capture_from: Option, + last_token_start: usize, + word_sources: HashMap, + // INCLUDE machinery: injected source loader (CLI: filesystem; web: + // virtual or absent) and the stack of files being included -- + // (resolved path, current 1-based line) -- for cycle/depth checks, + // relative-path resolution, and file:line error context. + source_loader: Option, + include_frames: Vec<(String, usize)>, // Output buffer output: Arc>, // Next table index (mirrors dictionary.next_fn_index conceptually, @@ -219,7 +354,12 @@ pub struct ForthVM { // True when CREATE appeared in the current colon definition before DOES> saw_create_in_def: bool, // Pending action from compiled defining/parsing words - // 0 = none, 1 = CONSTANT, 2 = VARIABLE, 3 = CREATE, 4 = EVALUATE + // 0 = none, 1 = CONSTANT, 2 = VARIABLE, 3 = CREATE, 4 = EVALUATE, + // 5 = WORD, 6 = FIND, 7 = PARSE, 8 = PARSE-NAME, 9 = 2CONSTANT, + // 10 = 2VARIABLE, 11 = DEFER, 12 = IMMEDIATE, 20 = GET-CURRENT, + // 21 = SET-CURRENT, 25 = SEARCH-WORDLIST, 33 = DEFINITIONS, + // 40 = WORDS, 41 = SEE, 42 = SEE-IR, 43 = HELP, 44 = INCLUDED, + // 45 = INCLUDE pending_define: Arc>>, /// Pending actions from host functions (COMPILE,, CS-PICK, CS-ROLL, POSTPONE of control words). pending_actions: Arc>>, @@ -227,6 +367,12 @@ pub struct ForthVM { pending_does_patch: Arc>>, // Exception word set: throw code shared between CATCH and THROW host functions throw_code: Arc>>, + // ABORT" texts by compiled index, plus the not-yet-reported payload of + // the most recent ABORT" (printed only if the -2 throw goes uncaught) + abort_messages: Arc>>, + abort_message: Arc>>, + // Set by BYE: the embedding REPL/driver should exit + bye: Arc, // Shared dictionary lookup: maps uppercase name -> (WordId, is_immediate) word_lookup: Arc>>, // Set of word_ids that are 2VALUEs (need 2-cell TO semantics) @@ -251,6 +397,10 @@ pub struct ForthVM { recording_toplevel: bool, /// Saved states for MARKER words: `marker_id` -> `MarkerState` marker_states: HashMap, + /// EMPTY's rollback target: boot state, or wherever GILD re-baselined. + gild_state: Option>, + /// Table index of `_STACK_FAULT_` (compiled stack-guard target). + stack_fault_id: u32, /// Pending MARKER restore: after a marker word executes, restore this state pending_marker_restore: Arc>>, /// Conditional compilation skip depth: >0 means we're skipping tokens for [IF]/[ELSE] @@ -308,6 +458,69 @@ pub enum LocalKind { Float, } +/// Standard message for a Forth 2012 THROW code (subset wafer can raise). +fn throw_message(code: i32) -> Option<&'static str> { + Some(match code { + -1 => "ABORT", + -2 => "ABORT\"", + -3 => "Stack overflow", + -4 => "Stack underflow", + -5 => "Return stack overflow", + -6 => "Return stack underflow", + -8 => "Dictionary overflow", + -9 => "Invalid memory address", + -10 => "Division by zero", + -11 => "Result out of range", + -13 => "Undefined word", + -14 => "Interpreting a compile-only word", + -16 => "Attempt to use zero-length string as a name", + -18 => "Parsed string overflow", + -22 => "Control structure mismatch", + -24 => "Invalid numeric argument", + -28 => "User interrupt", + -31 => "Word not defined by CREATE", + -42 => "Floating-point divide by zero", + -43 => "Floating-point result out of range", + -44 => "Floating-point stack overflow", + -45 => "Floating-point stack underflow", + -56 => "QUIT", + _ => return None, + }) +} + +/// Format a cell as Forth `.` would: signed, in the given base (2..=36). +fn fmt_in_base(v: i32, base: u32) -> String { + let base = if (2..=36).contains(&base) { base } else { 10 }; + if base == 10 { + return v.to_string(); + } + let neg = v < 0; + let mut m = (v as i64).unsigned_abs(); + let mut digits = Vec::new(); + loop { + digits.push(char::from_digit((m % u64::from(base)) as u32, base).unwrap()); + m /= u64::from(base); + if m == 0 { + break; + } + } + if neg { + digits.push('-'); + } + digits.iter().rev().collect::().to_ascii_uppercase() +} + +/// Pop the data-stack top from host-function context. +fn host_pop(ctx: &mut dyn HostAccess) -> anyhow::Result { + let sp = ctx.get_dsp(); + if sp >= DATA_STACK_TOP { + anyhow::bail!("Stack underflow"); + } + let v = ctx.mem_read_i32(sp); + ctx.set_dsp(sp + CELL_SIZE); + Ok(v) +} + /// Advance past the next `\n` in `buf`, starting at `from`. Returns the /// byte index of the first character on the next line (or `buf.len()` if /// there's no more newline). Used by the `\` line-comment handler per @@ -356,6 +569,12 @@ impl ForthVM { compiling_ir: Vec::new(), control_stack: Vec::new(), compiling_word_id: None, + compiling_source: String::new(), + source_capture_from: None, + last_token_start: 0, + word_sources: HashMap::new(), + source_loader: None, + include_frames: Vec::new(), output, next_table_index: 0, host_word_names: HashMap::new(), @@ -372,6 +591,9 @@ impl ForthVM { pending_actions: Arc::new(Mutex::new(Vec::new())), pending_does_patch: Arc::new(Mutex::new(None)), throw_code: Arc::new(Mutex::new(None)), + abort_messages: Arc::new(Mutex::new(Vec::new())), + abort_message: Arc::new(Mutex::new(None)), + bye: Arc::new(std::sync::atomic::AtomicBool::new(false)), word_lookup: Arc::new(Mutex::new(HashMap::new())), two_value_words: std::collections::HashSet::new(), fvalue_words: std::collections::HashSet::new(), @@ -384,6 +606,8 @@ impl ForthVM { toplevel_ir: Vec::new(), recording_toplevel: false, marker_states: HashMap::new(), + gild_state: None, + stack_fault_id: 0, pending_marker_restore: Arc::new(Mutex::new(None)), conditional_skip_depth: 0, next_block_label: 0, @@ -417,6 +641,9 @@ impl ForthVM { vm.register_primitives()?; + // Boot state is the default EMPTY target (until GILD re-baselines) + vm.gild_state = Some(Box::new(vm.snapshot_marker_state(true))); + Ok(vm) } @@ -443,9 +670,14 @@ impl ForthVM { self.compiling_local_kinds.clear(); self.local_batch_base = None; self.compile_frames.clear(); - return Err(e); + self.compiling_source.clear(); + self.source_capture_from = None; + return Err(self.describe_uncaught(e)); } } + if self.bye.load(std::sync::atomic::Ordering::Relaxed) { + break; + } // Read >IN back from WASM memory. Only apply if Forth code changed it // (i.e., the WASM value differs from what sync_input_to_wasm wrote). // This distinguishes Forth's `>IN !` from Rust-side parse_until changes. @@ -459,6 +691,17 @@ impl ForthVM { } } + // Multi-line definition: bank this buffer's tail into the capture + // and continue from the start of the next buffer. + if self.state != 0 + && let Some(from) = self.source_capture_from + { + let from = from.min(self.input_buffer.len()); + self.compiling_source.push_str(&self.input_buffer[from..]); + self.compiling_source.push('\n'); + self.source_capture_from = Some(0); + } + Ok(()) } @@ -467,6 +710,35 @@ impl ForthVM { self.state != 0 } + /// True once BYE has executed; the embedding REPL/driver should exit. + pub fn bye_requested(&self) -> bool { + self.bye.load(std::sync::atomic::Ordering::Relaxed) + } + + /// Turn an internal error into the message shown to the user. + /// + /// An uncaught THROW leaves its code in `throw_code` (the "forth-throw" + /// sentinel error itself may get wrapped in a wasmtime trap when the + /// throw crosses compiled code, so the code — not the message — is the + /// reliable signal). Map the code to its standard message, or to the + /// recorded text for `ABORT"`. Always drains the payload so a stale + /// code can't leak into a later, unrelated error. + fn describe_uncaught(&mut self, e: anyhow::Error) -> anyhow::Error { + let code = self.throw_code.lock().unwrap().take(); + let text = self.abort_message.lock().unwrap().take(); + let (code, message) = match (code, text) { + (Some(-2), Some(t)) => (-2, t), + (Some(code), _) => match throw_message(code) { + Some(m) => (code, format!("{m} (throw {code})")), + None => (code, format!("Catch = {code}")), + }, + (None, _) => return e, + }; + // Typed carrier: display text unchanged, THROW code reachable via + // downcast_ref::() for CLI/web consumers. + anyhow::Error::new(crate::error::WaferError::UncaughtThrow { code, message }) + } + /// Get and clear the output buffer. pub fn take_output(&mut self) -> String { let mut out = self.output.lock().unwrap(); @@ -542,6 +814,12 @@ impl ForthVM { &self.host_word_names } + /// Names of all user-facing words (visible, non-internal), newest + /// first — the completion/browsing view of the dictionary. + pub fn word_names(&self) -> Vec { + self.dictionary.visible_words(false) + } + /// Resolve a word name to its `WordId`. Returns `None` if not found. pub fn resolve_word(&self, name: &str) -> Option { self.dictionary @@ -574,6 +852,7 @@ impl ForthVM { return None; } let start = self.input_pos; + self.last_token_start = start; while self.input_pos < bytes.len() && !bytes[self.input_pos].is_ascii_whitespace() { self.input_pos += 1; } @@ -826,7 +1105,18 @@ impl ForthVM { return Ok(()); } "BUFFER:" => return self.define_buffer(), - "MARKER" => return self.define_marker(), + "MARKER" => return self.define_marker(false), + "REMEMBER" => return self.define_marker(true), + "GILD" => { + self.gild_state = Some(Box::new(self.snapshot_marker_state(true))); + return Ok(()); + } + "EMPTY" => { + if let Some(state) = self.gild_state.clone() { + self.apply_marker_state(*state); + } + return Ok(()); + } "2CONSTANT" => return self.define_2constant(), "2VARIABLE" => return self.define_2variable(), "2VALUE" => return self.define_2value(), @@ -836,11 +1126,20 @@ impl ForthVM { "CONSOLIDATE" => return self.consolidate(), "SYNONYM" => return self.define_synonym(), "ORDER" => { + // wid 1 is FORTH-WORDLIST; other wids are anonymous. + let wid_name = |wid: u32| { + if wid == 1 { + "FORTH".to_string() + } else { + format!("wid#{wid}") + } + }; let so = self.search_order.lock().unwrap(); + let names: Vec = so.iter().map(|&w| wid_name(w)).collect(); let output = format!( - "Search order: {:?} Compilation: {}\n", - *so, - self.dictionary.current_wid() + "Search order: {} Compilation: {}\n", + names.join(" "), + wid_name(self.dictionary.current_wid()) ); self.output.lock().unwrap().push_str(&output); return Ok(()); @@ -991,22 +1290,18 @@ impl ForthVM { // Handle ABORT" in compile mode if token_upper == "ABORT\"" { if let Some(s) = self.parse_until('"') { - // Compile: IF TYPE ABORT THEN - // The flag is already on stack; compile the check - self.refresh_user_here(); - let addr = self.user_here; - let bytes = s.as_bytes(); - let len = bytes.len() as u32; - self.rt.mem_write_slice(addr as u32, bytes); - self.user_here += len; - self.sync_here_cell(); - - // ABORT" throws -2 without displaying the message. - // The message (addr, len) is saved but not typed here. - let throw_call = self.dictionary.find("THROW").map(|(_, id, _)| id); - let mut then_body = vec![IrOp::PushI32(-2)]; - if let Some(throw_id) = throw_call { - then_body.push(IrOp::Call(throw_id)); + // Record the text and compile: IF _ABORT_Q_ THEN. + // _ABORT_Q_ stashes the text as the pending abort payload + // and throws -2; the text is shown only if nothing CATCHes. + let idx = { + let mut msgs = self.abort_messages.lock().unwrap(); + msgs.push(s); + (msgs.len() - 1) as i32 + }; + let abort_q = self.dictionary.find("_ABORT_Q_").map(|(_, id, _)| id); + let mut then_body = vec![IrOp::PushI32(idx)]; + if let Some(id) = abort_q { + then_body.push(IrOp::Call(id)); } self.push_ir(IrOp::If { then_body, @@ -1959,6 +2254,10 @@ impl ForthVM { if self.state != 0 { anyhow::bail!("nested colon definitions not allowed"); } + // SEE source capture starts at the `:` token itself (its position + // was recorded by next_token before dispatch reached us). + self.compiling_source.clear(); + self.source_capture_from = Some(self.last_token_start); let name = self .next_token() .ok_or_else(|| anyhow::anyhow!("expected word name after :"))?; @@ -2153,17 +2452,23 @@ impl ForthVM { .compiling_word_id .take() .ok_or_else(|| anyhow::anyhow!("no word being compiled"))?; + // SEE: bank the tail of the current buffer through the `;` token. + // Capture is only armed by `:` — :NONAME and quotations never store. + if let Some(from) = self.source_capture_from.take() { + let end = self.input_pos.min(self.input_buffer.len()); + let mut src = std::mem::take(&mut self.compiling_source); + src.push_str(&self.input_buffer[from.min(end)..end]); + self.word_sources + .insert(word_id, src.trim_end().to_string()); + } + let ir = std::mem::take(&mut self.compiling_ir); let bodies = self.ir_bodies.clone(); let ir = self.optimize_ir(ir, &bodies); self.ir_bodies.insert(word_id, ir.clone()); // Compile to WASM - let config = CodegenConfig { - base_fn_index: word_id.0, - table_size: self.table_size(), - stack_to_local_promotion: self.config.codegen.stack_to_local_promotion, - }; + let config = self.codegen_config(word_id.0); let compiled = compile_word(&name, &ir, &config).map_err(|e| anyhow::anyhow!("codegen error: {e}"))?; @@ -2222,8 +2527,13 @@ impl ForthVM { let table_size = self.table_size(); // Compile the consolidated module - let module_bytes = compile_consolidated_module(&words, &local_fn_map, table_size) - .map_err(|e| anyhow::anyhow!("consolidation codegen error: {e}"))?; + let module_bytes = compile_consolidated_module( + &words, + &local_fn_map, + table_size, + self.stack_guard_param(), + ) + .map_err(|e| anyhow::anyhow!("consolidation codegen error: {e}"))?; // Instantiate: the element section in the module handles table placement // We use fn_index=0 since the element section has the correct offsets @@ -2247,8 +2557,13 @@ impl ForthVM { self.ensure_table_size(self.next_table_index)?; let table_size = self.table_size(); - let module_bytes = compile_consolidated_module(&words, &local_fn_map, table_size) - .map_err(|e| anyhow::anyhow!("batch compile error: {e}"))?; + let module_bytes = compile_consolidated_module( + &words, + &local_fn_map, + table_size, + self.stack_guard_param(), + ) + .map_err(|e| anyhow::anyhow!("batch compile error: {e}"))?; self.total_module_bytes += module_bytes.len() as u64; // Instantiate: the element section in the module handles table placement @@ -2505,6 +2820,24 @@ impl ForthVM { // Primitive registration // ----------------------------------------------------------------------- + /// `_STACK_FAULT_` table index when stack guards are enabled. + pub(crate) fn stack_guard_param(&self) -> Option { + self.config + .codegen + .stack_guards + .then_some(self.stack_fault_id) + } + + /// Codegen configuration for compiling one word. + fn codegen_config(&mut self, base_fn_index: u32) -> CodegenConfig { + CodegenConfig { + base_fn_index, + table_size: self.table_size(), + stack_to_local_promotion: self.config.codegen.stack_to_local_promotion, + stack_guards: self.stack_guard_param(), + } + } + /// Register a primitive word by compiling its IR body and installing it. fn register_primitive( &mut self, @@ -2527,11 +2860,7 @@ impl ForthVM { // Defer WASM compilation for batch processing self.deferred_ir.push((word_id, ir_body)); } else { - let config = CodegenConfig { - base_fn_index: word_id.0, - table_size: self.table_size(), - stack_to_local_promotion: self.config.codegen.stack_to_local_promotion, - }; + let config = self.codegen_config(word_id.0); let compiled = compile_word(name, &ir_body, &config) .map_err(|e| anyhow::anyhow!("codegen error for {name}: {e}"))?; self.instantiate_and_install(&compiled, word_id)?; @@ -2570,6 +2899,19 @@ impl ForthVM { fn register_primitives(&mut self) -> anyhow::Result<()> { self.batch_mode = true; + // _STACK_FAULT_ must exist before any guarded word is compiled: + // compiled guards write a throw code to SYSVAR_FAULT_CODE and + // call this word, which converts it into a Forth THROW. + let throw_code = Arc::clone(&self.throw_code); + let func: HostFn = Box::new(move |ctx: &mut dyn HostAccess| { + let code = ctx.mem_read_i32(SYSVAR_FAULT_CODE); + *throw_code.lock().unwrap() = Some(code); + Err(anyhow::anyhow!("forth-throw")) + }); + self.stack_fault_id = self + .register_host_primitive("_STACK_FAULT_", false, func)? + .0; + // -- Stack manipulation -- self.register_primitive("DUP", false, vec![IrOp::Dup])?; self.register_primitive("DROP", false, vec![IrOp::Drop])?; @@ -2691,6 +3033,8 @@ impl ForthVM { // -- Priority 6: System/compiler -- self.register_primitive("EXECUTE", false, vec![IrOp::Execute])?; self.register_primitive("SP@", false, vec![IrOp::SpFetch])?; + self.register_primitive("RP@", false, vec![IrOp::RpFetch])?; + // RDEPTH, .RS: defined in boot.fth (use RP@ IR op) self.register_immediate_word()?; self.register_decimal()?; self.register_hex()?; @@ -2700,6 +3044,7 @@ impl ForthVM { self.register_environment_q()?; // SOURCE: defined in boot.fth self.register_abort()?; + self.register_tools()?; // . (dot): defined in boot.fth self.register_dot_s()?; @@ -2886,12 +3231,14 @@ impl ForthVM { return Ok(()); } let depth = (DATA_STACK_TOP - sp) / CELL_SIZE; + let base = ctx.mem_read_i32(SYSVAR_BASE_VAR) as u32; out.push_str(&format!("<{depth}> ")); - // Print from bottom to top + // Print from bottom to top, in the current BASE let mut addr = DATA_STACK_TOP - CELL_SIZE; while addr >= sp { let v = ctx.mem_read_i32(addr as u32); - out.push_str(&format!("{v} ")); + out.push_str(&fmt_in_base(v, base)); + out.push(' '); if addr < CELL_SIZE { break; } @@ -2904,6 +3251,63 @@ impl ForthVM { Ok(()) } + /// Register interactive inspection tools: DUMP and F.S. + fn register_tools(&mut self) -> anyhow::Result<()> { + // DUMP ( addr u -- ) hex+ASCII dump, 16 bytes per line. + let output = Arc::clone(&self.output); + let func: HostFn = Box::new(move |ctx: &mut dyn HostAccess| { + const MAX: i32 = 4096; + let len = host_pop(ctx)?; + let addr = host_pop(ctx)? as u32; + let end = ctx.mem_len() as u32; + let clipped = (len.clamp(0, MAX) as u32).min(end.saturating_sub(addr.min(end))); + let bytes = ctx.mem_read_slice(addr, clipped as usize); + let mut out = output.lock().unwrap(); + for (i, row) in bytes.chunks(16).enumerate() { + let hex: Vec = row.iter().map(|b| format!("{b:02X}")).collect(); + let ascii: String = row + .iter() + .map(|&b| { + if (0x20..0x7F).contains(&b) { + b as char + } else { + '.' + } + }) + .collect(); + let row_addr = addr + (i as u32) * 16; + out.push_str(&format!( + "{row_addr:08X}: {:<47} |{ascii}|\n", + hex.join(" ") + )); + } + if len as u32 > clipped { + out.push_str("... (truncated)\n"); + } + Ok(()) + }); + self.register_host_primitive("DUMP", false, func)?; + + // F.S ( -- ) print the float stack without consuming. + let output = Arc::clone(&self.output); + let func: HostFn = Box::new(move |ctx: &mut dyn HostAccess| { + let sp = ctx.get_fsp(); + let depth = (FLOAT_STACK_TOP.saturating_sub(sp)) / FLOAT_SIZE; + let mut out = output.lock().unwrap(); + out.push_str(&format!("F:<{depth}> ")); + for i in (0..depth).rev() { + let bytes: [u8; 8] = ctx + .mem_read_slice(sp + i * FLOAT_SIZE, 8) + .try_into() + .unwrap(); + out.push_str(&format!("{} ", f64::from_le_bytes(bytes))); + } + Ok(()) + }); + self.register_host_primitive("F.S", false, func)?; + Ok(()) + } + // ----------------------------------------------------------------------- // Crypto: SHA1 / SHA256 / SHA512 (and any algos in `crypto::ALGOS`) // ----------------------------------------------------------------------- @@ -2993,11 +3397,9 @@ impl ForthVM { // Compile a tiny word that pushes the variable's address let ir_body = vec![IrOp::PushI32(var_addr as i32)]; self.ir_bodies.insert(word_id, ir_body.clone()); - let config = CodegenConfig { - base_fn_index: word_id.0, - table_size: self.table_size(), - stack_to_local_promotion: self.config.codegen.stack_to_local_promotion, - }; + self.word_sources + .insert(word_id, format!("VARIABLE {name}")); + let config = self.codegen_config(word_id.0); let compiled = compile_word(&name, &ir_body, &config) .map_err(|e| anyhow::anyhow!("codegen error for VARIABLE {name}: {e}"))?; @@ -3025,11 +3427,9 @@ impl ForthVM { // Compile a word that pushes the constant value let ir_body = vec![IrOp::PushI32(value)]; self.ir_bodies.insert(word_id, ir_body.clone()); - let config = CodegenConfig { - base_fn_index: word_id.0, - table_size: self.table_size(), - stack_to_local_promotion: self.config.codegen.stack_to_local_promotion, - }; + self.word_sources + .insert(word_id, format!("{value} CONSTANT {name}")); + let config = self.codegen_config(word_id.0); let compiled = compile_word(&name, &ir_body, &config) .map_err(|e| anyhow::anyhow!("codegen error for CONSTANT {name}: {e}"))?; @@ -3062,11 +3462,8 @@ impl ForthVM { // Compile a word that pushes the pfa let ir_body = vec![IrOp::PushI32(pfa as i32)]; self.ir_bodies.insert(word_id, ir_body.clone()); - let config = CodegenConfig { - base_fn_index: word_id.0, - table_size: self.table_size(), - stack_to_local_promotion: self.config.codegen.stack_to_local_promotion, - }; + self.word_sources.insert(word_id, format!("CREATE {name}")); + let config = self.codegen_config(word_id.0); let compiled = compile_word(&name, &ir_body, &config) .map_err(|e| anyhow::anyhow!("codegen error for CREATE {name}: {e}"))?; @@ -3108,11 +3505,7 @@ impl ForthVM { // Compile a word that fetches from the value's address let ir_body = vec![IrOp::PushI32(val_addr as i32), IrOp::Fetch]; self.ir_bodies.insert(word_id, ir_body.clone()); - let config = CodegenConfig { - base_fn_index: word_id.0, - table_size: self.table_size(), - stack_to_local_promotion: self.config.codegen.stack_to_local_promotion, - }; + let config = self.codegen_config(word_id.0); let compiled = compile_word(&name, &ir_body, &config) .map_err(|e| anyhow::anyhow!("codegen error for VALUE {name}: {e}"))?; @@ -3150,11 +3543,7 @@ impl ForthVM { // Compile a word that fetches the xt and executes it let ir_body = vec![IrOp::PushI32(defer_addr as i32), IrOp::Fetch, IrOp::Execute]; self.ir_bodies.insert(word_id, ir_body.clone()); - let config = CodegenConfig { - base_fn_index: word_id.0, - table_size: self.table_size(), - stack_to_local_promotion: self.config.codegen.stack_to_local_promotion, - }; + let config = self.codegen_config(word_id.0); let compiled = compile_word(&name, &ir_body, &config) .map_err(|e| anyhow::anyhow!("codegen error for DEFER {name}: {e}"))?; @@ -3187,11 +3576,9 @@ impl ForthVM { let ir_body = vec![IrOp::Call(word_id)]; self.ir_bodies.insert(new_word_id, ir_body.clone()); - let config = CodegenConfig { - base_fn_index: new_word_id.0, - table_size: self.table_size(), - stack_to_local_promotion: self.config.codegen.stack_to_local_promotion, - }; + self.word_sources + .insert(new_word_id, format!("SYNONYM {new_name} {old_name}")); + let config = self.codegen_config(new_word_id.0); let compiled = compile_word(&new_name, &ir_body, &config) .map_err(|e| anyhow::anyhow!("codegen error for SYNONYM: {e}"))?; self.instantiate_and_install(&compiled, new_word_id)?; @@ -3239,11 +3626,9 @@ impl ForthVM { // Compile a word that pushes the buffer address let ir_body = vec![IrOp::PushI32(buf_addr as i32)]; self.ir_bodies.insert(word_id, ir_body.clone()); - let config = CodegenConfig { - base_fn_index: word_id.0, - table_size: self.table_size(), - stack_to_local_promotion: self.config.codegen.stack_to_local_promotion, - }; + self.word_sources + .insert(word_id, format!("{size} BUFFER: {name}")); + let config = self.codegen_config(word_id.0); let compiled = compile_word(&name, &ir_body, &config) .map_err(|e| anyhow::anyhow!("codegen error for BUFFER: {name}: {e}"))?; @@ -3259,13 +3644,9 @@ impl ForthVM { /// MARKER -- create a marker that restores dictionary state. /// Saves a snapshot of the VM; when the marker word is executed, restores it. - fn define_marker(&mut self) -> anyhow::Result<()> { - let name = self - .next_token() - .ok_or_else(|| anyhow::anyhow!("MARKER: expected name"))?; - - // Save state BEFORE creating the marker word itself - let saved = MarkerState { + /// Snapshot everything a marker rollback restores. + fn snapshot_marker_state(&self, keep: bool) -> MarkerState { + MarkerState { dict_state: self.dictionary.save_state(), user_here: self.user_here, next_table_index: self.next_table_index, @@ -3273,18 +3654,62 @@ impl ForthVM { ir_bodies: self.ir_bodies.clone(), does_definitions: self.does_definitions.clone(), host_word_names: self.host_word_names.clone(), + word_sources: self.word_sources.clone(), two_value_words: self.two_value_words.clone(), fvalue_words: self.fvalue_words.clone(), - }; + search_order: self.search_order.lock().unwrap().clone(), + next_wid: *self.next_wid.lock().unwrap(), + current_wid: self.dictionary.current_wid(), + substitutions: self.substitutions.lock().unwrap().clone(), + abort_messages_len: self.abort_messages.lock().unwrap().len(), + keep, + } + } + + /// Roll the VM back to a marker snapshot. Also discards marker + /// entries created after the snapshot (their words no longer exist). + fn apply_marker_state(&mut self, state: MarkerState) { + self.dictionary.restore_state(state.dict_state); + self.dictionary.set_current_wid(state.current_wid); + self.user_here = state.user_here; + self.next_table_index = state.next_table_index; + self.word_pfa_map = state.word_pfa_map; + self.ir_bodies = state.ir_bodies; + self.does_definitions = state.does_definitions; + self.host_word_names = state.host_word_names; + self.word_sources = state.word_sources; + self.two_value_words = state.two_value_words; + self.fvalue_words = state.fvalue_words; + *self.search_order.lock().unwrap() = state.search_order; + *self.next_wid.lock().unwrap() = state.next_wid; + *self.substitutions.lock().unwrap() = state.substitutions; + self.abort_messages + .lock() + .unwrap() + .truncate(state.abort_messages_len); + self.marker_states + .retain(|&k, _| k < state.next_table_index); + self.sync_here_cell(); + self.rebuild_word_lookup(); + } + + /// MARKER (keep = false): rollback to just BEFORE the marker + /// was defined; the marker removes itself. + /// REMEMBER (keep = true): rollback to just AFTER the marker + /// was defined; the marker survives and can be re-run. + fn define_marker(&mut self, keep: bool) -> anyhow::Result<()> { + let name = self + .next_token() + .ok_or_else(|| anyhow::anyhow!("MARKER: expected name"))?; + + // MARKER saves state before its own word exists + let before = (!keep).then(|| self.snapshot_marker_state(false)); let word_id = self .dictionary .create(&name, false) .map_err(|e| anyhow::anyhow!("{e}"))?; - // Store the saved state keyed by word_id - self.marker_states.insert(word_id.0, saved); - // Compile the marker word: push marker_id, call _MARKER_RESTORE_ let restore_id = self .dictionary @@ -3293,11 +3718,7 @@ impl ForthVM { .ok_or_else(|| anyhow::anyhow!("_MARKER_RESTORE_ not found"))?; let ir_body = vec![IrOp::PushI32(word_id.0 as i32), IrOp::Call(restore_id)]; self.ir_bodies.insert(word_id, ir_body.clone()); - let config = CodegenConfig { - base_fn_index: word_id.0, - table_size: self.table_size(), - stack_to_local_promotion: self.config.codegen.stack_to_local_promotion, - }; + let config = self.codegen_config(word_id.0); let compiled = compile_word(&name, &ir_body, &config) .map_err(|e| anyhow::anyhow!("codegen error for MARKER {name}: {e}"))?; @@ -3305,6 +3726,10 @@ impl ForthVM { self.dictionary.reveal(); self.next_table_index = self.next_table_index.max(word_id.0 + 1); + // REMEMBER saves state after its own word is fully in place + let saved = before.unwrap_or_else(|| self.snapshot_marker_state(true)); + self.marker_states.insert(word_id.0, saved); + Ok(()) } @@ -3888,14 +4313,35 @@ impl ForthVM { /// ABORT -- clear stacks and throw error. fn register_abort(&mut self) -> anyhow::Result<()> { + let throw_code = Arc::clone(&self.throw_code); let func: HostFn = Box::new(move |ctx: &mut dyn HostAccess| { - // Reset stack pointers + // Reset stack pointers and throw -1 (ABORT) ctx.set_dsp((DATA_STACK_TOP as i32) as u32); ctx.set_rsp((RETURN_STACK_TOP as i32) as u32); - Err(anyhow::anyhow!("ABORT")) + *throw_code.lock().unwrap() = Some(-1); + Err(anyhow::anyhow!("forth-throw")) }); - self.register_host_primitive("ABORT", false, func)?; + + // _ABORT_Q_ ( idx -- ) runtime of ABORT": record text, throw -2. + let msgs = Arc::clone(&self.abort_messages); + let pending = Arc::clone(&self.abort_message); + let throw_code = Arc::clone(&self.throw_code); + let func: HostFn = Box::new(move |ctx: &mut dyn HostAccess| { + let idx = host_pop(ctx)? as usize; + *pending.lock().unwrap() = msgs.lock().unwrap().get(idx).cloned(); + *throw_code.lock().unwrap() = Some(-2); + Err(anyhow::anyhow!("forth-throw")) + }); + self.register_host_primitive("_ABORT_Q_", false, func)?; + + // BYE ( -- ) request REPL/driver exit. + let bye = Arc::clone(&self.bye); + let func: HostFn = Box::new(move |_ctx: &mut dyn HostAccess| { + bye.store(true, std::sync::atomic::Ordering::Relaxed); + Ok(()) + }); + self.register_host_primitive("BYE", false, func)?; Ok(()) } @@ -4024,13 +4470,7 @@ impl ForthVM { self.rt.mem_write_i32(crate::memory::SYSVAR_SOURCE_ID, -1); // Sync input buffer, >IN, and #TIB to WASM (for SOURCE and WORD) - { - let bytes = self.input_buffer.as_bytes(); - let len = bytes.len().min(INPUT_BUFFER_SIZE as usize); - self.rt.mem_write_slice(INPUT_BUFFER_BASE, &bytes[..len]); - self.rt.mem_write_i32(SYSVAR_TO_IN, 0); - self.rt.mem_write_i32(SYSVAR_NUM_TIB, len as i32); - } + self.sync_full_input_to_wasm(); // Interpret with >IN sync (supports >IN manipulation) while let Some(token) = self.next_token() { @@ -4049,22 +4489,135 @@ impl ForthVM { } } + // A definition left open by the EVALUATEd string: bank its tail and + // re-anchor capture at the resume point of the restored buffer. + let capture_open = self.state != 0 && self.source_capture_from.is_some(); + if let Some(from) = self.source_capture_from.filter(|_| self.state != 0) { + let from = from.min(self.input_buffer.len()); + self.compiling_source.push_str(&self.input_buffer[from..]); + self.compiling_source.push('\n'); + } + // Restore input state, SOURCE-ID, and sync back to WASM self.input_buffer = saved_buffer; self.input_pos = saved_pos; - { - let bytes = self.input_buffer.as_bytes(); - let len = bytes.len().min(INPUT_BUFFER_SIZE as usize); - self.rt.mem_write_slice(INPUT_BUFFER_BASE, &bytes[..len]); - self.rt.mem_write_i32(SYSVAR_TO_IN, self.input_pos as i32); - self.rt.mem_write_i32(SYSVAR_NUM_TIB, len as i32); - self.rt - .mem_write_i32(crate::memory::SYSVAR_SOURCE_ID, saved_source_id); + if capture_open { + self.source_capture_from = Some(self.input_pos); } + self.sync_full_input_to_wasm(); + self.rt + .mem_write_i32(crate::memory::SYSVAR_SOURCE_ID, saved_source_id); Ok(()) } + /// Write the current input buffer, >IN, and #TIB to WASM memory + /// (for SOURCE, WORD, and >IN manipulation from Forth code). + fn sync_full_input_to_wasm(&mut self) { + let bytes = self.input_buffer.as_bytes(); + let len = bytes.len().min(INPUT_BUFFER_SIZE as usize); + self.rt.mem_write_slice(INPUT_BUFFER_BASE, &bytes[..len]); + self.rt.mem_write_i32(SYSVAR_TO_IN, self.input_pos as i32); + self.rt.mem_write_i32(SYSVAR_NUM_TIB, len as i32); + } + + /// Register the loader that INCLUDE/INCLUDED use to read source files. + /// The CLI installs a filesystem reader; the web REPL leaves it unset, + /// which makes INCLUDE a defined error. + pub fn set_source_loader(&mut self, loader: SourceLoader) { + self.source_loader = Some(loader); + } + + /// Run a source file through the include machinery (also used by the + /// CLI file mode, so `wafer prog.fth` gets `file:line` error context and + /// a base directory for nested INCLUDEs). + pub fn include(&mut self, path: &str) -> anyhow::Result<()> { + self.include_file(path) + } + + /// INCLUDED's engine: resolve the path, load, and feed the file + /// line-by-line through `evaluate` with the parent input saved around + /// it. Compile state and SEE source capture already span `evaluate` + /// calls, so multi-line definitions inside files just work. + fn include_file(&mut self, path: &str) -> anyhow::Result<()> { + const MAX_INCLUDE_DEPTH: usize = 16; + // Relative paths resolve against the including file's directory. + let resolved = match self.include_frames.last() { + Some((parent, _)) if std::path::Path::new(path).is_relative() => { + match std::path::Path::new(parent).parent() { + Some(dir) if dir != std::path::Path::new("") => { + dir.join(path).to_string_lossy().into_owned() + } + _ => path.to_string(), + } + } + _ => path.to_string(), + }; + if self.include_frames.iter().any(|(p, _)| *p == resolved) { + anyhow::bail!("INCLUDE: cycle: {resolved}"); + } + if self.include_frames.len() >= MAX_INCLUDE_DEPTH { + anyhow::bail!("INCLUDE: nesting deeper than {MAX_INCLUDE_DEPTH}"); + } + let Some(loader) = self.source_loader.as_ref() else { + anyhow::bail!("INCLUDE: no source loader registered"); + }; + let text = loader(&resolved).map_err(|e| anyhow::anyhow!("INCLUDE: {resolved}: {e}"))?; + + // Save the parent input source; SOURCE-ID becomes a synthetic + // positive id per nesting level (0 = terminal, -1 = string). + let saved_buffer = std::mem::take(&mut self.input_buffer); + let saved_pos = self.input_pos; + let saved_source_id = self.rt.mem_read_i32(crate::memory::SYSVAR_SOURCE_ID); + self.rt.mem_write_i32( + crate::memory::SYSVAR_SOURCE_ID, + self.include_frames.len() as i32 + 1, + ); + self.include_frames.push((resolved, 0)); + + let mut result = Ok(()); + for (i, line) in text.lines().enumerate() { + if let Some(frame) = self.include_frames.last_mut() { + frame.1 = i + 1; + } + if let Err(e) = self.evaluate(line) { + let (p, n) = self.include_frames.last().cloned().unwrap_or_default(); + result = Err(e.context(format!("{p}:{n}"))); + break; + } + if self.bye.load(std::sync::atomic::Ordering::Relaxed) { + break; + } + } + + // Restore the parent input on every path (success, error, BYE). + self.include_frames.pop(); + self.input_buffer = saved_buffer; + self.input_pos = saved_pos; + // Re-anchor an open definition's capture at the parent resume point. + if self.state != 0 && self.source_capture_from.is_some() { + self.source_capture_from = Some(self.input_pos); + } + self.sync_full_input_to_wasm(); + self.rt + .mem_write_i32(crate::memory::SYSVAR_SOURCE_ID, saved_source_id); + result + } + + /// INCLUDED ( c-addr u -- ) -- include the named source file. + fn do_included(&mut self) -> anyhow::Result<()> { + let len = self.pop_data_stack()? as u32; + let addr = self.pop_data_stack()? as u32; + let path = String::from_utf8_lossy(&self.rt.mem_read_slice(addr, len as usize)).to_string(); + self.include_file(&path) + } + + /// INCLUDE -- parsing form of INCLUDED. + fn do_include(&mut self) -> anyhow::Result<()> { + let path = self.parse_name_arg("INCLUDE")?; + self.include_file(&path) + } + // ----------------------------------------------------------------------- // WORD -- parse delimited word from input // ----------------------------------------------------------------------- @@ -4155,11 +4708,7 @@ impl ForthVM { let word_id = WordId(fn_index); // Compile and replace - let config = CodegenConfig { - base_fn_index: word_id.0, - table_size: self.table_size(), - stack_to_local_promotion: self.config.codegen.stack_to_local_promotion, - }; + let config = self.codegen_config(word_id.0); let name = self .dictionary .word_name(latest) @@ -4249,11 +4798,7 @@ impl ForthVM { } let second_ir = std::mem::take(&mut self.compiling_ir); - let config = CodegenConfig { - base_fn_index: second_word_id.0, - table_size: self.table_size(), - stack_to_local_promotion: self.config.codegen.stack_to_local_promotion, - }; + let config = self.codegen_config(second_word_id.0); let compiled = compile_word("_does_action2_", &second_ir, &config) .map_err(|e| anyhow::anyhow!("codegen error for DOES> body 2: {e}"))?; self.instantiate_and_install(&compiled, second_word_id)?; @@ -4311,11 +4856,7 @@ impl ForthVM { } let does_ir = std::mem::take(&mut self.compiling_ir); - let config = CodegenConfig { - base_fn_index: does_word_id.0, - table_size: self.table_size(), - stack_to_local_promotion: self.config.codegen.stack_to_local_promotion, - }; + let config = self.codegen_config(does_word_id.0); let compiled = compile_word("_does_action_", &does_ir, &config) .map_err(|e| anyhow::anyhow!("codegen error for DOES> body: {e}"))?; self.instantiate_and_install(&compiled, does_word_id)?; @@ -4341,11 +4882,7 @@ impl ForthVM { // Compile the defining word as a no-op (the actual work is done // by the outer interpreter when it detects the does-definition). - let config = CodegenConfig { - base_fn_index: defining_word_id.0, - table_size: self.table_size(), - stack_to_local_promotion: self.config.codegen.stack_to_local_promotion, - }; + let config = self.codegen_config(defining_word_id.0); let compiled = compile_word(&defining_name, &[], &config) .map_err(|e| anyhow::anyhow!("codegen error for defining word: {e}"))?; self.instantiate_and_install(&compiled, defining_word_id)?; @@ -4402,11 +4939,7 @@ impl ForthVM { // Temporarily install a "push PFA" word (will be patched later) let ir_body = vec![IrOp::PushI32(pfa as i32)]; self.ir_bodies.insert(new_word_id, ir_body.clone()); - let config = CodegenConfig { - base_fn_index: new_word_id.0, - table_size: self.table_size(), - stack_to_local_promotion: self.config.codegen.stack_to_local_promotion, - }; + let config = self.codegen_config(new_word_id.0); let compiled = compile_word(&name, &ir_body, &config) .map_err(|e| anyhow::anyhow!("codegen: {e}"))?; self.instantiate_and_install(&compiled, new_word_id)?; @@ -4425,11 +4958,7 @@ impl ForthVM { let tmp_word_id = WordId(tmp_fn_idx); self.next_table_index = self.next_table_index.max(tmp_fn_idx + 1); - let config = CodegenConfig { - base_fn_index: tmp_word_id.0, - table_size: self.table_size(), - stack_to_local_promotion: self.config.codegen.stack_to_local_promotion, - }; + let config = self.codegen_config(tmp_word_id.0); let compiled = compile_word("_create_part_", &create_ir, &config) .map_err(|e| anyhow::anyhow!("codegen: {e}"))?; self.instantiate_and_install(&compiled, tmp_word_id)?; @@ -4438,11 +4967,7 @@ impl ForthVM { // Step 4: Patch the new word to push PFA and call does-action self.refresh_user_here(); let patched_ir = vec![IrOp::PushI32(pfa as i32), IrOp::Call(does_action_id)]; - let config = CodegenConfig { - base_fn_index: new_word_id.0, - table_size: self.table_size(), - stack_to_local_promotion: self.config.codegen.stack_to_local_promotion, - }; + let config = self.codegen_config(new_word_id.0); let compiled = compile_word(&name, &patched_ir, &config) .map_err(|e| anyhow::anyhow!("DOES> patch codegen: {e}"))?; self.instantiate_and_install(&compiled, new_word_id)?; @@ -4466,11 +4991,7 @@ impl ForthVM { .map_err(|e| anyhow::anyhow!("{e}"))?; let patched_ir = vec![IrOp::PushI32(pfa as i32), IrOp::Call(does_action_id)]; - let config = CodegenConfig { - base_fn_index: target_word_id.0, - table_size: self.table_size(), - stack_to_local_promotion: self.config.codegen.stack_to_local_promotion, - }; + let config = self.codegen_config(target_word_id.0); let compiled = compile_word(&name, &patched_ir, &config) .map_err(|e| anyhow::anyhow!("DOES> patch codegen: {e}"))?; self.instantiate_and_install(&compiled, target_word_id)?; @@ -5064,6 +5585,11 @@ impl ForthVM { } } 40 => self.do_words(), + 41 => self.do_see()?, + 42 => self.do_see_ir()?, + 43 => self.do_help()?, + 44 => self.do_included()?, + 45 => self.do_include()?, _ => {} } } @@ -5167,11 +5693,7 @@ impl ForthVM { .map_err(|e| anyhow::anyhow!("{e}"))?; let patched_ir = vec![IrOp::PushI32(pfa as i32), IrOp::Call(WordId(action_id))]; - let config = CodegenConfig { - base_fn_index: target_word_id.0, - table_size: self.table_size(), - stack_to_local_promotion: self.config.codegen.stack_to_local_promotion, - }; + let config = self.codegen_config(target_word_id.0); let compiled = compile_word(&name, &patched_ir, &config) .map_err(|e| anyhow::anyhow!("runtime DOES> patch codegen: {e}"))?; self.instantiate_and_install(&compiled, target_word_id)?; @@ -5187,22 +5709,18 @@ impl ForthVM { let mut p = self.pending_marker_restore.lock().unwrap(); p.take() }; - if let Some(id) = marker_id - && let Some(state) = self.marker_states.remove(&id) - { - self.dictionary.restore_state(state.dict_state); - self.user_here = state.user_here; - self.next_table_index = state.next_table_index; - self.word_pfa_map = state.word_pfa_map; - self.ir_bodies = state.ir_bodies; - self.does_definitions = state.does_definitions; - self.host_word_names = state.host_word_names; - self.two_value_words = state.two_value_words; - self.fvalue_words = state.fvalue_words; - self.sync_here_cell(); - self.rebuild_word_lookup(); - // Remove any marker states that were created after this one - self.marker_states.retain(|&k, _| k < id); + if let Some(id) = marker_id { + // REMEMBER-style entries survive their own execution; MARKER + // entries remove themselves. apply_marker_state discards all + // entries newer than the snapshot either way. + let state = match self.marker_states.get(&id) { + Some(s) if s.keep => Some(s.clone()), + Some(_) => self.marker_states.remove(&id), + None => None, + }; + if let Some(state) = state { + self.apply_marker_state(state); + } } Ok(()) } @@ -5784,14 +6302,253 @@ impl ForthVM { Ok(()) } - /// WORDS ( -- ) Print all visible dictionary words. + /// WORDS ( -- ) list visible words; `WORDS ` filters them + /// (case-insensitive). Internal (underscore-prefixed) words are + /// skipped. Output wraps at 78 columns and ends with a count. fn do_words(&mut self) { - let names = self.dictionary.visible_words(); - let mut out = self.output.lock().unwrap(); - for name in &names { - out.push_str(name); - out.push(' '); + // In interpret mode an optional token on the same line is a filter; + // the special filter ALL switches to the grouped full view. + let filter = if self.state == 0 { + self.next_token().map(|t| t.to_ascii_uppercase()) + } else { + None + }; + if filter.as_deref() == Some("ALL") { + self.do_words_all(); + return; } + let names = self.dictionary.visible_words(false); + let shown: Vec<&str> = names + .iter() + .filter(|n| { + filter + .as_ref() + .is_none_or(|f| n.to_ascii_uppercase().contains(f)) + }) + .map(String::as_str) + .collect(); + let mut out = self.output.lock().unwrap(); + push_wrapped(&mut out, &shown); + out.push_str(&format!("{} words\n", shown.len())); + } + + /// `WORDS ALL` -- grouped full view: one section per wordlist (search + /// order first, then any other populated wids), then internal words. + fn do_words_all(&mut self) { + let entries = self.dictionary.visible_entries(); + let mut wids: Vec = self.search_order.lock().unwrap().clone(); + for (_, wid, _) in &entries { + if !wids.contains(wid) { + wids.push(*wid); + } + } + let wid_name = |wid: u32| { + if wid == 1 { + "FORTH".to_string() + } else { + format!("wid#{wid}") + } + }; + let mut out = self.output.lock().unwrap(); + for wid in wids { + let names: Vec<&str> = entries + .iter() + .filter(|(_, w, internal)| *w == wid && !internal) + .map(|(n, _, _)| n.as_str()) + .collect(); + if names.is_empty() { + continue; + } + out.push_str(&format!("-- {} ({} words)\n", wid_name(wid), names.len())); + push_wrapped(&mut out, &names); + } + let internals: Vec<&str> = entries + .iter() + .filter(|(_, _, internal)| *internal) + .map(|(n, _, _)| n.as_str()) + .collect(); + if !internals.is_empty() { + out.push_str(&format!("-- internal ({} words)\n", internals.len())); + push_wrapped(&mut out, &internals); + } + } + + /// Map function-table index -> word name via a dictionary walk. + /// Newest-first, so redefinitions resolve to the visible name. + fn word_id_names(&self) -> HashMap { + let mut map = HashMap::new(); + let mut addr = self.dictionary.latest(); + while addr != 0 { + if let (Ok(name), Ok(code)) = ( + self.dictionary.word_name(addr), + self.dictionary.code_field(addr), + ) { + map.entry(code).or_insert(name); + } + let link = self.dictionary.read_link(addr); + if link == addr { + break; + } + addr = link; + } + map + } + + /// Parse the mandatory word-name argument of SEE/SEE-IR/HELP. + fn parse_name_arg(&mut self, who: &str) -> anyhow::Result { + self.next_token() + .ok_or_else(|| anyhow::anyhow!("{who}: expected word name")) + } + + /// `HELP [name]` — stack effect + description from the doc table; user + /// words echo their leading `( ... -- ... )` comment from the captured + /// source. Bare HELP prints usage. + fn do_help(&mut self) -> anyhow::Result<()> { + // Like WORDS' filter, the name is read from the same line (optional). + let name = if self.state == 0 { + self.next_token() + } else { + None + }; + let Some(name) = name else { + self.output.lock().unwrap().push_str( + "HELP -- stack effect + description. \ + Also try: WORDS, SEE , SEE-IR \n", + ); + return Ok(()); + }; + let upper = name.to_ascii_uppercase(); + let found = self.dictionary.find(&upper); + let mut line = if let Some((effect, desc)) = crate::wordhelp::lookup(&upper) { + format!("{upper} {effect} {desc}") + } else if let Some((_, word_id, _)) = found { + match self + .word_sources + .get(&word_id) + .and_then(|s| crate::wordhelp::stack_comment(s)) + { + Some(effect) => { + format!("{upper} {effect} user word; SEE {upper} shows the source") + } + None => format!("no help for {upper}; try SEE {upper}"), + } + } else if INTERPRETER_TOKENS.contains(&upper.as_str()) { + format!("{upper} is handled by the outer interpreter") + } else { + anyhow::bail!("HELP: unknown word: {name}"); + }; + if found.is_some_and(|(_, _, imm)| imm) { + line.push_str(" immediate"); + } + line.push('\n'); + self.output.lock().unwrap().push_str(&line); + Ok(()) + } + + /// `SEE name` — print captured source, a synthesized definition for + /// data words, or an IR/stub fallback. Never dead-ends on a defined word. + fn do_see(&mut self) -> anyhow::Result<()> { + let name = self.parse_name_arg("SEE")?; + let upper = name.to_ascii_uppercase(); + let Some((addr, word_id, is_immediate)) = self.dictionary.find(&upper) else { + if INTERPRETER_TOKENS.contains(&upper.as_str()) { + self.output.lock().unwrap().push_str(&format!( + "SEE: {upper} is handled by the outer interpreter (compiler word)\n" + )); + return Ok(()); + } + anyhow::bail!("SEE: unknown word: {name}"); + }; + let stored_name = self.dictionary.word_name(addr).unwrap_or(upper); + // Built-in words carry their HELP line as a leading comment. + let help = crate::wordhelp::lookup(&stored_name) + .map(|(effect, desc)| format!("\\ {stored_name} {effect} {desc}\n")) + .unwrap_or_default(); + let mut text = if let Some(src) = self.word_sources.get(&word_id) { + src.clone() + } else if let Some(synth) = self.synthesize_data_word(&stored_name, word_id) { + synth + } else if let Some(body) = self.ir_bodies.get(&word_id) { + let names = self.word_id_names(); + let ir = crate::see::format_ir_with(body, &|id| names.get(&id.0).cloned()); + format!("\\ {stored_name} is a primitive; IR:\n{}", ir.trim_end()) + } else if self.host_word_names.contains_key(&word_id) { + format!("\\ {stored_name} is a built-in host word") + } else { + format!("\\ {stored_name}: no source available") + }; + if is_immediate { + text.push_str("\nimmediate"); + } + text.push('\n'); + self.output.lock().unwrap().push_str(&(help + &text)); + Ok(()) + } + + /// Synthesize `SEE` output for mutable data words (VALUE family, DEFER) + /// whose current value lives in WASM memory. Gated on `word_pfa_map` so + /// address-pushing primitives (BASE, ...) never masquerade as data + /// words. A DOES>-product whose body happens to match a VALUE shape + /// prints as one — behaviorally equivalent, provenance lost. + fn synthesize_data_word(&mut self, name: &str, word_id: WordId) -> Option { + let &pfa = self.word_pfa_map.get(&word_id.0)?; + if self.two_value_words.contains(&word_id.0) { + let lo = self.rt.mem_read_i32(pfa); + let hi = self.rt.mem_read_i32(pfa + CELL_SIZE); + return Some(format!("{lo} {hi} 2VALUE {name}")); + } + if self.fvalue_words.contains(&word_id.0) { + let bytes: [u8; 8] = self.rt.mem_read_slice(pfa, 8).try_into().ok()?; + let r = f64::from_le_bytes(bytes); + return Some(format!("{r:e} FVALUE {name}")); + } + match self.ir_bodies.get(&word_id)?.as_slice() { + [IrOp::PushI32(addr), IrOp::Fetch] => { + let cur = self.rt.mem_read_i32(*addr as u32); + Some(format!("{cur} VALUE {name}")) + } + [IrOp::PushI32(addr), IrOp::Fetch, IrOp::Execute] => { + let xt = self.rt.mem_read_i32(*addr as u32) as u32; + Some(match self.word_id_names().get(&xt) { + Some(t) => format!("DEFER {name} ( IS {t} )"), + None => format!("DEFER {name}"), + }) + } + _ => None, + } + } + + /// `SEE-IR name` — print the stored post-optimization IR of a word. + fn do_see_ir(&mut self) -> anyhow::Result<()> { + let name = self.parse_name_arg("SEE-IR")?; + let upper = name.to_ascii_uppercase(); + let help = crate::wordhelp::lookup(&upper) + .map(|(effect, desc)| format!("\\ {upper} {effect} {desc}\n")) + .unwrap_or_default(); + let text = if let Some((_addr, word_id, is_immediate)) = self.dictionary.find(&upper) { + if let Some(body) = self.ir_bodies.get(&word_id) { + let mut header = format!("\\ {upper} -- {} ops (optimized IR)", body.len()); + if is_immediate { + header.push_str(" immediate"); + } + if self.does_definitions.contains_key(&word_id) { + header.push_str(" does>"); + } + header.push('\n'); + let names = self.word_id_names(); + header + &crate::see::format_ir_with(body, &|id| names.get(&id.0).cloned()) + } else if self.host_word_names.contains_key(&word_id) { + format!("SEE-IR: {upper} is a built-in host word\n") + } else { + format!("SEE-IR: {upper} has no IR body\n") + } + } else if INTERPRETER_TOKENS.contains(&upper.as_str()) { + format!("SEE-IR: {upper} is handled directly by the outer interpreter\n") + } else { + anyhow::bail!("SEE-IR: unknown word: {name}"); + }; + self.output.lock().unwrap().push_str(&(help + &text)); + Ok(()) } /// Register Search-Order word set words. @@ -6019,14 +6776,25 @@ impl ForthVM { Ok(()) } - /// Register WORDS for the Programming-Tools word set. + /// Register WORDS / SEE-IR for the Programming-Tools word set. + /// Each runs Rust-side via `pending_define` so it can parse arguments + /// with `next_token()` and write to `self.output`. fn register_words(&mut self) -> anyhow::Result<()> { - let pending = Arc::clone(&self.pending_define); - let func: HostFn = Box::new(move |_ctx: &mut dyn HostAccess| { - pending.lock().unwrap().push(40); // WORDS action - Ok(()) - }); - self.register_host_primitive("WORDS", false, func)?; + for (name, code) in [ + ("WORDS", 40), + ("SEE", 41), + ("SEE-IR", 42), + ("HELP", 43), + ("INCLUDED", 44), + ("INCLUDE", 45), + ] { + let pending = Arc::clone(&self.pending_define); + let func: HostFn = Box::new(move |_ctx: &mut dyn HostAccess| { + pending.lock().unwrap().push(code); + Ok(()) + }); + self.register_host_primitive(name, false, func)?; + } Ok(()) } @@ -6236,11 +7004,9 @@ impl ForthVM { let ir = vec![IrOp::PushI32(lo), IrOp::PushI32(hi)]; self.ir_bodies.insert(word_id, ir.clone()); - let config = CodegenConfig { - base_fn_index: word_id.0, - table_size: self.table_size(), - stack_to_local_promotion: self.config.codegen.stack_to_local_promotion, - }; + self.word_sources + .insert(word_id, format!("{lo} {hi} 2CONSTANT {name}")); + let config = self.codegen_config(word_id.0); let compiled = compile_word(&name, &ir, &config) .map_err(|e| anyhow::anyhow!("2CONSTANT codegen: {e}"))?; self.instantiate_and_install(&compiled, word_id)?; @@ -6266,11 +7032,9 @@ impl ForthVM { let ir = vec![IrOp::PushI32(addr as i32)]; self.ir_bodies.insert(word_id, ir.clone()); - let config = CodegenConfig { - base_fn_index: word_id.0, - table_size: self.table_size(), - stack_to_local_promotion: self.config.codegen.stack_to_local_promotion, - }; + self.word_sources + .insert(word_id, format!("2VARIABLE {name}")); + let config = self.codegen_config(word_id.0); let compiled = compile_word(&name, &ir, &config) .map_err(|e| anyhow::anyhow!("2VARIABLE codegen: {e}"))?; self.instantiate_and_install(&compiled, word_id)?; @@ -6309,11 +7073,7 @@ impl ForthVM { IrOp::Fetch, ]; self.ir_bodies.insert(word_id, ir.clone()); - let config = CodegenConfig { - base_fn_index: word_id.0, - table_size: self.table_size(), - stack_to_local_promotion: self.config.codegen.stack_to_local_promotion, - }; + let config = self.codegen_config(word_id.0); let compiled = compile_word(&name, &ir, &config) .map_err(|e| anyhow::anyhow!("2VALUE codegen: {e}"))?; self.instantiate_and_install(&compiled, word_id)?; @@ -7063,11 +7823,9 @@ impl ForthVM { // Compile a word that pushes the address onto the DATA stack let ir_body = vec![IrOp::PushI32(addr as i32)]; self.ir_bodies.insert(word_id, ir_body.clone()); - let config = CodegenConfig { - base_fn_index: word_id.0, - table_size: self.table_size(), - stack_to_local_promotion: self.config.codegen.stack_to_local_promotion, - }; + self.word_sources + .insert(word_id, format!("FVARIABLE {name}")); + let config = self.codegen_config(word_id.0); let compiled = compile_word(&name, &ir_body, &config) .map_err(|e| anyhow::anyhow!("codegen error for FVARIABLE {name}: {e}"))?; @@ -7107,6 +7865,8 @@ impl ForthVM { self.rt.ensure_table_size(word_id.0)?; self.rt.register_host_func(word_id.0, func)?; self.dictionary.reveal(); + self.word_sources + .insert(word_id, format!("{val:e} FCONSTANT {name}")); self.sync_word_lookup(&name, word_id, false); self.next_table_index = self.next_table_index.max(word_id.0 + 1); @@ -8753,6 +9513,682 @@ mod tests { assert!(output.contains("MYTEST")); } + #[test] + fn test_words_filter_and_count() { + let output = eval_output("WORDS FDEPTH"); + assert!(output.contains("FDEPTH")); + assert!(!output.contains("SWAP")); + assert!(output.contains(" words\n")); + } + + #[test] + fn test_words_all_grouped() { + let output = eval_output("WORDS ALL"); + assert!(output.contains("-- FORTH ("), "{output}"); + assert!(output.contains("-- internal ("), "{output}"); + // Internals are visible in the ALL view. + assert!(output.contains("_STACK_FAULT_"), "{output}"); + } + + #[test] + fn test_words_all_groups_other_wordlists() { + let output = + eval_output("WORDLIST SET-CURRENT : INSIDE 1 ; FORTH-WORDLIST SET-CURRENT WORDS ALL"); + assert!(output.contains("-- wid#2 (1 words)"), "{output}"); + let wid2_section = output.split("-- wid#2").nth(1).unwrap(); + assert!( + wid2_section.lines().nth(1).unwrap().contains("INSIDE"), + "{output}" + ); + } + + #[test] + fn test_words_hides_internal() { + let output = eval_output("WORDS"); + assert!(!output.contains("_ABORT_Q_")); + assert!(!output.contains("__CTRL__")); + } + + // -- Error reporting (WS-008) -- + + #[test] + fn test_uncaught_throw_is_typed() { + let mut vm = ForthVM::::new().unwrap(); + let err = vm.evaluate("-4 THROW").unwrap_err(); + assert_eq!(err.to_string(), "Stack underflow (throw -4)"); + match err.downcast_ref::() { + Some(crate::error::WaferError::UncaughtThrow { code, .. }) => assert_eq!(*code, -4), + other => panic!("expected UncaughtThrow, got {other:?}"), + } + } + + #[test] + fn test_uncaught_abort_quote_is_typed() { + let mut vm = ForthVM::::new().unwrap(); + let err = vm.evaluate(": F ABORT\" bad input\" ; -1 F").unwrap_err(); + assert_eq!(err.to_string(), "bad input"); + match err.downcast_ref::() { + Some(crate::error::WaferError::UncaughtThrow { code, .. }) => assert_eq!(*code, -2), + other => panic!("expected UncaughtThrow, got {other:?}"), + } + } + + #[test] + fn test_trap_names_faulting_word() { + let mut vm = ForthVM::::new().unwrap(); + // Out-of-bounds fetch traps inside the compiled word; the name + // section + backtrace naming must identify CRASHER. + vm.evaluate(": CRASHER 999999999 @ ;").unwrap(); + let err = vm.evaluate("CRASHER").unwrap_err(); + let msg = format!("{err:#}"); + assert!(msg.contains("CRASHER"), "{msg}"); + assert!(msg.contains("out of bounds"), "{msg}"); + } + + // -- INCLUDE / INCLUDED -- + + /// VM with a virtual source loader over the given (path, text) pairs. + fn vm_with_files(files: &[(&str, &str)]) -> ForthVM { + let mut vm = ForthVM::::new().unwrap(); + let map: HashMap = files + .iter() + .map(|(p, t)| (p.to_string(), t.to_string())) + .collect(); + vm.set_source_loader(Box::new(move |p| { + map.get(p) + .cloned() + .ok_or_else(|| anyhow::anyhow!("file not found")) + })); + vm + } + + #[test] + fn test_include_defines_words_and_resumes_parent_line() { + let mut vm = vm_with_files(&[("lib.fth", ": DOUBLE 2 * ;\n: TRIPLE 3 * ;\n")]); + // The 5 after INCLUDE proves the parent input buffer resumes. + vm.evaluate("INCLUDE lib.fth 5 DOUBLE .").unwrap(); + assert_eq!(vm.take_output(), "10 "); + } + + #[test] + fn test_included_string_form() { + let mut vm = vm_with_files(&[("lib.fth", ": Q 7 ;")]); + vm.evaluate("S\" lib.fth\" INCLUDED Q .").unwrap(); + assert_eq!(vm.take_output(), "7 "); + } + + #[test] + fn test_include_multiline_definition_and_see() { + let mut vm = vm_with_files(&[("lib.fth", ": TRI\n DUP DUP ;\n")]); + vm.evaluate("INCLUDE lib.fth SEE TRI").unwrap(); + assert_eq!(vm.take_output(), ": TRI\n DUP DUP ;\n"); + } + + #[test] + fn test_include_nested_relative_path() { + let mut vm = vm_with_files(&[ + ("dir/a.fth", "INCLUDE b.fth : A B 1 + ;"), + ("dir/b.fth", ": B 41 ;"), + ]); + vm.evaluate("INCLUDE dir/a.fth A .").unwrap(); + assert_eq!(vm.take_output(), "42 "); + } + + #[test] + fn test_include_cycle_detected() { + let mut vm = vm_with_files(&[("a.fth", "INCLUDE b.fth"), ("b.fth", "INCLUDE a.fth")]); + let err = vm.evaluate("INCLUDE a.fth").unwrap_err(); + assert!(format!("{err:#}").contains("cycle"), "{err:#}"); + } + + #[test] + fn test_include_depth_bounded() { + let files: Vec<(String, String)> = (0..20) + .map(|i| (format!("f{i}.fth"), format!("INCLUDE f{}.fth", i + 1))) + .collect(); + let refs: Vec<(&str, &str)> = files + .iter() + .map(|(p, t)| (p.as_str(), t.as_str())) + .collect(); + let mut vm = vm_with_files(&refs); + let err = vm.evaluate("INCLUDE f0.fth").unwrap_err(); + assert!(format!("{err:#}").contains("nesting deeper"), "{err:#}"); + } + + #[test] + fn test_include_missing_file_and_no_loader() { + let mut vm = vm_with_files(&[]); + let err = vm.evaluate("INCLUDE nosuch.fth").unwrap_err(); + assert!( + format!("{err:#}").contains("INCLUDE: nosuch.fth"), + "{err:#}" + ); + let mut vm = ForthVM::::new().unwrap(); + let err = vm.evaluate("INCLUDE x.fth").unwrap_err(); + assert!(err.to_string().contains("no source loader"), "{err}"); + } + + #[test] + fn test_include_error_carries_file_and_line() { + let mut vm = vm_with_files(&[("lib.fth", "1 2 +\nNOSUCHWORD\n3 4 +")]); + let err = vm.evaluate("INCLUDE lib.fth").unwrap_err(); + let msg = format!("{err:#}"); + assert!(msg.contains("lib.fth:2"), "{msg}"); + assert!(msg.contains("NOSUCHWORD"), "{msg}"); + // Parent VM stays usable, compile state clean. + vm.evaluate(": OK 1 ; OK .").unwrap(); + assert_eq!(vm.take_output(), "1 "); + } + + #[test] + fn test_include_nested_error_context_chains() { + let mut vm = vm_with_files(&[("outer.fth", "INCLUDE inner.fth"), ("inner.fth", "BOOM")]); + let err = vm.evaluate("INCLUDE outer.fth").unwrap_err(); + let msg = format!("{err:#}"); + assert!(msg.contains("outer.fth:1"), "{msg}"); + assert!(msg.contains("inner.fth:1"), "{msg}"); + } + + #[test] + fn test_include_throw_propagates_vm_usable() { + let mut vm = vm_with_files(&[("t.fth", ": BAD -4 THROW ;\nBAD")]); + let err = vm.evaluate("INCLUDE t.fth").unwrap_err(); + assert!(format!("{err:#}").contains("Stack underflow"), "{err:#}"); + vm.evaluate("6 7 * .").unwrap(); + assert_eq!(vm.take_output(), "42 "); + } + + #[test] + fn test_include_remember_reload_loop() { + let mut vm = vm_with_files(&[("app.fth", "REMEMBER -WORK\n: APP 1 ;")]); + vm.evaluate("INCLUDE app.fth").unwrap(); + vm.evaluate("-WORK INCLUDE app.fth APP .").unwrap(); + assert_eq!(vm.take_output(), "1 "); + // Only one APP in the dictionary after the reload. + let names = vm.word_names(); + assert_eq!(names.iter().filter(|n| *n == "APP").count(), 1); + } + + #[test] + fn test_include_bye_stops_file() { + let mut vm = vm_with_files(&[("b.fth", "1 .\nBYE\n2 .")]); + vm.evaluate("INCLUDE b.fth").unwrap(); + assert_eq!(vm.take_output(), "1 "); + assert!(vm.bye_requested()); + } + + #[test] + fn test_include_source_id_nested_and_restored() { + let mut vm = vm_with_files(&[ + ("a.fth", "SOURCE-ID N1 ! INCLUDE b.fth"), + ("b.fth", "SOURCE-ID N2 !"), + ]); + vm.evaluate("VARIABLE N1 VARIABLE N2").unwrap(); + vm.evaluate("INCLUDE a.fth N1 @ . N2 @ . SOURCE-ID .") + .unwrap(); + assert_eq!(vm.take_output(), "1 2 0 "); + } + + // -- HELP -- + + #[test] + fn test_help_documented_word() { + let output = eval_output("HELP DUP"); + assert_eq!( + output, + "DUP ( x -- x x ) Duplicate the top of the data stack.\n" + ); + // Case-insensitive. + assert_eq!(eval_output("HELP dup"), output); + } + + #[test] + fn test_help_bare_prints_usage() { + let output = eval_output("HELP"); + assert!(output.contains("HELP "), "{output}"); + assert!(output.contains("SEE "), "{output}"); + } + + #[test] + fn test_help_user_word_echoes_stack_comment() { + let output = eval_output(": SQ ( n -- n^2 ) DUP * ; HELP SQ"); + assert!(output.contains("SQ ( n -- n^2 )"), "{output}"); + assert!(output.contains("SEE SQ"), "{output}"); + } + + #[test] + fn test_help_undocumented_user_word_hints_see() { + let output = eval_output(": MYW 1 ; HELP MYW"); + assert_eq!(output, "no help for MYW; try SEE MYW\n"); + } + + #[test] + fn test_help_immediate_marker() { + let output = eval_output(": IMH 1 ; IMMEDIATE HELP IMH"); + assert!(output.contains("immediate"), "{output}"); + } + + #[test] + fn test_help_unknown_word_errors() { + let mut vm = ForthVM::::new().unwrap(); + let err = vm.evaluate("HELP NOSUCHWORD").unwrap_err(); + assert!(err.to_string().contains("HELP: unknown word: NOSUCHWORD")); + } + + #[test] + fn test_help_covers_every_word_in_fresh_vm() { + // Total-coverage gate: every visible dictionary word and every + // outer-interpreter token must have a WORD_DOCS entry, and every + // entry must resolve back to a real word or token. + let vm = ForthVM::::new().unwrap(); + let mut missing: Vec = Vec::new(); + let mut names = vm.word_names(); + names.extend(INTERPRETER_TOKENS.iter().map(ToString::to_string)); + for name in &names { + if crate::wordhelp::lookup(name).is_none() { + missing.push(name.clone()); + } + } + missing.sort(); + missing.dedup(); + assert!(missing.is_empty(), "words without HELP docs: {missing:?}"); + + for (name, effect, desc) in crate::wordhelp::WORD_DOCS { + let known = vm.dictionary.find(&name.to_ascii_uppercase()).is_some() + || INTERPRETER_TOKENS + .iter() + .any(|t| t.eq_ignore_ascii_case(name)); + // SHA words vanish without the crypto feature; keep their docs. + let feature_gated = !cfg!(feature = "crypto") && name.starts_with("SHA"); + assert!( + known || feature_gated, + "WORD_DOCS entry for nonexistent word: {name}" + ); + assert!(!desc.is_empty(), "empty description for {name}"); + let e = *effect; + assert!( + e.starts_with('(') && e.ends_with(')'), + "malformed stack effect for {name}: {e:?}" + ); + } + } + + // -- SEE -- + + #[test] + fn test_see_colon_word_verbatim() { + let output = eval_output(": SQ DUP * ; SEE SQ"); + assert_eq!(output, ": SQ DUP * ;\n"); + } + + #[test] + fn test_see_multiline_definition() { + let mut vm = ForthVM::::new().unwrap(); + vm.evaluate(": TRI").unwrap(); + vm.evaluate(" DUP DUP ;").unwrap(); + vm.evaluate("SEE TRI").unwrap(); + assert_eq!(vm.take_output(), ": TRI\n DUP DUP ;\n"); + } + + #[test] + fn test_see_comment_survives() { + let output = eval_output(": C ( n -- n ) 1+ ; SEE C"); + assert!(output.contains("( n -- n )"), "{output}"); + } + + #[test] + fn test_see_data_words() { + assert_eq!(eval_output("42 CONSTANT A SEE A"), "42 CONSTANT A\n"); + assert_eq!(eval_output("VARIABLE V SEE V"), "VARIABLE V\n"); + assert_eq!(eval_output("CREATE CR8 SEE CR8"), "CREATE CR8\n"); + assert_eq!(eval_output("16 BUFFER: B SEE B"), "16 BUFFER: B\n"); + assert_eq!(eval_output("1 2 2CONSTANT D2 SEE D2"), "1 2 2CONSTANT D2\n"); + assert_eq!( + eval_output("SYNONYM NEWDUP DUP SEE NEWDUP"), + "SYNONYM NEWDUP DUP\n" + ); + } + + #[test] + fn test_see_value_shows_current() { + assert_eq!(eval_output("5 VALUE X SEE X"), "5 VALUE X\n"); + assert_eq!(eval_output("5 VALUE X 9 TO X SEE X"), "9 VALUE X\n"); + } + + #[test] + fn test_see_defer_shows_target() { + let output = eval_output("DEFER D ' DUP IS D SEE D"); + assert_eq!(output, "DEFER D ( IS DUP )\n"); + } + + #[test] + fn test_see_boot_word_shows_source() { + // WITHIN is defined in boot.fth as a colon word; SEE must show + // real source (with its HELP header line), not an IR dump. + let output = eval_output("SEE WITHIN"); + assert!(output.starts_with("\\ WITHIN ("), "{output}"); + assert!( + output.ends_with(": WITHIN OVER - >R - R> U< ;\n"), + "{output}" + ); + } + + #[test] + fn test_see_primitive_ir_fallback() { + let output = eval_output("SEE DUP"); + assert!(output.contains("DUP is a primitive; IR:"), "{output}"); + assert!(output.contains("dup"), "{output}"); + } + + #[test] + fn test_see_host_word_and_interpreter_token() { + let output = eval_output("SEE WORDS"); + assert!(output.contains("WORDS is a built-in host word"), "{output}"); + // `:` has no dictionary entry — outer-interpreter stub. + let output = eval_output("SEE :"); + assert!( + output.contains(": is handled by the outer interpreter"), + "{output}" + ); + } + + #[test] + fn test_see_immediate_flag() { + let output = eval_output(": I2 ; IMMEDIATE SEE I2"); + assert!(output.contains(": I2 ;\nimmediate"), "{output}"); + } + + #[test] + fn test_see_unknown_word_errors() { + let mut vm = ForthVM::::new().unwrap(); + let err = vm.evaluate("SEE NOSUCHWORD").unwrap_err(); + assert!(err.to_string().contains("SEE: unknown word: NOSUCHWORD")); + } + + #[test] + fn test_see_error_path_no_capture_debris() { + let mut vm = ForthVM::::new().unwrap(); + // Force an unknown-word error mid-definition, then define fresh. + assert!(vm.evaluate(": BAD NOSUCHWORD ;").is_err()); + vm.evaluate(": GOOD 1 ; SEE GOOD").unwrap(); + assert_eq!(vm.take_output(), ": GOOD 1 ;\n"); + } + + #[test] + fn test_see_marker_roundtrip_restores_source() { + let mut vm = ForthVM::::new().unwrap(); + vm.evaluate(": W 1 ; MARKER MK : W 2 ;").unwrap(); + vm.evaluate("SEE W").unwrap(); + assert_eq!(vm.take_output(), ": W 2 ;\n"); + vm.evaluate("MK SEE W").unwrap(); + assert_eq!(vm.take_output(), ": W 1 ;\n"); + } + + #[test] + fn test_see_redefinition_shows_newest() { + let output = eval_output(": R 1 ; : R 2 ; SEE R"); + assert_eq!(output, ": R 2 ;\n"); + } + + // -- SEE-IR -- + + #[test] + fn test_see_ir_colon_word() { + let output = eval_output(": SQ DUP * ; SEE-IR SQ"); + assert!(output.contains("\\ SQ -- 2 ops (optimized IR)"), "{output}"); + assert!(output.contains("dup")); + assert!(output.contains("mul")); + } + + #[test] + fn test_see_ir_shows_inlined_body() { + let output = eval_output(": SQ DUP * ; : FOO SQ SQ ; SEE-IR FOO"); + // Inlining threshold covers SQ: FOO's stored IR has both muls inlined. + assert_eq!(output.matches("mul").count(), 2, "{output}"); + assert!(!output.contains("call"), "{output}"); + } + + #[test] + fn test_see_ir_resolves_callee_names() { + // A body over the inlining threshold keeps its calls. + let output = eval_output( + ": BIG DUP DUP DUP DUP DUP DUP DUP DUP DUP * * * * * * * * * ; \ + : USER BIG BIG ; SEE-IR USER", + ); + assert!( + output.contains("call BIG") || output.contains("tail-call BIG"), + "{output}" + ); + } + + #[test] + fn test_see_ir_primitive_and_host_word() { + let output = eval_output("SEE-IR DUP"); + assert!(output.contains("(optimized IR)"), "{output}"); + assert!(output.contains("dup")); + let output = eval_output("SEE-IR WORDS"); + assert!(output.contains("WORDS is a built-in host word"), "{output}"); + } + + #[test] + fn test_see_ir_control_flow_indented() { + let output = eval_output(": T IF 1 ELSE 2 THEN ; SEE-IR T"); + assert!( + output.contains("if\n push 1\nelse\n push 2\nthen\n"), + "{output}" + ); + } + + #[test] + fn test_see_ir_immediate_flag() { + let output = eval_output(": IMM 1 ; IMMEDIATE SEE-IR IMM"); + assert!(output.contains("immediate"), "{output}"); + } + + #[test] + fn test_see_ir_interpreter_token() { + let output = eval_output("SEE-IR :"); + assert!( + output.contains(": is handled directly by the outer interpreter"), + "{output}" + ); + } + + #[test] + fn test_see_ir_errors() { + let mut vm = ForthVM::::new().unwrap(); + let err = vm.evaluate("SEE-IR NOSUCHWORD").unwrap_err(); + assert!(err.to_string().contains("SEE-IR: unknown word: NOSUCHWORD")); + let mut vm = ForthVM::::new().unwrap(); + let err = vm.evaluate("SEE-IR").unwrap_err(); + assert!(err.to_string().contains("SEE-IR: expected word name")); + } + + #[test] + fn test_dot_s_honors_base() { + assert_eq!(eval_output("HEX FF .S"), "<1> FF "); + assert_eq!(eval_output("HEX -A .S"), "<1> -A "); + assert_eq!(eval_output("5 2 BASE ! .S"), "<1> 101 "); + } + + #[test] + fn test_question_fetches_and_prints() { + assert_eq!(eval_output("VARIABLE QV 17 QV ! QV ?"), "17 "); + } + + #[test] + fn test_dump_smoke() { + let output = eval_output("VARIABLE DV 65 DV C! DV 4 DUMP"); + assert!(output.contains("41")); + assert!(output.contains("|A")); + } + + #[test] + fn test_f_dot_s() { + let output = eval_output("1.5E0 2.5E0 F.S"); + assert!(output.starts_with("F:<2> 1.5 2.5")); + } + + #[test] + fn test_spaces_negative_prints_nothing() { + assert_eq!(eval_output("-1 SPACES"), ""); + // width smaller than the number: no padding at all + assert_eq!(eval_output("123 0 .R"), "123"); + } + + #[test] + fn test_rdepth_empty() { + assert_eq!(eval_stack("RDEPTH"), vec![0]); + } + + #[test] + fn test_rdepth_counts_r_items() { + assert_eq!( + eval_stack(": TRD 10 >R 20 >R RDEPTH 2R> 2DROP ; TRD"), + vec![2] + ); + } + + #[test] + fn test_dot_rs_empty() { + assert_eq!(eval_output(".RS"), "R:<0> "); + } + + #[test] + fn test_dot_rs_bottom_to_top() { + assert_eq!( + eval_output(": TRS 10 >R 20 >R .RS 2R> 2DROP ; TRS"), + "R:<2> 10 20 " + ); + } + + #[test] + fn test_dot_rs_honors_base() { + assert_eq!( + eval_output("HEX : TRS16 FF >R .RS R> DROP ; TRS16"), + "R:<1> FF " + ); + } + + #[test] + fn test_rdepth_sees_loop_params() { + // Inside DO/LOOP the return stack holds the loop parameters, + // so RDEPTH must be > 0 there and 0 again after the loop. + // eval_stack returns top-first: [after-loop RDEPTH, inside-loop RDEPTH] + let stack = eval_stack(": TRL 0 3 1 DO DROP RDEPTH LOOP RDEPTH ; TRL"); + assert_eq!(stack.len(), 2); + assert!(stack[1] > 0, "inside loop: expected non-empty return stack"); + assert_eq!(stack[0], 0, "after loop: return stack must be empty"); + } + + #[test] + fn test_bye_sets_flag_and_stops_line() { + let mut vm = ForthVM::::new().unwrap(); + vm.evaluate("1 BYE 2").unwrap(); + assert!(vm.bye_requested()); + // BYE stops the rest of the line: only the 1 was pushed + assert_eq!(vm.data_stack(), vec![1]); + } + + #[test] + fn test_stack_guard_underflow_interpreted() { + let mut vm = ForthVM::::new().unwrap(); + let e = vm.evaluate("DROP").unwrap_err(); + assert_eq!(e.to_string(), "Stack underflow (throw -4)"); + // Stack machinery intact afterwards + vm.evaluate("7 .").unwrap(); + assert_eq!(vm.take_output(), "7 "); + } + + #[test] + fn test_stack_guard_underflow_in_compiled_word() { + // Straight-line word (promoted path) and mid-word underflow + let mut vm = ForthVM::::new().unwrap(); + vm.evaluate(": D2 DROP DROP ;").unwrap(); + let e = vm.evaluate("1 D2").unwrap_err(); + assert_eq!(e.to_string(), "Stack underflow (throw -4)"); + // The guard fires BEFORE corruption: the 1 must still be there + assert_eq!(vm.data_stack(), vec![1]); + // Non-promoted (contains a call): same guard. Fresh VM — the + // failed D2 call above deliberately left its stack untouched. + let mut vm = ForthVM::::new().unwrap(); + vm.evaluate(": D3 DUP . DROP DROP ;").unwrap(); + let e = vm.evaluate("2 D3").unwrap_err(); + assert_eq!(e.to_string(), "Stack underflow (throw -4)"); + } + + #[test] + fn test_stack_guard_overflow_compiled_loop() { + let mut vm = ForthVM::::new().unwrap(); + vm.evaluate(": FLOOD BEGIN 1 AGAIN ;").unwrap(); + let e = vm.evaluate("FLOOD").unwrap_err(); + assert_eq!(e.to_string(), "Stack overflow (throw -3)"); + } + + #[test] + fn test_stack_guard_float_underflow() { + let mut vm = ForthVM::::new().unwrap(); + vm.evaluate(": FD FDROP ;").unwrap(); + let e = vm.evaluate("FD").unwrap_err(); + assert_eq!(e.to_string(), "Floating-point stack underflow (throw -45)"); + } + + #[test] + fn test_stack_guard_return_stack_underflow() { + let mut vm = ForthVM::::new().unwrap(); + vm.evaluate(": RU R> DROP ;").unwrap(); + // R> with nothing user-pushed underflows the return stack + let e = vm.evaluate("RU").unwrap_err(); + assert_eq!(e.to_string(), "Return stack underflow (throw -6)"); + } + + #[test] + fn test_stack_guard_catchable() { + let mut vm = ForthVM::::new().unwrap(); + vm.evaluate(": BAD DROP ;").unwrap(); + vm.evaluate("' BAD CATCH").unwrap(); + assert_eq!(vm.data_stack(), vec![-4]); + } + + #[test] + fn test_stack_guards_off_config() { + let mut cfg = WaferConfig::all(); + cfg.codegen.stack_guards = false; + let mut vm = ForthVM::::new_with_config(cfg).unwrap(); + // Compiled DROP underflows silently (documented unguarded mode) + vm.evaluate(": D1 DROP ;").unwrap(); + assert!(vm.evaluate("D1").is_ok()); + } + + #[test] + fn test_uncaught_throw_has_message() { + let mut vm = ForthVM::::new().unwrap(); + let e = vm.evaluate("-4 THROW").unwrap_err(); + assert_eq!(e.to_string(), "Stack underflow (throw -4)"); + let e = vm.evaluate("-77 THROW").unwrap_err(); + assert_eq!(e.to_string(), "Catch = -77"); + } + + #[test] + fn test_uncaught_abort_quote_shows_text() { + let mut vm = ForthVM::::new().unwrap(); + let e = vm + .evaluate(": TST -1 ABORT\" all is lost\" ; TST") + .unwrap_err(); + assert_eq!(e.to_string(), "all is lost"); + // Caught -2 must NOT print the text, and the payload must not leak + vm.evaluate(": TC ['] TST CATCH ; TC").unwrap(); + assert_eq!(vm.take_output(), ""); + assert_eq!(vm.data_stack(), vec![-2]); + } + + #[test] + fn test_order_names_forth() { + let output = eval_output("ORDER"); + assert!(output.contains("FORTH")); + assert!(!output.contains('[')); + } + // =================================================================== // Double DOES>: Forth 2012 WEIRD: W1 test // =================================================================== @@ -8981,6 +10417,51 @@ mod tests { vm.evaluate("MARKER MARK1").unwrap(); // MARK1 should exist and be callable vm.evaluate("MARK1").unwrap(); + // ...and remove itself when executed + assert!(vm.evaluate("MARK1").is_err()); + } + + #[test] + fn test_remember_survives_and_reruns() { + let mut vm = ForthVM::::new().unwrap(); + vm.evaluate("REMEMBER RST : AA 1 ;").unwrap(); + vm.evaluate("RST").unwrap(); + assert!(vm.evaluate("AA").is_err(), "AA must be pruned"); + // RST survived its own execution and is re-runnable + vm.evaluate(": BB 2 ; RST").unwrap(); + assert!(vm.evaluate("BB").is_err(), "BB must be pruned"); + // VM stays fully usable afterwards + vm.evaluate(": CC 3 ; CC").unwrap(); + assert_eq!(vm.data_stack(), vec![3]); + } + + #[test] + fn test_remember_restores_wordlist_allocation() { + let mut vm = ForthVM::::new().unwrap(); + vm.evaluate("REMEMBER RST WORDLIST DROP RST WORDLIST") + .unwrap(); + // wid allocation rolled back: the post-RST WORDLIST reuses wid 2 + assert_eq!(vm.data_stack(), vec![2]); + } + + #[test] + fn test_empty_returns_to_boot() { + let mut vm = ForthVM::::new().unwrap(); + vm.evaluate(": ZZ 9 ;").unwrap(); + vm.evaluate("EMPTY").unwrap(); + assert!(vm.evaluate("ZZ").is_err(), "ZZ must be pruned"); + vm.evaluate("1 2 + .").unwrap(); + assert_eq!(vm.take_output(), "3 "); + } + + #[test] + fn test_gild_rebaselines_empty() { + let mut vm = ForthVM::::new().unwrap(); + vm.evaluate(": KEEP1 1 ; GILD : GONE1 2 ;").unwrap(); + vm.evaluate("EMPTY").unwrap(); + assert!(vm.evaluate("GONE1").is_err(), "GONE1 must be pruned"); + vm.evaluate("KEEP1 .").unwrap(); + assert_eq!(vm.take_output(), "1 "); } #[test] diff --git a/crates/core/src/runtime_native.rs b/crates/core/src/runtime_native.rs index 37b250f..de29f3e 100644 --- a/crates/core/src/runtime_native.rs +++ b/crates/core/src/runtime_native.rs @@ -98,11 +98,29 @@ impl HostAccess for CallerHostAccess<'_, '_> { let func = *func_ref .unwrap_func() .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(()) } } +/// 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 : 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::() + .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. pub struct NativeRuntime { engine: Engine, @@ -293,7 +311,8 @@ impl Runtime for NativeRuntime { let func = *r .unwrap_func() .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(()) } diff --git a/crates/core/src/see.rs b/crates/core/src/see.rs new file mode 100644 index 0000000..30ea2b6 --- /dev/null +++ b/crates/core/src/see.rs @@ -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 { + 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 { + resolve(id).unwrap_or_else(|| format!("#{}", id.0)) +} + +fn write_ops( + out: &mut String, + ops: &[IrOp], + depth: usize, + resolve: &dyn Fn(WordId) -> Option, +) { + 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) { + // 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())); + } +} diff --git a/crates/core/src/wordhelp.rs b/crates/core/src/wordhelp.rs new file mode 100644 index 0000000..2cae0cd --- /dev/null +++ b/crates/core/src/wordhelp.rs @@ -0,0 +1,1141 @@ +//! Static per-word documentation for `HELP` (and the `SEE`/`SEE-IR` header +//! line): name, stack effect, one-line description. +//! +//! Coverage is **total by construction**: a unit test in `outer.rs` walks a +//! freshly booted VM and asserts every visible dictionary word and every +//! outer-interpreter token has an entry here, and that every entry resolves +//! back. Adding a word to WAFER without documenting it fails the build's +//! test gate. +//! +//! Stack effects follow Forth 2012 notation; `F:` marks the float stack, +//! `R:` the return stack, quoted names (`"name"`) are parsed from the input. + +/// (NAME, stack effect, one-line description) +pub const WORD_DOCS: &[(&str, &str, &str)] = &[ + // -- Core: stack manipulation -- + ( + "DUP", + "( x -- x x )", + "Duplicate the top of the data stack.", + ), + ("DROP", "( x -- )", "Discard the top of the data stack."), + ( + "SWAP", + "( x1 x2 -- x2 x1 )", + "Exchange the top two stack items.", + ), + ( + "OVER", + "( x1 x2 -- x1 x2 x1 )", + "Copy the second item to the top.", + ), + ( + "ROT", + "( x1 x2 x3 -- x2 x3 x1 )", + "Rotate the third item to the top.", + ), + ( + "-ROT", + "( x1 x2 x3 -- x3 x1 x2 )", + "Rotate the top item to third place.", + ), + ("NIP", "( x1 x2 -- x2 )", "Discard the second stack item."), + ( + "TUCK", + "( x1 x2 -- x2 x1 x2 )", + "Copy the top item below the second.", + ), + ( + "?DUP", + "( x -- 0 | x x )", + "Duplicate the top item only if it is nonzero.", + ), + ( + "PICK", + "( xu..x0 u -- xu..x0 xu )", + "Copy the u-th stack item to the top.", + ), + ( + "ROLL", + "( xu..x0 u -- xu-1..x0 xu )", + "Rotate the u-th stack item to the top.", + ), + ("DEPTH", "( -- n )", "Number of cells on the data stack."), + ( + "2DUP", + "( x1 x2 -- x1 x2 x1 x2 )", + "Duplicate the top cell pair.", + ), + ("2DROP", "( x1 x2 -- )", "Discard the top cell pair."), + ( + "2SWAP", + "( x1 x2 x3 x4 -- x3 x4 x1 x2 )", + "Exchange the top two cell pairs.", + ), + ( + "2OVER", + "( x1 x2 x3 x4 -- x1 x2 x3 x4 x1 x2 )", + "Copy the second cell pair to the top.", + ), + ( + "2ROT", + "( x1 x2 x3 x4 x5 x6 -- x3 x4 x5 x6 x1 x2 )", + "Rotate the third cell pair to the top.", + ), + // -- Core: return stack -- + ( + ">R", + "( x -- ) ( R: -- x )", + "Move the top item to the return stack.", + ), + ( + "R>", + "( -- x ) ( R: x -- )", + "Move the top return-stack item back.", + ), + ( + "R@", + "( -- x ) ( R: x -- x )", + "Copy the top return-stack item.", + ), + ( + "2>R", + "( x1 x2 -- ) ( R: -- x1 x2 )", + "Move the top cell pair to the return stack.", + ), + ( + "2R>", + "( -- x1 x2 ) ( R: x1 x2 -- )", + "Move a cell pair back from the return stack.", + ), + ( + "2R@", + "( -- x1 x2 ) ( R: x1 x2 -- x1 x2 )", + "Copy the top return-stack cell pair.", + ), + ( + "N>R", + "( i*n +n -- ) ( R: -- j*x +n )", + "Move +n items to the return stack.", + ), + ( + "NR>", + "( -- i*x +n ) ( R: j*x +n -- )", + "Move back items stored by N>R.", + ), + ("SP@", "( -- addr )", "Current data-stack pointer."), + ("RP@", "( -- addr )", "Current return-stack pointer."), + ("RDEPTH", "( -- n )", "Number of cells on the return stack."), + // -- Core: arithmetic -- + ("+", "( n1 n2 -- n3 )", "Add: n3 = n1 + n2."), + ("-", "( n1 n2 -- n3 )", "Subtract: n3 = n1 - n2."), + ("*", "( n1 n2 -- n3 )", "Multiply: n3 = n1 * n2."), + ("/", "( n1 n2 -- n3 )", "Divide: n3 = n1 / n2."), + ("MOD", "( n1 n2 -- n3 )", "Remainder of n1 / n2."), + ( + "/MOD", + "( n1 n2 -- rem quot )", + "Remainder and quotient of n1 / n2.", + ), + ( + "*/", + "( n1 n2 n3 -- n4 )", + "n1 * n2 / n3 with a double-cell intermediate.", + ), + ( + "*/MOD", + "( n1 n2 n3 -- rem quot )", + "n1 * n2 / n3, remainder and quotient.", + ), + ("1+", "( n -- n+1 )", "Add one."), + ("1-", "( n -- n-1 )", "Subtract one."), + ( + "2*", + "( n -- n*2 )", + "Shift left one bit (multiply by two).", + ), + ( + "2/", + "( n -- n/2 )", + "Arithmetic shift right one bit (divide by two).", + ), + ("NEGATE", "( n -- -n )", "Two's-complement negation."), + ("ABS", "( n -- |n| )", "Absolute value."), + ("MIN", "( n1 n2 -- n3 )", "Smaller of n1 and n2."), + ("MAX", "( n1 n2 -- n3 )", "Larger of n1 and n2."), + ( + "M*", + "( n1 n2 -- d )", + "Signed multiply to a double-cell product.", + ), + ( + "M+", + "( d1 n -- d2 )", + "Add a single-cell number to a double.", + ), + ( + "M*/", + "( d1 n1 +n2 -- d2 )", + "d1 * n1 / n2 with a triple-cell intermediate.", + ), + ( + "UM*", + "( u1 u2 -- ud )", + "Unsigned multiply to a double-cell product.", + ), + ( + "UM/MOD", + "( ud u1 -- rem quot )", + "Unsigned double divided by single.", + ), + ( + "SM/REM", + "( d n1 -- rem quot )", + "Symmetric signed double / single division.", + ), + ( + "FM/MOD", + "( d n1 -- rem quot )", + "Floored signed double / single division.", + ), + // -- Core: comparison -- + ("=", "( x1 x2 -- flag )", "True if equal."), + ("<>", "( x1 x2 -- flag )", "True if not equal."), + ("<", "( n1 n2 -- flag )", "True if n1 < n2 (signed)."), + (">", "( n1 n2 -- flag )", "True if n1 > n2 (signed)."), + ("<=", "( n1 n2 -- flag )", "True if n1 <= n2 (signed)."), + (">=", "( n1 n2 -- flag )", "True if n1 >= n2 (signed)."), + ("U<", "( u1 u2 -- flag )", "True if u1 < u2 (unsigned)."), + ( + "WITHIN", + "( x lo hi -- flag )", + "True if lo <= x < hi (circular compare).", + ), + ("U>", "( u1 u2 -- flag )", "True if u1 > u2 (unsigned)."), + ("0=", "( x -- flag )", "True if zero."), + ("0<", "( n -- flag )", "True if negative."), + ("0>", "( n -- flag )", "True if positive."), + ("0<>", "( x -- flag )", "True if nonzero."), + // -- Core: logic -- + ("AND", "( x1 x2 -- x3 )", "Bitwise AND."), + ("OR", "( x1 x2 -- x3 )", "Bitwise OR."), + ("XOR", "( x1 x2 -- x3 )", "Bitwise exclusive OR."), + ("INVERT", "( x -- ~x )", "Bitwise complement."), + ("LSHIFT", "( x u -- x' )", "Shift left by u bits."), + ("RSHIFT", "( x u -- x' )", "Logical shift right by u bits."), + ("TRUE", "( -- -1 )", "All-bits-set true flag."), + ("FALSE", "( -- 0 )", "Zero false flag."), + // -- Core: memory -- + ("@", "( addr -- x )", "Fetch the cell at addr."), + ("!", "( x addr -- )", "Store x into the cell at addr."), + ("C@", "( addr -- char )", "Fetch the byte at addr."), + ("C!", "( char addr -- )", "Store a byte at addr."), + ("+!", "( n addr -- )", "Add n to the cell at addr."), + ("2@", "( addr -- x1 x2 )", "Fetch the cell pair at addr."), + ("2!", "( x1 x2 addr -- )", "Store a cell pair at addr."), + ("HERE", "( -- addr )", "Next free data-space address."), + ("ALLOT", "( n -- )", "Reserve n bytes of data space."), + (",", "( x -- )", "Compile a cell into data space."), + ("C,", "( char -- )", "Compile a byte into data space."), + ( + "ALIGN", + "( -- )", + "Align the data-space pointer to a cell boundary.", + ), + ( + "ALIGNED", + "( addr -- a-addr )", + "Round addr up to a cell boundary.", + ), + ("CELLS", "( n1 -- n2 )", "Size in bytes of n1 cells."), + ("CELL+", "( addr1 -- addr2 )", "Advance addr by one cell."), + ("CHARS", "( n1 -- n2 )", "Size in bytes of n1 characters."), + ( + "CHAR+", + "( addr1 -- addr2 )", + "Advance addr by one character.", + ), + ( + "MOVE", + "( addr1 addr2 u -- )", + "Copy u bytes, overlap-safe.", + ), + ("CMOVE", "( addr1 addr2 u -- )", "Copy u bytes low-to-high."), + ( + "CMOVE>", + "( addr1 addr2 u -- )", + "Copy u bytes high-to-low.", + ), + ("FILL", "( addr u char -- )", "Fill u bytes with char."), + ("ERASE", "( addr u -- )", "Fill u bytes with zero."), + ("BLANK", "( addr u -- )", "Fill u bytes with spaces."), + ("PAD", "( -- addr )", "Scratch buffer address."), + ("UNUSED", "( -- u )", "Bytes of data space remaining."), + ( + "ALLOCATE", + "( u -- addr ior )", + "Allocate u bytes from the heap.", + ), + ("FREE", "( addr -- ior )", "Release an ALLOCATEd region."), + ( + "RESIZE", + "( addr1 u -- addr2 ior )", + "Resize an ALLOCATEd region.", + ), + // -- Core: I/O and numeric output -- + ( + ".", + "( n -- )", + "Print n in the current BASE, followed by a space.", + ), + ("U.", "( u -- )", "Print u as an unsigned number."), + ( + ".R", + "( n u -- )", + "Print n right-aligned in a u-wide field.", + ), + ( + "U.R", + "( u1 u2 -- )", + "Print u1 unsigned, right-aligned in u2 columns.", + ), + ("D.", "( d -- )", "Print a double-cell number."), + ( + "D.R", + "( d u -- )", + "Print a double right-aligned in u columns.", + ), + ("EMIT", "( char -- )", "Output one character."), + ( + "TYPE", + "( c-addr u -- )", + "Output u characters from c-addr.", + ), + ("CR", "( -- )", "Output a newline."), + ("SPACE", "( -- )", "Output one space."), + ("SPACES", "( n -- )", "Output n spaces."), + ("PAGE", "( -- )", "Output a form feed (clear screen)."), + ("BL", "( -- 32 )", "The space character code."), + ( + "COUNT", + "( c-addr1 -- c-addr2 u )", + "Unpack a counted string.", + ), + ("<#", "( -- )", "Begin pictured numeric output."), + ( + "#", + "( ud1 -- ud2 )", + "Convert one digit into the pictured buffer.", + ), + ("#S", "( ud -- 0 0 )", "Convert all remaining digits."), + ( + "#>", + "( ud -- c-addr u )", + "End pictured output; yield the string.", + ), + ( + "HOLD", + "( char -- )", + "Insert char into the pictured buffer.", + ), + ( + "HOLDS", + "( c-addr u -- )", + "Insert a string into the pictured buffer.", + ), + ("SIGN", "( n -- )", "Insert a minus sign if n is negative."), + ( + ">NUMBER", + "( ud1 c-addr1 u1 -- ud2 c-addr2 u2 )", + "Convert digits, accumulating into ud.", + ), + ("BASE", "( -- addr )", "Variable holding the number base."), + ("HEX", "( -- )", "Set BASE to sixteen."), + ("DECIMAL", "( -- )", "Set BASE to ten."), + // -- Core: strings -- + ( + "S\"", + "( \"ccc\" -- c-addr u )", + "String literal (interpret or compile).", + ), + ( + "S\\\"", + "( \"ccc\" -- c-addr u )", + "String literal with escape sequences.", + ), + ( + "C\"", + "( \"ccc\" -- c-addr )", + "Counted-string literal.", + ), + ( + "S", + "( \"name\" -- c-addr u )", + "WAFER: next token as a string literal.", + ), + (".\"", "( \"ccc\" -- )", "Print a string literal."), + ( + ".(", + "( \"ccc\" -- )", + "Print immediately while parsing.", + ), + ( + "COMPARE", + "( c-addr1 u1 c-addr2 u2 -- n )", + "Lexicographic string comparison.", + ), + ( + "SEARCH", + "( c-addr1 u1 c-addr2 u2 -- c-addr3 u3 flag )", + "Find a substring.", + ), + ( + "-TRAILING", + "( c-addr u1 -- c-addr u2 )", + "Drop trailing spaces from a string.", + ), + ( + "/STRING", + "( c-addr1 u1 n -- c-addr2 u2 )", + "Advance a string by n characters.", + ), + ( + "UNESCAPE", + "( c-addr1 u1 c-addr2 -- c-addr2 u2 )", + "Double each % for SUBSTITUTE.", + ), + ( + "SUBSTITUTE", + "( c1 u1 c2 u2 -- c2 u3 n )", + "Expand %name% substitutions.", + ), + ( + "REPLACES", + "( c1 u1 c2 u2 -- )", + "Define a SUBSTITUTE replacement.", + ), + // -- Core: definitions and execution -- + (":", "( \"name\" -- )", "Begin a colon definition."), + (";", "( -- )", "End a colon definition."), + ( + ":NONAME", + "( -- xt )", + "Begin an anonymous definition; leaves its xt.", + ), + ("[:", "( -- )", "Begin a quotation (nestable anonymous xt)."), + (";]", "( -- xt )", "End a quotation; yields its xt."), + ("[", "( -- )", "Switch to interpret state."), + ("]", "( -- )", "Switch to compile state."), + ( + "{:", + "( arg*i \"locals :}\" -- )", + "Declare named locals for this definition.", + ), + ( + "(LOCAL)", + "( c-addr u -- )", + "Declare one local (Forth 2012 batch protocol).", + ), + ( + "CREATE", + "( \"name\" -- )", + "Define a word that pushes its data address.", + ), + ("VARIABLE", "( \"name\" -- )", "Define a one-cell variable."), + ( + "2VARIABLE", + "( \"name\" -- )", + "Define a two-cell variable.", + ), + ("FVARIABLE", "( \"name\" -- )", "Define a float variable."), + ( + "CONSTANT", + "( x \"name\" -- )", + "Define a constant pushing x.", + ), + ( + "2CONSTANT", + "( x1 x2 \"name\" -- )", + "Define a double-cell constant.", + ), + ( + "FCONSTANT", + "( F: r -- ) ( \"name\" -- )", + "Define a float constant.", + ), + ( + "VALUE", + "( x \"name\" -- )", + "Define a value; read by name, set with TO.", + ), + ( + "2VALUE", + "( x1 x2 \"name\" -- )", + "Define a double-cell value.", + ), + ( + "FVALUE", + "( F: r -- ) ( \"name\" -- )", + "Define a float value.", + ), + ("TO", "( x \"name\" -- )", "Store into a VALUE or local."), + ( + "BUFFER:", + "( u \"name\" -- )", + "Define a word pushing a u-byte buffer.", + ), + ( + "DEFER", + "( \"name\" -- )", + "Define a deferred word (vector).", + ), + ( + "DEFER@", + "( xt1 -- xt2 )", + "Fetch a deferred word's action.", + ), + ( + "DEFER!", + "( xt2 xt1 -- )", + "Store a deferred word's action.", + ), + ("IS", "( xt \"name\" -- )", "Set a deferred word's action."), + ( + "ACTION-OF", + "( \"name\" -- xt )", + "Fetch a deferred word's action by name.", + ), + ( + "DOES>", + "( -- )", + "Give the latest CREATEd word a runtime action.", + ), + ( + "IMMEDIATE", + "( -- )", + "Mark the latest definition immediate.", + ), + ( + "POSTPONE", + "( \"name\" -- )", + "Compile the compilation semantics of name.", + ), + ( + "LITERAL", + "( x -- )", + "Compile x as a literal (compile-only).", + ), + ("2LITERAL", "( x1 x2 -- )", "Compile a double-cell literal."), + ("FLITERAL", "( F: r -- )", "Compile a float literal."), + ("SLITERAL", "( c-addr u -- )", "Compile a string literal."), + ("COMPILE,", "( xt -- )", "Compile a call to xt."), + ( + "[']", + "( \"name\" -- )", + "Compile the xt of name as a literal.", + ), + ( + "'", + "( \"name\" -- xt )", + "Find name; push its execution token.", + ), + ( + "CHAR", + "( \"name\" -- char )", + "First character of the next word.", + ), + ( + "[CHAR]", + "( \"name\" -- )", + "Compile the first character as a literal.", + ), + ( + "EXECUTE", + "( xt -- )", + "Execute the word with execution token xt.", + ), + ( + "EXIT", + "( -- )", + "Return from the current word (compile-only).", + ), + ( + "RECURSE", + "( -- )", + "Call the word currently being defined.", + ), + ( + "SYNONYM", + "( \"new\" \"old\" -- )", + "Define new as an alias of old.", + ), + ( + "FIND", + "( c-addr -- c-addr 0 | xt 1 | xt -1 )", + "Look up a counted-string name.", + ), + ( + ">BODY", + "( xt -- a-addr )", + "Data-field address of a CREATEd word.", + ), + // -- Core: control flow -- + ( + "IF", + "( flag -- )", + "Execute the following if flag is nonzero.", + ), + ("ELSE", "( -- )", "Alternative branch of IF."), + ("THEN", "( -- )", "End of IF."), + ("BEGIN", "( -- )", "Start an indefinite loop."), + ( + "UNTIL", + "( flag -- )", + "Loop back to BEGIN while flag is zero.", + ), + ("AGAIN", "( -- )", "Loop back to BEGIN forever."), + ( + "WHILE", + "( flag -- )", + "Continue the loop while flag is nonzero.", + ), + ("REPEAT", "( -- )", "Loop back to BEGIN (after WHILE)."), + ("DO", "( limit start -- )", "Start a counted loop."), + ( + "?DO", + "( limit start -- )", + "Counted loop; skip entirely if limit = start.", + ), + ( + "LOOP", + "( -- )", + "Increment the index; repeat until it meets the limit.", + ), + ( + "+LOOP", + "( n -- )", + "Add n to the index; repeat conditionally.", + ), + ("I", "( -- n )", "Innermost loop index."), + ("J", "( -- n )", "Next-outer loop index."), + ("LEAVE", "( -- )", "Exit the current counted loop."), + ("UNLOOP", "( -- )", "Discard loop parameters before EXIT."), + ( + "AHEAD", + "( -- )", + "Unconditional forward branch (resolved by THEN).", + ), + ("CASE", "( -- )", "Start a CASE structure."), + ("OF", "( x1 x2 -- | x1 )", "Match one CASE selector."), + ("ENDOF", "( -- )", "End one OF clause."), + ("ENDCASE", "( x -- )", "End the CASE structure."), + ( + "CS-PICK", + "( u -- )", + "Copy a control-flow stack entry (compilation).", + ), + ( + "CS-ROLL", + "( u -- )", + "Rotate control-flow stack entries (compilation).", + ), + // -- Core: interpreter, input, exceptions -- + ( + "(", + "( \"ccc\" -- )", + "Comment until the closing paren.", + ), + ("\\", "( \"ccc\" -- )", "Comment until end of line."), + ( + "[IF]", + "( flag -- )", + "Conditional compilation: take branch if nonzero.", + ), + ("[ELSE]", "( -- )", "Conditional-compilation alternative."), + ("[THEN]", "( -- )", "End conditional compilation."), + ( + "[DEFINED]", + "( \"name\" -- flag )", + "True if name is defined.", + ), + ( + "[UNDEFINED]", + "( \"name\" -- flag )", + "True if name is not defined.", + ), + ("STATE", "( -- addr )", "Variable: nonzero while compiling."), + ("SOURCE", "( -- c-addr u )", "Current input buffer."), + ( + "SOURCE-ID", + "( -- n )", + "Input source: 0 terminal, -1 string.", + ), + ( + ">IN", + "( -- addr )", + "Variable: offset into the input buffer.", + ), + ( + "WORD", + "( char \"ccc\" -- c-addr )", + "Parse delimited by char; counted string.", + ), + ( + "PARSE", + "( char \"ccc\" -- c-addr u )", + "Parse delimited by char.", + ), + ( + "PARSE-NAME", + "( \"name\" -- c-addr u )", + "Parse a whitespace-delimited name.", + ), + ( + "EVALUATE", + "( c-addr u -- )", + "Interpret the string as Forth input.", + ), + ( + "REFILL", + "( -- flag )", + "Refill the input buffer (false when piped).", + ), + ( + "ACCEPT", + "( c-addr +n1 -- +n2 )", + "Read a line of input (unsupported here).", + ), + ("ABORT", "( i*x -- )", "Empty the stacks and abort."), + ( + "ABORT\"", + "( flag -- )", + "If flag is nonzero, abort with a message.", + ), + ( + "CATCH", + "( xt -- 0 | code )", + "Execute xt, catching any THROW.", + ), + ( + "THROW", + "( code -- )", + "Raise exception code (0 is a no-op).", + ), + ( + "ENVIRONMENT?", + "( c-addr u -- false | val true )", + "Query an environment property.", + ), + ("BYE", "( -- )", "Leave the REPL / end the session."), + ("S>D", "( n -- d )", "Sign-extend a single to a double."), + ("D>S", "( d -- n )", "Narrow a double to a single."), + // -- Double-cell words -- + ("D+", "( d1 d2 -- d3 )", "Double-cell add."), + ("D-", "( d1 d2 -- d3 )", "Double-cell subtract."), + ("DNEGATE", "( d -- -d )", "Double-cell negate."), + ("DABS", "( d -- |d| )", "Double-cell absolute value."), + ("D0=", "( d -- flag )", "True if the double is zero."), + ("D0<", "( d -- flag )", "True if the double is negative."), + ("D=", "( d1 d2 -- flag )", "True if doubles are equal."), + ("D<", "( d1 d2 -- flag )", "True if d1 < d2 (signed)."), + ( + "DU<", + "( ud1 ud2 -- flag )", + "True if ud1 < ud2 (unsigned).", + ), + ("D2*", "( d1 -- d2 )", "Double-cell shift left."), + ("D2/", "( d1 -- d2 )", "Double-cell arithmetic shift right."), + ("DMIN", "( d1 d2 -- d3 )", "Smaller double."), + ("DMAX", "( d1 d2 -- d3 )", "Larger double."), + // -- Float words -- + ("F+", "( F: r1 r2 -- r3 )", "Float add."), + ("F-", "( F: r1 r2 -- r3 )", "Float subtract."), + ("F*", "( F: r1 r2 -- r3 )", "Float multiply."), + ("F/", "( F: r1 r2 -- r3 )", "Float divide."), + ("F**", "( F: r1 r2 -- r3 )", "Raise r1 to the power r2."), + ("FNEGATE", "( F: r -- -r )", "Float negate."), + ("FABS", "( F: r -- |r| )", "Float absolute value."), + ("FSQRT", "( F: r -- r' )", "Float square root."), + ("FMIN", "( F: r1 r2 -- r3 )", "Smaller float."), + ("FMAX", "( F: r1 r2 -- r3 )", "Larger float."), + ("FLOOR", "( F: r -- r' )", "Round toward negative infinity."), + ("FROUND", "( F: r -- r' )", "Round to nearest."), + ("FDUP", "( F: r -- r r )", "Duplicate the float top."), + ("FDROP", "( F: r -- )", "Discard the float top."), + ( + "FSWAP", + "( F: r1 r2 -- r2 r1 )", + "Exchange the top two floats.", + ), + ( + "FOVER", + "( F: r1 r2 -- r1 r2 r1 )", + "Copy the second float to the top.", + ), + ( + "FROT", + "( F: r1 r2 r3 -- r2 r3 r1 )", + "Rotate the third float to the top.", + ), + ("FNIP", "( F: r1 r2 -- r2 )", "Discard the second float."), + ( + "FTUCK", + "( F: r1 r2 -- r2 r1 r2 )", + "Copy the float top below the second.", + ), + ("FDEPTH", "( -- n )", "Number of floats on the float stack."), + ( + "F=", + "( F: r1 r2 -- ) ( -- flag )", + "True if floats are equal.", + ), + ("F<", "( F: r1 r2 -- ) ( -- flag )", "True if r1 < r2."), + ( + "F0=", + "( F: r -- ) ( -- flag )", + "True if the float is zero.", + ), + ( + "F0<", + "( F: r -- ) ( -- flag )", + "True if the float is negative.", + ), + ( + "F~", + "( F: r1 r2 r3 -- ) ( -- flag )", + "Approximate float equality test.", + ), + ("F@", "( addr -- ) ( F: -- r )", "Fetch a float from addr."), + ("F!", "( addr -- ) ( F: r -- )", "Store a float at addr."), + ("SF@", "( addr -- ) ( F: -- r )", "Fetch a 32-bit float."), + ("SF!", "( addr -- ) ( F: r -- )", "Store a 32-bit float."), + ("DF@", "( addr -- ) ( F: -- r )", "Fetch a 64-bit float."), + ("DF!", "( addr -- ) ( F: r -- )", "Store a 64-bit float."), + ("S>F", "( n -- ) ( F: -- r )", "Convert single to float."), + ( + "F>S", + "( F: r -- ) ( -- n )", + "Convert float to single (truncate).", + ), + ("D>F", "( d -- ) ( F: -- r )", "Convert double to float."), + ( + "F>D", + "( F: r -- ) ( -- d )", + "Convert float to double (truncate).", + ), + ("FLOATS", "( n1 -- n2 )", "Size in bytes of n1 floats."), + ("FLOAT+", "( addr1 -- addr2 )", "Advance addr by one float."), + ( + "SFLOATS", + "( n1 -- n2 )", + "Size in bytes of n1 32-bit floats.", + ), + ( + "SFLOAT+", + "( addr1 -- addr2 )", + "Advance addr by one 32-bit float.", + ), + ( + "DFLOATS", + "( n1 -- n2 )", + "Size in bytes of n1 64-bit floats.", + ), + ( + "DFLOAT+", + "( addr1 -- addr2 )", + "Advance addr by one 64-bit float.", + ), + ("FALIGN", "( -- )", "Align data space for a float."), + ( + "FALIGNED", + "( addr -- f-addr )", + "Round addr up to float alignment.", + ), + ("SFALIGN", "( -- )", "Align data space for a 32-bit float."), + ( + "SFALIGNED", + "( addr -- sf-addr )", + "Round addr up to 32-bit float alignment.", + ), + ("DFALIGN", "( -- )", "Align data space for a 64-bit float."), + ( + "DFALIGNED", + "( addr -- df-addr )", + "Round addr up to 64-bit float alignment.", + ), + ( + "F.", + "( F: r -- )", + "Print a float in fixed-point notation.", + ), + ( + "FE.", + "( F: r -- )", + "Print a float in engineering notation.", + ), + ( + "FS.", + "( F: r -- )", + "Print a float in scientific notation.", + ), + ("PRECISION", "( -- u )", "Digits used by float output."), + ("SET-PRECISION", "( u -- )", "Set float output digits."), + ( + "REPRESENT", + "( c-addr u -- n flag1 flag2 ) ( F: r -- )", + "Convert a float to digit text.", + ), + ( + ">FLOAT", + "( c-addr u -- flag ) ( F: -- r | )", + "Parse a string as a float.", + ), + ("FSIN", "( F: r -- r' )", "Sine (radians)."), + ("FCOS", "( F: r -- r' )", "Cosine (radians)."), + ("FTAN", "( F: r -- r' )", "Tangent (radians)."), + ("FASIN", "( F: r -- r' )", "Arc sine."), + ("FACOS", "( F: r -- r' )", "Arc cosine."), + ("FATAN", "( F: r -- r' )", "Arc tangent."), + ( + "FATAN2", + "( F: ry rx -- r )", + "Arc tangent of ry/rx, quadrant-correct.", + ), + ( + "FSINCOS", + "( F: r -- rsin rcos )", + "Sine and cosine together.", + ), + ("FSINH", "( F: r -- r' )", "Hyperbolic sine."), + ("FCOSH", "( F: r -- r' )", "Hyperbolic cosine."), + ("FTANH", "( F: r -- r' )", "Hyperbolic tangent."), + ("FASINH", "( F: r -- r' )", "Inverse hyperbolic sine."), + ("FACOSH", "( F: r -- r' )", "Inverse hyperbolic cosine."), + ("FATANH", "( F: r -- r' )", "Inverse hyperbolic tangent."), + ("FEXP", "( F: r -- r' )", "e to the power r."), + ("FEXPM1", "( F: r -- r' )", "e**r - 1, accurate near zero."), + ("FLN", "( F: r -- r' )", "Natural logarithm."), + ("FLNP1", "( F: r -- r' )", "ln(1+r), accurate near zero."), + ("FLOG", "( F: r -- r' )", "Base-10 logarithm."), + ("FALOG", "( F: r -- r' )", "10 to the power r."), + // -- Structures -- + ( + "BEGIN-STRUCTURE", + "( \"name\" -- addr 0 )", + "Start a structure definition.", + ), + ( + "END-STRUCTURE", + "( addr +n -- )", + "Finish a structure definition.", + ), + ( + "+FIELD", + "( offset size \"name\" -- offset' )", + "Define a field of the given size.", + ), + ( + "FIELD:", + "( offset \"name\" -- offset' )", + "Define an aligned cell field.", + ), + ( + "CFIELD:", + "( offset \"name\" -- offset' )", + "Define a one-byte field.", + ), + ( + "FFIELD:", + "( offset \"name\" -- offset' )", + "Define an aligned float field.", + ), + ( + "SFFIELD:", + "( offset \"name\" -- offset' )", + "Define a 32-bit float field.", + ), + ( + "DFFIELD:", + "( offset \"name\" -- offset' )", + "Define a 64-bit float field.", + ), + // -- Search order -- + ( + "FORTH-WORDLIST", + "( -- wid )", + "The main wordlist identifier.", + ), + ("WORDLIST", "( -- wid )", "Create a new wordlist."), + ( + "GET-CURRENT", + "( -- wid )", + "Wordlist receiving new definitions.", + ), + ("SET-CURRENT", "( wid -- )", "Set the compilation wordlist."), + ("GET-ORDER", "( -- widn..wid1 n )", "Current search order."), + ( + "SET-ORDER", + "( widn..wid1 n -- )", + "Set the search order (-1 = default).", + ), + ( + "SEARCH-WORDLIST", + "( c-addr u wid -- 0 | xt 1 | xt -1 )", + "Look up a name in one wordlist.", + ), + ( + "DEFINITIONS", + "( -- )", + "New definitions go to the top wordlist.", + ), + ("ALSO", "( -- )", "Duplicate the top of the search order."), + ("ONLY", "( -- )", "Reset the search order to the minimum."), + ("PREVIOUS", "( -- )", "Drop the top of the search order."), + ( + "FORTH", + "( -- )", + "Replace the search-order top with FORTH-WORDLIST.", + ), + ( + "ORDER", + "( -- )", + "Print the search order and compilation wordlist.", + ), + // -- Programming tools -- + ( + "WORDS", + "( \"filter\"? -- )", + "List words; optional substring filter; ALL = grouped view.", + ), + ( + "SEE", + "( \"name\" -- )", + "Show a word's source (or best fallback).", + ), + ( + "SEE-IR", + "( \"name\" -- )", + "Show a word's post-optimization IR.", + ), + ( + "HELP", + "( \"name\"? -- )", + "Show stack effect and description for a word.", + ), + (".S", "( -- )", "Print the data stack, respecting BASE."), + ("F.S", "( -- )", "Print the float stack."), + (".RS", "( -- )", "Print the return stack, respecting BASE."), + ("?", "( addr -- )", "Fetch and print the cell at addr."), + ( + "DUMP", + "( addr u -- )", + "Hex + ASCII dump of u bytes at addr.", + ), + ( + "MARKER", + "( \"name\" -- )", + "Word that rolls the dictionary back to before itself.", + ), + ( + "REMEMBER", + "( \"name\" -- )", + "Re-runnable marker: rolls back to just after itself.", + ), + ( + "EMPTY", + "( -- )", + "Roll back to the boot (or GILDed) state.", + ), + ( + "GILD", + "( -- )", + "Make the current state the EMPTY baseline.", + ), + ( + "INCLUDED", + "( c-addr u -- )", + "Interpret the named source file (nestable).", + ), + ( + "INCLUDE", + "( \"name\" -- )", + "Interpret the source file named in the input.", + ), + // -- WAFER-specific -- + ( + "CONSOLIDATE", + "( -- )", + "Recompile all IR words into one optimized module.", + ), + ( + "SHA1", + "( c-addr u -- c-addr2 20 )", + "SHA-1 digest into the hash scratch area.", + ), + ( + "SHA256", + "( c-addr u -- c-addr2 32 )", + "SHA-256 digest into the hash scratch area.", + ), + ( + "SHA512", + "( c-addr u -- c-addr2 64 )", + "SHA-512 digest into the hash scratch area.", + ), + ( + "RANDOM", + "( -- u )", + "32-bit pseudo-random number (xorshift64).", + ), + ( + "RND-SEED", + "( u -- )", + "Reseed the PRNG (0 forced nonzero).", + ), + ( + "UTIME", + "( -- d )", + "Microseconds since the Unix epoch, as a double.", + ), +]; + +/// Extract a leading stack-effect comment from source text, e.g. +/// `: SQ ( n -- n^2 ) DUP * ;` yields `( n -- n^2 )`. Returns the first +/// parenthesized comment that contains `--`. +pub fn stack_comment(source: &str) -> Option { + let open = source.find('(')?; + let close = open + source[open..].find(')')?; + let inner = &source[open..=close]; + inner.contains("--").then(|| inner.to_string()) +} + +/// Case-insensitive lookup: returns (stack effect, description). +pub fn lookup(name: &str) -> Option<(&'static str, &'static str)> { + WORD_DOCS + .iter() + .find(|(n, _, _)| n.eq_ignore_ascii_case(name)) + .map(|(_, effect, desc)| (*effect, *desc)) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn lookup_is_case_insensitive() { + assert_eq!(lookup("dup"), lookup("DUP")); + assert!(lookup("DUP").is_some()); + assert!(lookup("NO-SUCH-WORD-EVER").is_none()); + } + + #[test] + fn no_duplicate_names() { + let mut seen = std::collections::HashSet::new(); + for (name, _, _) in WORD_DOCS { + assert!( + seen.insert(name.to_ascii_uppercase()), + "duplicate WORD_DOCS entry: {name}" + ); + } + } +} diff --git a/crates/core/tests/comparison.rs b/crates/core/tests/comparison.rs index 5736677..2d7ab50 100644 --- a/crates/core/tests/comparison.rs +++ b/crates/core/tests/comparison.rs @@ -93,6 +93,8 @@ fn find_sf64() -> Option<&'static str> { /// Spawn `binary`, write `input` to its stdin, and collect the output. fn run_via_stdin(binary: &str, input: &str) -> Option { 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()) @@ -622,6 +624,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 { + 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) // ----------------------------------------------------------------------- diff --git a/crates/core/tests/compliance.rs b/crates/core/tests/compliance.rs index 6f2f373..95978be 100644 --- a/crates/core/tests/compliance.rs +++ b/crates/core/tests/compliance.rs @@ -105,8 +105,13 @@ fn expected_load_failures(path: &str) -> u32 { // 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 37; + return 38; } 0 } diff --git a/crates/web/Cargo.toml b/crates/web/Cargo.toml index 49a0bd5..f081149 100644 --- a/crates/web/Cargo.toml +++ b/crates/web/Cargo.toml @@ -12,7 +12,7 @@ workspace = true crate-type = ["cdylib", "rlib"] [dependencies] -wafer-core = { path = "../core", version = "0.1.0", default-features = false, features = ["crypto"] } +wafer-core = { path = "../core", version = "0.2.0", default-features = false, features = ["crypto"] } wasm-bindgen = "0.2" js-sys = "0.3" send_wrapper = { workspace = true } diff --git a/crates/web/src/lib.rs b/crates/web/src/lib.rs index 226919f..7b1ff0e 100644 --- a/crates/web/src/lib.rs +++ b/crates/web/src/lib.rs @@ -6,8 +6,9 @@ use send_wrapper::SendWrapper; use wasm_bindgen::prelude::*; 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::runtime::Runtime; use wafer_core::runtime::{HostAccess, HostFn}; use crate::runtime_web::WebRuntime; @@ -53,9 +54,12 @@ impl WaferRepl { /// Get the current number base (10 = decimal, 16 = hex). pub fn base(&mut self) -> u32 { - // BASE is stored at SYSVAR_BASE_VAR in WASM memory - self.vm.take_output(); // no-op side effect; just return base - 10 // TODO: read from memory once we have a getter + self.vm.runtime_mut().mem_read_i32(SYSVAR_BASE_VAR) as u32 + } + + /// Names of all user-facing words (visible, non-internal), newest first. + pub fn words(&self) -> Vec { + self.vm.word_names() } /// Reset the VM to initial state. diff --git a/crates/web/www/app.js b/crates/web/www/app.js index 9d6a4ec..55bdfab 100644 --- a/crates/web/www/app.js +++ b/crates/web/www/app.js @@ -1,8 +1,11 @@ import init, { WaferRepl } from './pkg/wafer_web.js'; let repl = null; -const history = []; -let historyIdx = -1; +const HISTORY_KEY = 'wafer-history'; +const HISTORY_MAX = 200; +const history = JSON.parse(localStorage.getItem(HISTORY_KEY) || '[]'); +let historyIdx = history.length; +let builtinWords = null; const WORD_CATEGORIES = { '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; try { const stack = repl.data_stack(); + const base = repl.base(); + const suffix = base !== 10 ? ` [base ${base}]` : ''; if (stack.length === 0) { - stackBar.textContent = 'Stack: (empty)'; + stackBar.textContent = `Stack: (empty)${suffix}`; } else { - stackBar.textContent = `Stack <${stack.length}> ${stack.join(' ')}`; + stackBar.textContent = `Stack <${stack.length}> ${stack.join(' ')}${suffix}`; } } catch { stackBar.textContent = 'Stack: (error)'; @@ -50,19 +55,25 @@ function updateStack() { } function updateUserWords() { - const cat = document.getElementById('cat-user'); - if (!cat) return; - // We'll track user words by checking what the REPL evaluates - // For now, just show the category + const list = document.getElementById('user-word-list'); + if (!list || !repl || !builtinWords) return; + list.innerHTML = ''; + 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; const trimmed = line.trim(); if (!trimmed) return; - // Add to history - history.push(trimmed); + // Add to history (user-typed lines only; skip consecutive duplicates) + if (record && history[history.length - 1] !== 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; try { @@ -83,6 +94,7 @@ function evaluate(line) { updatePrompt(); updateStack(); + updateUserWords(); } // Input handling @@ -116,6 +128,18 @@ document.getElementById('btn-toggle-words').addEventListener('click', () => { 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() { const container = document.getElementById('word-categories'); container.innerHTML = ''; @@ -129,15 +153,7 @@ function buildWordPanel() { const list = document.createElement('div'); list.className = 'word-list'; for (const w of words) { - 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(); - }); - list.appendChild(chip); + list.appendChild(wordChip(w)); } cat.appendChild(list); container.appendChild(cat); @@ -179,7 +195,7 @@ document.getElementById('btn-run-init').addEventListener('click', () => { if (code.trim()) { // Run each line separately for (const line of code.split('\n')) { - if (line.trim()) evaluate(line); + if (line.trim()) evaluate(line, false); } } localStorage.setItem('wafer-init-code', code); @@ -214,6 +230,7 @@ document.getElementById('btn-reset').addEventListener('click', () => { appendLine('WAFER reset.', 'line-ok'); updatePrompt(); updateStack(); + updateUserWords(); } catch (e) { appendLine(`Reset error: ${e.message}`, 'line-error'); } @@ -225,6 +242,8 @@ async function boot() { try { await init(); repl = new WaferRepl(); + // Everything defined at boot is "builtin"; later definitions are user words + builtinWords = new Set(repl.words()); output.innerHTML = ''; appendLine('WAFER — WebAssembly Forth Engine in Rust', '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; if (initCode.trim()) { for (const line of initCode.split('\n')) { - if (line.trim()) evaluate(line); + if (line.trim()) evaluate(line, false); } localStorage.setItem('wafer-init-code', initCode); } @@ -252,7 +271,7 @@ async function boot() { const code = atob(location.hash.slice(1)); document.getElementById('init-code').value = code; for (const line of code.split('\n')) { - if (line.trim()) evaluate(line); + if (line.trim()) evaluate(line, false); } } catch { /* ignore bad hash */ } } diff --git a/tools/editor-support/bat/WAFER.sublime-syntax b/tools/editor-support/bat/WAFER.sublime-syntax index cebd56e..a65c28b 100644 --- a/tools/editor-support/bat/WAFER.sublime-syntax +++ b/tools/editor-support/bat/WAFER.sublime-syntax @@ -33,7 +33,10 @@ contexts: - include: compare - include: memory - include: io + - include: pictured + - include: string_ops - include: float + - include: tools - include: dictionary - include: exception - include: parsing @@ -95,27 +98,31 @@ contexts: # 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|BUFFER:|FCONSTANT|FVARIABLE)(\s+)(\S+)?' + - 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){{ident_break}}' + - 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: - - match: '(?i)(?:^|(?<=\s))(>R|R>|R@|2>R|2R>|2R@|N>R|NR>|I|J|CS-PICK|CS-ROLL){{ident_break}}' + # 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){{ident_break}}' + - 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: @@ -123,21 +130,36 @@ contexts: scope: keyword.operator.logical.forth compare: - - match: '(?i)(?:^|(?<=\s))(=|<>|<|>|<=|>=|U<|U>|0=|0<>|0<|0>){{ident_break}}' + - 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!|ALLOT|HERE|ALIGN|ALIGNED|CELL\+|CELLS|CHAR\+|CHARS|UNUSED|MOVE|CMOVE|CMOVE>|FILL|ERASE|BLANK|ALLOCATE|FREE|RESIZE|PAD){{ident_break}}' + - 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){{ident_break}}' + - 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/|FNEGATE|FABS|FMAX|FMIN|FSQRT|FFLOOR|FROUND|FSINCOS|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|FALIGNED|DFALIGNED|SFALIGNED|DF@|DF!|SF@|SF!){{ident_break}}' + - 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 @@ -147,7 +169,7 @@ contexts: scope: keyword.control.exception.forth parsing: - - match: '(?i)(?:^|(?<=\s))(PARSE|PARSE-NAME|WORD|REFILL|EVALUATE|SOURCE|SOURCE-ID|>IN|BASE|STATE|>NUMBER|SEARCH|SUBSTITUTE|UNESCAPE|REPLACES|S){{ident_break}}' + - 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: @@ -185,5 +207,5 @@ contexts: 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){{ident_break}}' + - match: '(?i)(?:^|(?<=\s))(CONSOLIDATE|RANDOM|RND-SEED|UTIME|READ-PASSWORD|EMPTY|GILD){{ident_break}}' scope: support.function.wafer-extra.forth