diff --git a/Cargo.lock b/Cargo.lock index 4cb12a3..b0c7d8a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -17,21 +17,6 @@ version = "0.2.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" -[[package]] -name = "ambient-authority" -version = "0.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e9d4ee0d472d1cd2e28c97dfa124b3d8d992e10eb0a035f33f5d12e3a177ba3b" - -[[package]] -name = "android_system_properties" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" -dependencies = [ - "libc", -] - [[package]] name = "anstream" version = "1.0.0" @@ -171,90 +156,6 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" -[[package]] -name = "bytes" -version = "1.11.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" - -[[package]] -name = "cap-fs-ext" -version = "3.4.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d5528f85b1e134ae811704e41ef80930f56e795923f866813255bc342cc20654" -dependencies = [ - "cap-primitives", - "cap-std", - "io-lifetimes", - "windows-sys 0.59.0", -] - -[[package]] -name = "cap-net-ext" -version = "3.4.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "20a158160765c6a7d0d8c072a53d772e4cb243f38b04bfcf6b4939cfbe7482e7" -dependencies = [ - "cap-primitives", - "cap-std", - "rustix 1.1.4", - "smallvec", -] - -[[package]] -name = "cap-primitives" -version = "3.4.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6cf3aea8a5081171859ef57bc1606b1df6999df4f1110f8eef68b30098d1d3a" -dependencies = [ - "ambient-authority", - "fs-set-times", - "io-extras", - "io-lifetimes", - "ipnet", - "maybe-owned", - "rustix 1.1.4", - "rustix-linux-procfs", - "windows-sys 0.59.0", - "winx", -] - -[[package]] -name = "cap-rand" -version = "3.4.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d8144c22e24bbcf26ade86cb6501a0916c46b7e4787abdb0045a467eb1645a1d" -dependencies = [ - "ambient-authority", - "rand 0.8.5", -] - -[[package]] -name = "cap-std" -version = "3.4.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6dc3090992a735d23219de5c204927163d922f42f575a0189b005c62d37549a" -dependencies = [ - "cap-primitives", - "io-extras", - "io-lifetimes", - "rustix 1.1.4", -] - -[[package]] -name = "cap-time-ext" -version = "3.4.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "def102506ce40c11710a9b16e614af0cde8e76ae51b1f48c04b8d79f4b671a80" -dependencies = [ - "ambient-authority", - "cap-primitives", - "iana-time-zone", - "once_cell", - "rustix 1.1.4", - "winx", -] - [[package]] name = "cc" version = "1.2.58" @@ -354,12 +255,6 @@ dependencies = [ "windows-sys 0.61.2", ] -[[package]] -name = "core-foundation-sys" -version = "0.8.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" - [[package]] name = "cpp_demangle" version = "0.4.5" @@ -577,26 +472,6 @@ dependencies = [ "dirs-sys-next", ] -[[package]] -name = "dirs" -version = "4.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca3aa72a6f96ea37bbc5aa912f6788242832f75369bdfdadcb0e38423f100059" -dependencies = [ - "dirs-sys", -] - -[[package]] -name = "dirs-sys" -version = "0.3.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b1d1d91c932ef41c0f2663aa8b0ca0342d444d842c06914aa0a7e352d0bada6" -dependencies = [ - "libc", - "redox_users", - "winapi", -] - [[package]] name = "dirs-sys-next" version = "0.1.2" @@ -608,17 +483,6 @@ dependencies = [ "winapi", ] -[[package]] -name = "displaydoc" -version = "0.2.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - [[package]] name = "either" version = "1.15.0" @@ -721,86 +585,6 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" -[[package]] -name = "form_urlencoded" -version = "1.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" -dependencies = [ - "percent-encoding", -] - -[[package]] -name = "fs-set-times" -version = "0.20.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94e7099f6313ecacbe1256e8ff9d617b75d1bcb16a6fddef94866d225a01a14a" -dependencies = [ - "io-lifetimes", - "rustix 1.1.4", - "windows-sys 0.59.0", -] - -[[package]] -name = "futures" -version = "0.3.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b147ee9d1f6d097cef9ce628cd2ee62288d963e16fb287bd9286455b241382d" -dependencies = [ - "futures-channel", - "futures-core", - "futures-io", - "futures-sink", - "futures-task", - "futures-util", -] - -[[package]] -name = "futures-channel" -version = "0.3.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d" -dependencies = [ - "futures-core", - "futures-sink", -] - -[[package]] -name = "futures-core" -version = "0.3.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" - -[[package]] -name = "futures-io" -version = "0.3.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718" - -[[package]] -name = "futures-sink" -version = "0.3.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893" - -[[package]] -name = "futures-task" -version = "0.3.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" - -[[package]] -name = "futures-util" -version = "0.3.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" -dependencies = [ - "futures-core", - "futures-sink", - "futures-task", - "pin-project-lite", -] - [[package]] name = "fxhash" version = "0.2.1" @@ -911,138 +695,12 @@ dependencies = [ "windows-sys 0.61.2", ] -[[package]] -name = "iana-time-zone" -version = "0.1.65" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470" -dependencies = [ - "android_system_properties", - "core-foundation-sys", - "iana-time-zone-haiku", - "js-sys", - "log", - "wasm-bindgen", - "windows-core", -] - -[[package]] -name = "iana-time-zone-haiku" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" -dependencies = [ - "cc", -] - -[[package]] -name = "icu_collections" -version = "2.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c6b649701667bbe825c3b7e6388cb521c23d88644678e83c0c4d0a621a34b43" -dependencies = [ - "displaydoc", - "potential_utf", - "yoke", - "zerofrom", - "zerovec", -] - -[[package]] -name = "icu_locale_core" -version = "2.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "edba7861004dd3714265b4db54a3c390e880ab658fec5f7db895fae2046b5bb6" -dependencies = [ - "displaydoc", - "litemap", - "tinystr", - "writeable", - "zerovec", -] - -[[package]] -name = "icu_normalizer" -version = "2.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f6c8828b67bf8908d82127b2054ea1b4427ff0230ee9141c54251934ab1b599" -dependencies = [ - "icu_collections", - "icu_normalizer_data", - "icu_properties", - "icu_provider", - "smallvec", - "zerovec", -] - -[[package]] -name = "icu_normalizer_data" -version = "2.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7aedcccd01fc5fe81e6b489c15b247b8b0690feb23304303a9e560f37efc560a" - -[[package]] -name = "icu_properties" -version = "2.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "020bfc02fe870ec3a66d93e677ccca0562506e5872c650f893269e08615d74ec" -dependencies = [ - "icu_collections", - "icu_locale_core", - "icu_properties_data", - "icu_provider", - "zerotrie", - "zerovec", -] - -[[package]] -name = "icu_properties_data" -version = "2.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "616c294cf8d725c6afcd8f55abc17c56464ef6211f9ed59cccffe534129c77af" - -[[package]] -name = "icu_provider" -version = "2.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85962cf0ce02e1e0a629cc34e7ca3e373ce20dda4c4d7294bbd0bf1fdb59e614" -dependencies = [ - "displaydoc", - "icu_locale_core", - "writeable", - "yoke", - "zerofrom", - "zerotrie", - "zerovec", -] - [[package]] name = "id-arena" version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" -[[package]] -name = "idna" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" -dependencies = [ - "idna_adapter", - "smallvec", - "utf8_iter", -] - -[[package]] -name = "idna_adapter" -version = "1.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" -dependencies = [ - "icu_normalizer", - "icu_properties", -] - [[package]] name = "indexmap" version = "2.13.0" @@ -1067,28 +725,6 @@ dependencies = [ "tempfile", ] -[[package]] -name = "io-extras" -version = "0.18.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2285ddfe3054097ef4b2fe909ef8c3bcd1ea52a8f0d274416caebeef39f04a65" -dependencies = [ - "io-lifetimes", - "windows-sys 0.59.0", -] - -[[package]] -name = "io-lifetimes" -version = "2.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "06432fb54d3be7964ecd3649233cddf80db2832f47fec34c01f65b3d9d774983" - -[[package]] -name = "ipnet" -version = "2.12.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2" - [[package]] name = "is_terminal_polyfill" version = "1.70.2" @@ -1150,12 +786,6 @@ dependencies = [ "wasm-bindgen", ] -[[package]] -name = "leb128" -version = "0.2.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "884e2677b40cc8c339eaefcb701c32ef1fd2493d71118dc0ca4b6a736c93bd67" - [[package]] name = "leb128fmt" version = "0.1.0" @@ -1195,12 +825,6 @@ version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" -[[package]] -name = "litemap" -version = "0.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6373607a59f0be73a39b6fe456b8192fcc3585f602af20751600e974dd455e77" - [[package]] name = "log" version = "0.4.29" @@ -1216,12 +840,6 @@ dependencies = [ "libc", ] -[[package]] -name = "maybe-owned" -version = "0.3.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4facc753ae494aeb6e3c22f839b158aebd4f9270f55cd3c79906c45476c47ab4" - [[package]] name = "memchr" version = "2.8.0" @@ -1237,17 +855,6 @@ dependencies = [ "rustix 1.1.4", ] -[[package]] -name = "mio" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50b7e5b27aa02a74bac8c3f23f448f8d87ff11f92d3aac1a6ed369ee08cc56c1" -dependencies = [ - "libc", - "wasi", - "windows-sys 0.61.2", -] - [[package]] name = "nibble_vec" version = "0.1.0" @@ -1317,18 +924,6 @@ version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" -[[package]] -name = "percent-encoding" -version = "2.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" - -[[package]] -name = "pin-project-lite" -version = "0.2.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" - [[package]] name = "pkg-config" version = "0.3.32" @@ -1347,15 +942,6 @@ dependencies = [ "serde", ] -[[package]] -name = "potential_utf" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b73949432f5e2a09657003c25bca5e19a0e9c84f8058ca374f49e0ebe605af77" -dependencies = [ - "zerovec", -] - [[package]] name = "ppv-lite86" version = "0.2.21" @@ -1394,8 +980,8 @@ dependencies = [ "bit-vec", "bitflags", "num-traits", - "rand 0.9.2", - "rand_chacha 0.9.0", + "rand", + "rand_chacha", "rand_xorshift", "regex-syntax", "rusty-fork", @@ -1461,35 +1047,14 @@ dependencies = [ "nibble_vec", ] -[[package]] -name = "rand" -version = "0.8.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" -dependencies = [ - "libc", - "rand_chacha 0.3.1", - "rand_core 0.6.4", -] - [[package]] name = "rand" version = "0.9.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" dependencies = [ - "rand_chacha 0.9.0", - "rand_core 0.9.5", -] - -[[package]] -name = "rand_chacha" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" -dependencies = [ - "ppv-lite86", - "rand_core 0.6.4", + "rand_chacha", + "rand_core", ] [[package]] @@ -1499,16 +1064,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" dependencies = [ "ppv-lite86", - "rand_core 0.9.5", -] - -[[package]] -name = "rand_core" -version = "0.6.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" -dependencies = [ - "getrandom 0.2.17", + "rand_core", ] [[package]] @@ -1526,7 +1082,7 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "513962919efc330f829edb2535844d1b912b0fbe2ca165d613e4e8788bb05a5a" dependencies = [ - "rand_core 0.9.5", + "rand_core", ] [[package]] @@ -1618,16 +1174,6 @@ dependencies = [ "windows-sys 0.61.2", ] -[[package]] -name = "rustix-linux-procfs" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2fc84bf7e9aa16c4f2c758f27412dc9841341e16aa682d9c7ac308fe3ee12056" -dependencies = [ - "once_cell", - "rustix 1.1.4", -] - [[package]] name = "rustversion" version = "1.0.22" @@ -1741,15 +1287,6 @@ dependencies = [ "digest", ] -[[package]] -name = "shellexpand" -version = "2.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ccc8076840c4da029af4f87e4e8daeb0fca6b87bbb02e10cb60b791450e11e4" -dependencies = [ - "dirs", -] - [[package]] name = "shlex" version = "1.3.0" @@ -1771,16 +1308,6 @@ dependencies = [ "serde", ] -[[package]] -name = "socket2" -version = "0.6.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a766e1110788c36f4fa1c2b71b387a7815aa65f88ce0229841826633d93723e" -dependencies = [ - "libc", - "windows-sys 0.61.2", -] - [[package]] name = "sptr" version = "0.3.2" @@ -1810,33 +1337,6 @@ dependencies = [ "unicode-ident", ] -[[package]] -name = "synstructure" -version = "0.13.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "system-interface" -version = "0.27.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc4592f674ce18521c2a81483873a49596655b179f71c5e05d10c1fe66c78745" -dependencies = [ - "bitflags", - "cap-fs-ext", - "cap-std", - "fd-lock", - "io-lifetimes", - "rustix 0.38.44", - "windows-sys 0.59.0", - "winx", -] - [[package]] name = "target-lexicon" version = "0.13.5" @@ -1905,30 +1405,6 @@ dependencies = [ "syn", ] -[[package]] -name = "tinystr" -version = "0.8.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42d3e9c45c09de15d06dd8acf5f4e0e399e85927b7f00711024eb7ae10fa4869" -dependencies = [ - "displaydoc", - "zerovec", -] - -[[package]] -name = "tokio" -version = "1.50.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "27ad5e34374e03cfffefc301becb44e9dc3c17584f414349ebe29ed26661822d" -dependencies = [ - "bytes", - "libc", - "mio", - "pin-project-lite", - "socket2", - "windows-sys 0.61.2", -] - [[package]] name = "toml" version = "0.8.23" @@ -1970,37 +1446,6 @@ version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801" -[[package]] -name = "tracing" -version = "0.1.44" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" -dependencies = [ - "pin-project-lite", - "tracing-attributes", - "tracing-core", -] - -[[package]] -name = "tracing-attributes" -version = "0.1.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "tracing-core" -version = "0.1.36" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" -dependencies = [ - "once_cell", -] - [[package]] name = "trait-variant" version = "0.1.2" @@ -2048,24 +1493,6 @@ version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" -[[package]] -name = "url" -version = "2.5.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" -dependencies = [ - "form_urlencoded", - "idna", - "percent-encoding", - "serde", -] - -[[package]] -name = "utf8_iter" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" - [[package]] name = "utf8parse" version = "0.2.2" @@ -2096,8 +1523,6 @@ dependencies = [ "clap", "rustyline", "wafer-core", - "wasmtime", - "wasmtime-wasi", ] [[package]] @@ -2113,13 +1538,6 @@ dependencies = [ "wasmtime", ] -[[package]] -name = "wafer-web" -version = "0.1.0" -dependencies = [ - "wafer-core", -] - [[package]] name = "wait-timeout" version = "0.2.1" @@ -2536,50 +1954,6 @@ dependencies = [ "syn", ] -[[package]] -name = "wasmtime-wasi" -version = "31.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b425ede2633fade96bd624b6f35cea5f8be1995d149530882dbc35efbf1e31f" -dependencies = [ - "anyhow", - "async-trait", - "bitflags", - "bytes", - "cap-fs-ext", - "cap-net-ext", - "cap-rand", - "cap-std", - "cap-time-ext", - "fs-set-times", - "futures", - "io-extras", - "io-lifetimes", - "rustix 0.38.44", - "system-interface", - "thiserror 1.0.69", - "tokio", - "tracing", - "url", - "wasmtime", - "wasmtime-wasi-io", - "wiggle", - "windows-sys 0.59.0", -] - -[[package]] -name = "wasmtime-wasi-io" -version = "31.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2ec650d8891ec5ff823bdcefe3b370278becd1f33125bcfdcf628943dcde676" -dependencies = [ - "anyhow", - "async-trait", - "bytes", - "futures", - "wasmtime", -] - [[package]] name = "wasmtime-winch" version = "31.0.0" @@ -2609,15 +1983,6 @@ dependencies = [ "wit-parser 0.226.0", ] -[[package]] -name = "wast" -version = "35.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2ef140f1b49946586078353a453a1d28ba90adfc54dde75710bc1931de204d68" -dependencies = [ - "leb128", -] - [[package]] name = "wast" version = "245.0.1" @@ -2637,49 +2002,7 @@ version = "1.245.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cd48d1679b6858988cb96b154dda0ec5bbb09275b71db46057be37332d5477be" dependencies = [ - "wast 245.0.1", -] - -[[package]] -name = "wiggle" -version = "31.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1dc9a83fe01faa51423fc84941cdbe0ec33ba1e9a75524a560a27a4ad1ff2c3b" -dependencies = [ - "anyhow", - "async-trait", - "bitflags", - "thiserror 1.0.69", - "tracing", - "wasmtime", - "wiggle-macro", -] - -[[package]] -name = "wiggle-generate" -version = "31.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d250c01cd52cfdb40aad167fad579af55acbeccb85a54827099d31dc1b90cbd7" -dependencies = [ - "anyhow", - "heck", - "proc-macro2", - "quote", - "shellexpand", - "syn", - "witx", -] - -[[package]] -name = "wiggle-macro" -version = "31.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "35be0aee84be808a5e17f6b732e110eb75703d9d6e66e22c7464d841aa2600c5" -dependencies = [ - "proc-macro2", - "quote", - "syn", - "wiggle-generate", + "wast", ] [[package]] @@ -2731,65 +2054,12 @@ dependencies = [ "wasmtime-environ", ] -[[package]] -name = "windows-core" -version = "0.62.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" -dependencies = [ - "windows-implement", - "windows-interface", - "windows-link", - "windows-result", - "windows-strings", -] - -[[package]] -name = "windows-implement" -version = "0.60.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "windows-interface" -version = "0.59.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - [[package]] name = "windows-link" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" -[[package]] -name = "windows-result" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" -dependencies = [ - "windows-link", -] - -[[package]] -name = "windows-strings" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" -dependencies = [ - "windows-link", -] - [[package]] name = "windows-sys" version = "0.59.0" @@ -2881,16 +2151,6 @@ dependencies = [ "memchr", ] -[[package]] -name = "winx" -version = "0.36.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f3fd376f71958b862e7afb20cfe5a22830e1963462f3a17f49d82a6c1d1f42d" -dependencies = [ - "bitflags", - "windows-sys 0.59.0", -] - [[package]] name = "wit-bindgen" version = "0.51.0" @@ -2997,47 +2257,6 @@ dependencies = [ "wasmparser 0.244.0", ] -[[package]] -name = "witx" -version = "0.9.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e366f27a5cabcddb2706a78296a40b8fcc451e1a6aba2fc1d94b4a01bdaaef4b" -dependencies = [ - "anyhow", - "log", - "thiserror 1.0.69", - "wast 35.0.2", -] - -[[package]] -name = "writeable" -version = "0.6.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9" - -[[package]] -name = "yoke" -version = "0.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72d6e5c6afb84d73944e5cedb052c4680d5657337201555f9f2a16b7406d4954" -dependencies = [ - "stable_deref_trait", - "yoke-derive", - "zerofrom", -] - -[[package]] -name = "yoke-derive" -version = "0.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b659052874eb698efe5b9e8cf382204678a0086ebf46982b79d6ca3182927e5d" -dependencies = [ - "proc-macro2", - "quote", - "syn", - "synstructure", -] - [[package]] name = "zerocopy" version = "0.8.48" @@ -3058,60 +2277,6 @@ dependencies = [ "syn", ] -[[package]] -name = "zerofrom" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5" -dependencies = [ - "zerofrom-derive", -] - -[[package]] -name = "zerofrom-derive" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" -dependencies = [ - "proc-macro2", - "quote", - "syn", - "synstructure", -] - -[[package]] -name = "zerotrie" -version = "0.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a59c17a5562d507e4b54960e8569ebee33bee890c70aa3fe7b97e85a9fd7851" -dependencies = [ - "displaydoc", - "yoke", - "zerofrom", -] - -[[package]] -name = "zerovec" -version = "0.11.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c28719294829477f525be0186d13efa9a3c602f7ec202ca9e353d310fb9a002" -dependencies = [ - "yoke", - "zerofrom", - "zerovec-derive", -] - -[[package]] -name = "zerovec-derive" -version = "0.11.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eadce39539ca5cb3985590102671f2567e659fca9666581ad3411d59207951f3" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - [[package]] name = "zmij" version = "1.0.21" diff --git a/Cargo.toml b/Cargo.toml index 646182d..413d932 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -44,7 +44,6 @@ or_fun_call = "warn" wasm-encoder = "0.228" wasmparser = "0.228" wasmtime = "31" -wasmtime-wasi = "31" anyhow = "1" thiserror = "2" proptest = "1" diff --git a/crates/cli/Cargo.toml b/crates/cli/Cargo.toml index 7989589..ead7aec 100644 --- a/crates/cli/Cargo.toml +++ b/crates/cli/Cargo.toml @@ -5,16 +5,11 @@ version.workspace = true edition.workspace = true license.workspace = true -[package.metadata.cargo-machete] -ignored = ["wasmtime", "wasmtime-wasi"] - [lints] workspace = true [dependencies] wafer-core = { path = "../core", version = "0.1.0" } -wasmtime = { workspace = true } -wasmtime-wasi = { workspace = true } anyhow = { workspace = true } clap = { version = "4", features = ["derive"] } rustyline = "15" diff --git a/crates/cli/src/main.rs b/crates/cli/src/main.rs index 94b4639..ce26637 100644 --- a/crates/cli/src/main.rs +++ b/crates/cli/src/main.rs @@ -1,31 +1,149 @@ -//! WAFER CLI: Interactive REPL and AOT compiler for WAFER Forth. +//! WAFER CLI: Interactive REPL, AOT compiler, and WASM runner for WAFER Forth. -use clap::Parser; +use std::path::Path; + +use clap::{Parser, Subcommand}; +use wafer_core::export::{ExportConfig, export_module}; use wafer_core::outer::ForthVM; +use wafer_core::runner::run_wasm_file; /// WAFER: WebAssembly Forth Engine in Rust #[derive(Parser, Debug)] #[command(name = "wafer", version, about)] struct Cli { - /// Forth source file to execute + #[command(subcommand)] + command: Option, + + /// Forth source file to execute (when no subcommand is given) file: Option, +} - /// Compile all words into a single optimized WASM module - #[arg(long)] - consolidate: bool, +#[derive(Subcommand, Debug)] +enum Commands { + /// Compile a Forth source file to a standalone WASM module + Build { + /// Input Forth source file + file: String, - /// Output file for consolidated WASM (requires --consolidate) - #[arg(short, long)] - output: Option, + /// Output .wasm file (default: input with .wasm extension) + #[arg(short, long)] + output: Option, + + /// Entry-point word name (default: MAIN, or top-level execution) + #[arg(long)] + entry: Option, + + /// Also generate a JS loader and HTML page for browser execution + #[arg(long)] + js: bool, + }, + + /// Run a pre-compiled WASM module + Run { + /// .wasm file to execute + file: String, + }, } fn main() -> anyhow::Result<()> { let cli = Cli::parse(); + match cli.command { + Some(Commands::Build { + file, + output, + entry, + js, + }) => cmd_build(&file, output.as_deref(), entry, js), + + Some(Commands::Run { file }) => cmd_run(&file), + + None => cmd_eval_or_repl(cli.file.as_deref()), + } +} + +/// `wafer build program.fth -o program.wasm` +fn cmd_build( + file: &str, + output: Option<&str>, + entry: Option, + js: bool, +) -> anyhow::Result<()> { + let source = std::fs::read_to_string(file)?; + + let mut vm = ForthVM::new()?; + vm.set_recording(true); + vm.evaluate(&source)?; + + // Print any side-effect output from evaluation. + let eval_output = vm.take_output(); + if !eval_output.is_empty() { + print!("{eval_output}"); + } + + let config = ExportConfig { entry_word: entry }; + let (wasm_bytes, metadata) = export_module(&mut vm, &config)?; + + // Determine output path. + let out_path = match output { + Some(p) => p.to_string(), + None => { + let stem = Path::new(file) + .file_stem() + .and_then(|s| s.to_str()) + .unwrap_or("out"); + format!("{stem}.wasm") + } + }; + + std::fs::write(&out_path, &wasm_bytes)?; + + let word_count = vm.ir_words().len(); + let host_count = metadata.host_functions.len(); + eprintln!( + "Wrote {out_path} ({} bytes, {word_count} words, {host_count} host functions)", + wasm_bytes.len() + ); + + if js { + let out = Path::new(&out_path); + let wasm_filename = out + .file_name() + .and_then(|s| s.to_str()) + .unwrap_or("out.wasm"); + let stem = out.file_stem().and_then(|s| s.to_str()).unwrap_or("out"); + let dir = out.parent().unwrap_or_else(|| Path::new(".")); + + let js_path = dir.join(format!("{stem}.js")); + let html_path = dir.join(format!("{stem}.html")); + let js_filename = format!("{stem}.js"); + + let js_code = wafer_core::js_loader::generate_js_loader(wasm_filename, &metadata); + let html_code = wafer_core::js_loader::generate_html_page(wasm_filename, &js_filename); + + std::fs::write(&js_path, &js_code)?; + std::fs::write(&html_path, &html_code)?; + eprintln!("Wrote {} and {}", js_path.display(), html_path.display()); + } + + Ok(()) +} + +/// `wafer run program.wasm` +fn cmd_run(file: &str) -> anyhow::Result<()> { + let output = run_wasm_file(file)?; + if !output.is_empty() { + print!("{output}"); + } + Ok(()) +} + +/// `wafer` (REPL) or `wafer program.fth` (evaluate and exit) +fn cmd_eval_or_repl(file: Option<&str>) -> anyhow::Result<()> { let mut vm = ForthVM::new()?; - match cli.file { - Some(ref file) => { + match file { + Some(file) => { let source = std::fs::read_to_string(file)?; vm.evaluate(&source)?; let output = vm.take_output(); @@ -34,12 +152,10 @@ fn main() -> anyhow::Result<()> { } } None => { - // Check if stdin is a pipe (not a TTY) - if !atty_is_tty() { + if !stdin_is_tty() { // Non-interactive: read all of stdin and evaluate let mut input = String::new(); std::io::Read::read_to_string(&mut std::io::stdin(), &mut input)?; - // Evaluate line-by-line to handle multi-line input for line in input.lines() { match vm.evaluate(line) { Ok(()) => { @@ -106,7 +222,7 @@ fn main() -> anyhow::Result<()> { } /// Check if stdin is a terminal (TTY). -fn atty_is_tty() -> bool { +fn stdin_is_tty() -> bool { use std::io::IsTerminal; std::io::stdin().is_terminal() } diff --git a/crates/core/src/codegen.rs b/crates/core/src/codegen.rs index 873da2e..df25047 100644 --- a/crates/core/src/codegen.rs +++ b/crates/core/src/codegen.rs @@ -10,9 +10,10 @@ use std::borrow::Cow; use std::collections::HashMap; use wasm_encoder::{ - BlockType, CodeSection, ConstExpr, ElementSection, Elements, EntityType, ExportKind, - ExportSection, Function, FunctionSection, GlobalType, ImportSection, Instruction, MemArg, - MemoryType, Module, RefType, TableType, TypeSection, ValType, + BlockType, CodeSection, ConstExpr, CustomSection, DataCountSection, DataSection, + ElementSection, Elements, EntityType, ExportKind, ExportSection, Function, FunctionSection, + GlobalType, ImportSection, Instruction, MemArg, MemoryType, Module, RefType, TableType, + TypeSection, ValType, }; use crate::dictionary::WordId; @@ -2062,25 +2063,50 @@ fn emit_consolidated_do_loop( f.instruction(&Instruction::Drop); } -/// Compile all given words into a single consolidated WASM module. +/// Optional extras for exportable modules (data section, entry point, metadata). +pub struct ExportSections<'a> { + /// Memory snapshot to embed as a WASM data section. + pub memory_snapshot: &'a [u8], + /// If set, export this function index as `_start`. + pub entry_fn_index: Option, + /// JSON metadata to embed as a custom "wafer" section. + pub metadata_json: &'a [u8], +} + +/// Compile multiple IR-based words into a single WASM module with direct calls. /// -/// Each word becomes a function in the module. Calls between words within the -/// module use direct `call` instructions instead of `call_indirect` through the -/// function table, enabling Cranelift to inline and optimize across word -/// boundaries. -/// -/// # Arguments -/// -/// * `words` - Words to consolidate, sorted by `WordId`. Each entry is -/// `(WordId, Vec)` containing the word's IR body. -/// * `local_fn_map` - Maps each `WordId` in the module to its WASM function -/// index (imported functions come first, so defined functions start at 1). -/// * `table_size` - Current function table size, used for table import minimum. +/// Used at runtime by `CONSOLIDATE` and during startup batch compilation. pub fn compile_consolidated_module( words: &[(WordId, Vec)], local_fn_map: &HashMap, table_size: u32, ) -> WaferResult> { + compile_multi_word_module(words, local_fn_map, table_size, None) +} + +/// Compile an exportable WASM module with embedded memory and metadata. +/// +/// Same as [`compile_consolidated_module`] but adds a WASM data section +/// (memory snapshot), an optional `_start` entry point export, and a +/// custom "wafer" section with JSON metadata. +pub fn compile_exportable_module( + words: &[(WordId, Vec)], + local_fn_map: &HashMap, + table_size: u32, + export: &ExportSections<'_>, +) -> WaferResult> { + compile_multi_word_module(words, local_fn_map, table_size, Some(export)) +} + +/// Internal: build a multi-word WASM module. When `export` is `Some`, adds +/// data section, entry-point export, and custom metadata section. +fn compile_multi_word_module( + words: &[(WordId, Vec)], + local_fn_map: &HashMap, + table_size: u32, + export: Option<&ExportSections<'_>>, +) -> WaferResult> { + let has_data = export.is_some_and(|e| !e.memory_snapshot.is_empty()); let mut module = Module::new(); // -- Type section -- @@ -2157,10 +2183,15 @@ pub fn compile_consolidated_module( // +1 because emit is imported function index 0 exports.export(&name, ExportKind::Func, (i as u32) + 1); } + // Optionally export an entry point as "_start" + if let Some(e) = export + && let Some(fn_idx) = e.entry_fn_index + { + exports.export("_start", ExportKind::Func, fn_idx); + } module.section(&exports); // -- Element section: place each function in the table at its WordId slot -- - // Use a single element section with one active segment per word. let mut elements = ElementSection::new(); for (i, (word_id, _)) in words.iter().enumerate() { let offset = ConstExpr::i32_const(word_id.0 as i32); @@ -2174,6 +2205,11 @@ pub fn compile_consolidated_module( } module.section(&elements); + // -- DataCount section (required before Code when Data section is present) -- + if has_data { + module.section(&DataCountSection { count: 1 }); + } + // -- Code section: emit each function body -- let mut code = CodeSection::new(); for (_word_id, body) in words { @@ -2206,12 +2242,34 @@ pub fn compile_consolidated_module( } module.section(&code); + // -- Data section (memory snapshot for exportable modules) -- + if let Some(e) = export + && !e.memory_snapshot.is_empty() + { + let mut data = DataSection::new(); + data.active( + MEMORY_INDEX, + &ConstExpr::i32_const(0), + e.memory_snapshot.iter().copied(), + ); + module.section(&data); + } + + // -- Custom "wafer" section (metadata for exportable modules) -- + if let Some(e) = export + && !e.metadata_json.is_empty() + { + module.section(&CustomSection { + name: Cow::Borrowed("wafer"), + data: Cow::Borrowed(e.metadata_json), + }); + } + let bytes = module.finish(); // Validate - wasmparser::validate(&bytes).map_err(|e| { - WaferError::ValidationError(format!("Consolidated WASM failed validation: {e}")) - })?; + wasmparser::validate(&bytes) + .map_err(|e| WaferError::ValidationError(format!("WASM module failed validation: {e}")))?; Ok(bytes) } diff --git a/crates/core/src/export.rs b/crates/core/src/export.rs new file mode 100644 index 0000000..1a5ccd2 --- /dev/null +++ b/crates/core/src/export.rs @@ -0,0 +1,409 @@ +//! WASM module export: compile a Forth session to a standalone `.wasm` file. +//! +//! Orchestrates the export pipeline: collect IR words, resolve the entry point, +//! snapshot WASM memory, build metadata, and call the exportable codegen. + +use std::collections::{HashMap, HashSet}; +use std::fmt::Write; + +use crate::codegen::{ExportSections, compile_exportable_module}; +use crate::dictionary::WordId; +use crate::ir::IrOp; +use crate::outer::ForthVM; + +/// Configuration for `wafer build`. +pub struct ExportConfig { + /// Explicit entry-point word name (from `--entry` flag). + pub entry_word: Option, +} + +/// Metadata embedded in the "wafer" custom section of exported modules. +pub struct ExportMetadata { + /// Format version (currently 1). + pub version: u32, + /// Table index of the entry-point function, if any. + pub entry_table_index: Option, + /// Host functions referenced by consolidated code: (`table_index`, name). + pub host_functions: Vec<(u32, String)>, + /// Number of memory bytes in the data section snapshot. + pub memory_size: u32, + /// Initial data-stack pointer. + pub dsp_init: u32, + /// Initial return-stack pointer. + pub rsp_init: u32, + /// Initial float-stack pointer. + pub fsp_init: u32, +} + +/// Export the current VM state as a standalone WASM module. +/// +/// Returns the raw `.wasm` bytes ready to write to a file, plus the metadata. +pub fn export_module( + vm: &mut ForthVM, + config: &ExportConfig, +) -> anyhow::Result<(Vec, ExportMetadata)> { + let mut words = vm.ir_words(); + + // Determine the entry point. + // Priority: --entry flag > MAIN word > recorded top-level execution. + let toplevel = vm.toplevel_ir(); + let entry_word_id = if let Some(ref name) = config.entry_word { + Some( + vm.resolve_word(name) + .ok_or_else(|| anyhow::anyhow!("entry word '{name}' not found"))?, + ) + } else if let Some(main_id) = vm.resolve_word("MAIN") { + Some(main_id) + } else if !toplevel.is_empty() { + // Synthesize a _start word from recorded top-level execution. + // Pick a WordId that won't collide (one past the current table size). + let start_id = WordId(vm.current_table_size()); + words.push((start_id, toplevel.to_vec())); + Some(start_id) + } else { + None + }; + + if words.is_empty() { + anyhow::bail!("nothing to export: no compiled words found"); + } + + // Build local_fn_map: WordId -> module-internal function index. + // Imported functions occupy index 0 (emit), so defined functions start at 1. + let mut local_fn_map = HashMap::new(); + for (i, (word_id, _)) in words.iter().enumerate() { + local_fn_map.insert(*word_id, (i as u32) + 1); + } + + // Resolve entry function index within the module. + let entry_fn_index = entry_word_id.and_then(|id| local_fn_map.get(&id).copied()); + + // Snapshot memory (system variables + user data). + let memory_snapshot = vm.memory_snapshot(); + + // Table size: must accommodate all WordIds including the synthetic _start. + let max_word_id = words.iter().map(|(id, _)| id.0).max().unwrap_or(0); + let table_size = (max_word_id + 1).max(vm.current_table_size()); + + // Find host functions referenced by any consolidated word. + let ir_word_ids: HashSet = words.iter().map(|(id, _)| *id).collect(); + let mut referenced_host_ids: HashSet = HashSet::new(); + for (_, body) in &words { + collect_external_calls(body, &ir_word_ids, &mut referenced_host_ids); + } + + let host_names = vm.host_function_names(); + let mut host_functions: Vec<(u32, String)> = referenced_host_ids + .iter() + .filter_map(|id| host_names.get(id).map(|name| (id.0, name.clone()))) + .collect(); + host_functions.sort_by_key(|(idx, _)| *idx); + + let (dsp_init, rsp_init, fsp_init) = vm.stack_pointer_inits(); + + let metadata = ExportMetadata { + version: 1, + entry_table_index: entry_word_id.map(|id| id.0), + host_functions, + memory_size: memory_snapshot.len() as u32, + dsp_init, + rsp_init, + fsp_init, + }; + + let metadata_json = serialize_metadata(&metadata); + + let export_sections = ExportSections { + memory_snapshot: &memory_snapshot, + entry_fn_index, + 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}"))?; + + Ok((wasm_bytes, metadata)) +} + +/// Recursively collect `Call`/`TailCall` targets that are NOT in the IR word set +/// (i.e., they are host functions that the runner must provide). +fn collect_external_calls(ops: &[IrOp], ir_ids: &HashSet, host_ids: &mut HashSet) { + for op in ops { + match op { + IrOp::Call(id) | IrOp::TailCall(id) => { + if !ir_ids.contains(id) { + host_ids.insert(*id); + } + } + IrOp::If { + then_body, + else_body, + } => { + collect_external_calls(then_body, ir_ids, host_ids); + if let Some(eb) = else_body { + collect_external_calls(eb, ir_ids, host_ids); + } + } + IrOp::DoLoop { body, .. } | IrOp::BeginUntil { body } | IrOp::BeginAgain { body } => { + collect_external_calls(body, ir_ids, host_ids); + } + IrOp::BeginWhileRepeat { test, body } => { + collect_external_calls(test, ir_ids, host_ids); + collect_external_calls(body, ir_ids, host_ids); + } + IrOp::BeginDoubleWhileRepeat { + outer_test, + inner_test, + body, + after_repeat, + else_body, + } => { + collect_external_calls(outer_test, ir_ids, host_ids); + collect_external_calls(inner_test, ir_ids, host_ids); + collect_external_calls(body, ir_ids, host_ids); + collect_external_calls(after_repeat, ir_ids, host_ids); + if let Some(eb) = else_body { + collect_external_calls(eb, ir_ids, host_ids); + } + } + _ => {} + } + } +} + +/// Serialize export metadata to JSON (hand-rolled, no serde dependency). +fn serialize_metadata(m: &ExportMetadata) -> String { + let mut s = String::from("{\n"); + let _ = writeln!(s, " \"version\": {},", m.version); + match m.entry_table_index { + Some(idx) => { + let _ = writeln!(s, " \"entry_table_index\": {idx},"); + } + None => { + let _ = writeln!(s, " \"entry_table_index\": null,"); + } + } + let _ = writeln!(s, " \"memory_size\": {},", m.memory_size); + let _ = writeln!(s, " \"dsp_init\": {},", m.dsp_init); + let _ = writeln!(s, " \"rsp_init\": {},", m.rsp_init); + let _ = writeln!(s, " \"fsp_init\": {},", m.fsp_init); + let _ = write!(s, " \"host_functions\": ["); + for (i, (idx, name)) in m.host_functions.iter().enumerate() { + if i > 0 { + let _ = write!(s, ", "); + } + // Escape any quotes in the name (unlikely but safe). + let escaped: String = name + .chars() + .flat_map(|c| if c == '"' { vec!['\\', '"'] } else { vec![c] }) + .collect(); + let _ = write!(s, "{{\"index\": {idx}, \"name\": \"{escaped}\"}}"); + } + let _ = writeln!(s, "]"); + s.push('}'); + s +} + +/// Deserialize export metadata from JSON (minimal parser for our known format). +pub fn deserialize_metadata(json: &str) -> anyhow::Result { + // Simple extraction by key -- works for our flat JSON structure. + let get_u32 = |key: &str| -> anyhow::Result { + let pat = format!("\"{key}\": "); + let start = json + .find(&pat) + .ok_or_else(|| anyhow::anyhow!("missing key: {key}"))? + + pat.len(); + let end = json[start..] + .find([',', '\n', '}']) + .map_or(json.len(), |i| start + i); + json[start..end] + .trim() + .parse() + .map_err(|e| anyhow::anyhow!("bad {key}: {e}")) + }; + + let get_optional_u32 = |key: &str| -> anyhow::Result> { + let pat = format!("\"{key}\": "); + let Some(pos) = json.find(&pat) else { + return Ok(None); + }; + let start = pos + pat.len(); + let end = json[start..] + .find([',', '\n', '}']) + .map_or(json.len(), |i| start + i); + let val = json[start..end].trim(); + if val == "null" { + return Ok(None); + } + val.parse() + .map(Some) + .map_err(|e| anyhow::anyhow!("bad {key}: {e}")) + }; + + // Parse host_functions array + let mut host_functions = Vec::new(); + if let Some(arr_start) = json.find("\"host_functions\": [") { + let arr_start = arr_start + "\"host_functions\": [".len(); + let arr_end = json[arr_start..] + .find(']') + .map_or(json.len(), |i| arr_start + i); + let arr = &json[arr_start..arr_end]; + + // Parse each {"index": N, "name": "X"} object + let mut pos = 0; + while pos < arr.len() { + if let Some(obj_start) = arr[pos..].find('{') { + let obj_start = pos + obj_start; + if let Some(obj_end) = arr[obj_start..].find('}') { + let obj = &arr[obj_start..obj_start + obj_end + 1]; + + // Extract index + if let Some(idx_start) = obj.find("\"index\": ") { + let idx_start = idx_start + "\"index\": ".len(); + let idx_end = obj[idx_start..] + .find([',', '}']) + .map_or(obj.len(), |i| idx_start + i); + let idx: u32 = obj[idx_start..idx_end].trim().parse().unwrap_or(0); + + // Extract name + if let Some(name_start) = obj.find("\"name\": \"") { + let name_start = name_start + "\"name\": \"".len(); + if let Some(name_end) = obj[name_start..].find('"') { + let name = obj[name_start..name_start + name_end].to_string(); + host_functions.push((idx, name)); + } + } + } + pos = obj_start + obj_end + 1; + } else { + break; + } + } else { + break; + } + } + } + + Ok(ExportMetadata { + version: get_u32("version")?, + entry_table_index: get_optional_u32("entry_table_index")?, + host_functions, + memory_size: get_u32("memory_size")?, + dsp_init: get_u32("dsp_init")?, + rsp_init: get_u32("rsp_init")?, + fsp_init: get_u32("fsp_init")?, + }) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn metadata_roundtrip() { + let m = ExportMetadata { + version: 1, + entry_table_index: Some(42), + host_functions: vec![(5, ".".to_string()), (12, "TYPE".to_string())], + memory_size: 65536, + dsp_init: 5440, + rsp_init: 9536, + fsp_init: 11584, + }; + let json = serialize_metadata(&m); + let m2 = deserialize_metadata(&json).unwrap(); + assert_eq!(m2.version, 1); + assert_eq!(m2.entry_table_index, Some(42)); + assert_eq!(m2.host_functions.len(), 2); + assert_eq!(m2.host_functions[0], (5, ".".to_string())); + assert_eq!(m2.host_functions[1], (12, "TYPE".to_string())); + assert_eq!(m2.memory_size, 65536); + assert_eq!(m2.dsp_init, 5440); + } + + #[test] + fn metadata_null_entry() { + let m = ExportMetadata { + version: 1, + entry_table_index: None, + host_functions: vec![], + memory_size: 1024, + dsp_init: 5440, + rsp_init: 9536, + fsp_init: 11584, + }; + let json = serialize_metadata(&m); + assert!(json.contains("\"entry_table_index\": null")); + let m2 = deserialize_metadata(&json).unwrap(); + assert_eq!(m2.entry_table_index, None); + assert!(m2.host_functions.is_empty()); + } + + #[test] + fn collect_calls_finds_host_functions() { + let ir_ids: HashSet = [WordId(1), WordId(2)].iter().copied().collect(); + let body = vec![ + IrOp::Call(WordId(1)), // IR word, not host + IrOp::Call(WordId(99)), // host function + IrOp::If { + then_body: vec![IrOp::Call(WordId(50))], // host in nested body + else_body: None, + }, + ]; + let mut host = HashSet::new(); + collect_external_calls(&body, &ir_ids, &mut host); + assert!(host.contains(&WordId(99))); + assert!(host.contains(&WordId(50))); + assert!(!host.contains(&WordId(1))); + } + + /// Helper: evaluate Forth code, export to WASM, run, and return the output. + fn roundtrip(source: &str) -> String { + use crate::outer::ForthVM; + use crate::runner::run_wasm_bytes; + + let mut vm = ForthVM::new().unwrap(); + vm.set_recording(true); + vm.evaluate(source).unwrap(); + + let config = ExportConfig { entry_word: None }; + let (wasm_bytes, _metadata) = export_module(&mut vm, &config).unwrap(); + run_wasm_bytes(&wasm_bytes).unwrap() + } + + #[test] + fn roundtrip_simple_dot() { + assert_eq!(roundtrip(": main 42 . ;"), "42 "); + } + + #[test] + fn roundtrip_multiple_words() { + assert_eq!(roundtrip(": double 2 * ; : main 21 double . ;"), "42 "); + } + + #[test] + fn roundtrip_variable() { + assert_eq!(roundtrip("VARIABLE X 99 X ! : main X @ . ;"), "99 "); + } + + #[test] + fn roundtrip_emit() { + assert_eq!(roundtrip(": main 72 EMIT 73 EMIT 10 EMIT ;"), "HI\n"); + } + + #[test] + fn roundtrip_constant() { + assert_eq!(roundtrip("42 CONSTANT ANSWER : main ANSWER . ;"), "42 "); + } + + #[test] + fn roundtrip_toplevel_execution() { + // No MAIN: top-level calls become the entry point. + assert_eq!(roundtrip(": hello 42 . ; hello"), "42 "); + } + + #[test] + fn roundtrip_control_flow() { + assert_eq!(roundtrip(": main 1 IF 42 ELSE 0 THEN . ;"), "42 "); + } +} diff --git a/crates/core/src/js_loader.rs b/crates/core/src/js_loader.rs new file mode 100644 index 0000000..fbdcec1 --- /dev/null +++ b/crates/core/src/js_loader.rs @@ -0,0 +1,163 @@ +//! Generate JavaScript and HTML loaders for running exported WASM in the browser. + +use crate::export::ExportMetadata; + +/// Generate a JavaScript loader that instantiates a WAFER `.wasm` module. +/// +/// The loader provides the six required imports (emit, memory, dsp, rsp, fsp, +/// table) and host-function stubs, then calls `_start`. +pub fn generate_js_loader(wasm_filename: &str, metadata: &ExportMetadata) -> String { + let (dsp, rsp, fsp) = (metadata.dsp_init, metadata.rsp_init, metadata.fsp_init); + let memory_pages = metadata.memory_size.div_ceil(65536).max(16); + + // Build the host function registration code. + let mut host_registrations = String::new(); + for (idx, name) in &metadata.host_functions { + let js_impl = js_host_function(name); + host_registrations.push_str(&format!(" table.set({idx}, {js_impl});\n")); + } + + format!( + r#"// WAFER JS Loader - generated by wafer build --js +// Loads and runs {wasm_filename} in the browser. + +const WAFER = (() => {{ + const CELL_SIZE = 4; + const DATA_STACK_TOP = 0x1540; + const SYSVAR_BASE = 0x0004; + let outputCallback = (s) => {{ + const el = document.getElementById('output'); + if (el) el.textContent += s; + else console.log(s); + }}; + + async function run(opts) {{ + if (opts && opts.output) outputCallback = opts.output; + + const memory = new WebAssembly.Memory({{ initial: {memory_pages} }}); + const dsp = new WebAssembly.Global({{ value: 'i32', mutable: true }}, {dsp}); + const rsp = new WebAssembly.Global({{ value: 'i32', mutable: true }}, {rsp}); + const fsp = new WebAssembly.Global({{ value: 'i32', mutable: true }}, {fsp}); + const table = new WebAssembly.Table({{ element: 'anyfunc', initial: 256 }}); + + function emit(code) {{ + outputCallback(String.fromCharCode(code)); + }} + + const importObject = {{ + env: {{ emit, memory, dsp, rsp, fsp, table }} + }}; + + // Register host functions + const view = () => new DataView(memory.buffer); + const pop = () => {{ + const sp = dsp.value; + const v = view().getInt32(sp, true); + dsp.value = sp + CELL_SIZE; + return v; + }}; + const push = (v) => {{ + const sp = dsp.value - CELL_SIZE; + view().setInt32(sp, v, true); + dsp.value = sp; + }}; + +{host_registrations} + const response = await fetch('{wasm_filename}'); + const bytes = await response.arrayBuffer(); + const {{ instance }} = await WebAssembly.instantiate(bytes, importObject); + + if (instance.exports._start) {{ + instance.exports._start(); + }} + + return instance; + }} + + return {{ run }}; +}})(); +"# + ) +} + +/// Generate a minimal HTML page that loads the JS loader. +pub fn generate_html_page(wasm_filename: &str, js_filename: &str) -> String { + format!( + r#" + + + + WAFER - {wasm_filename} + + + +

WAFER Output

+
+ + + + +"# + ) +} + +/// Return a JS expression that creates a `WebAssembly.Function` for a known +/// host word. Falls back to a stub that logs an error. +fn js_host_function(name: &str) -> &'static str { + match name { + "." => { + r#"new WebAssembly.Function({parameters:[], results:[]}, () => { + const n = pop(); + const base = view().getUint32(SYSVAR_BASE, true); + outputCallback((base === 16 ? n.toString(16).toUpperCase() : n.toString()) + ' '); + })"# + } + "U." => { + r#"new WebAssembly.Function({parameters:[], results:[]}, () => { + const n = pop() >>> 0; + const base = view().getUint32(SYSVAR_BASE, true); + outputCallback((base === 16 ? n.toString(16).toUpperCase() : n.toString()) + ' '); + })"# + } + "TYPE" => { + r#"new WebAssembly.Function({parameters:[], results:[]}, () => { + const len = pop(); + const addr = pop(); + const bytes = new Uint8Array(memory.buffer, addr, len); + outputCallback(new TextDecoder().decode(bytes)); + })"# + } + "SPACES" => { + r#"new WebAssembly.Function({parameters:[], results:[]}, () => { + const n = pop(); + if (n > 0) outputCallback(' '.repeat(n)); + })"# + } + ".S" => { + r#"new WebAssembly.Function({parameters:[], results:[]}, () => { + const sp = dsp.value; + const depth = (DATA_STACK_TOP - sp) / CELL_SIZE; + let s = '<' + depth + '> '; + for (let a = DATA_STACK_TOP - CELL_SIZE; a >= sp; a -= CELL_SIZE) { + s += view().getInt32(a, true) + ' '; + } + outputCallback(s); + })"# + } + "DEPTH" => { + r#"new WebAssembly.Function({parameters:[], results:[]}, () => { + const depth = (DATA_STACK_TOP - dsp.value) / CELL_SIZE; + push(depth); + })"# + } + _ => { + r#"new WebAssembly.Function({parameters:[], results:[]}, () => { + console.error('Host function not available in standalone mode'); + })"# + } + } +} diff --git a/crates/core/src/lib.rs b/crates/core/src/lib.rs index 5726f21..8c09ccb 100644 --- a/crates/core/src/lib.rs +++ b/crates/core/src/lib.rs @@ -19,7 +19,10 @@ pub mod config; pub mod consolidate; pub mod dictionary; pub mod error; +pub mod export; pub mod ir; +pub mod js_loader; pub mod memory; pub mod optimizer; pub mod outer; +pub mod runner; diff --git a/crates/core/src/outer.rs b/crates/core/src/outer.rs index b2da080..b9dc0e4 100644 --- a/crates/core/src/outer.rs +++ b/crates/core/src/outer.rs @@ -194,9 +194,8 @@ pub struct ForthVM { next_table_index: u32, // The emit function (shared across all instantiated modules) emit_func: Func, - // Dot (print number) function -- kept for potential future use - #[allow(dead_code)] - dot_func: Func, + // Map from WordId to name for host-function words (for export metadata). + host_word_names: HashMap, // Shared HERE value for host functions (synced with user_here) here_cell: Option>>, // User data allocation pointer in WASM linear memory. @@ -241,6 +240,10 @@ pub struct ForthVM { batch_mode: bool, /// IR primitives deferred during `batch_mode` for single-module compilation. deferred_ir: Vec<(WordId, Vec)>, + /// Recorded top-level IR from interpretation mode (for `wafer build`). + toplevel_ir: Vec, + /// When true, interpretation-mode execution is recorded into `toplevel_ir`. + recording_toplevel: bool, } impl ForthVM { @@ -306,21 +309,6 @@ impl ForthVM { }, ); - // Create dot host function: (i32) -> () - // This is used to implement `.` -- it pops TOS and prints it. - // We create a host function that takes i32, converts to string, appends to output. - let out_ref2 = Arc::clone(&output); - let dot_func = Func::new( - &mut store, - FuncType::new(&engine, [ValType::I32], []), - move |_caller, params, _results| { - let n = params[0].unwrap_i32(); - let s = format!("{n} "); - out_ref2.lock().unwrap().push_str(&s); - Ok(()) - }, - ); - let dictionary = Dictionary::new(); let mut vm = ForthVM { @@ -343,7 +331,7 @@ impl ForthVM { output, next_table_index: 0, emit_func, - dot_func, + host_word_names: HashMap::new(), here_cell: None, // User data starts at 64K in WASM memory, well clear of all system regions user_here: 0x10000, @@ -366,6 +354,8 @@ impl ForthVM { total_module_bytes: 0, batch_mode: false, deferred_ir: Vec::new(), + toplevel_ir: Vec::new(), + recording_toplevel: false, }; vm.register_primitives()?; @@ -447,6 +437,69 @@ impl ForthVM { self.total_module_bytes } + // ----------------------------------------------------------------------- + // Export support: public accessors for `wafer build` + // ----------------------------------------------------------------------- + + /// Enable or disable top-level execution recording. + /// + /// When enabled, interpretation-mode word calls and literal pushes are + /// captured into an IR body that becomes the `_start` entry point in + /// exported WASM modules. + pub fn set_recording(&mut self, on: bool) { + self.recording_toplevel = on; + } + + /// Return the recorded top-level IR (empty if recording was not enabled). + pub fn toplevel_ir(&self) -> &[IrOp] { + &self.toplevel_ir + } + + /// Snapshot WASM linear memory from byte 0 through `user_here`. + /// + /// The returned bytes contain system variables, stack regions, and all + /// user-allocated data (VARIABLEs, strings, etc.). This becomes the + /// WASM data section in exported modules. + pub fn memory_snapshot(&mut self) -> Vec { + self.refresh_user_here(); + let data = self.memory.data(&self.store); + let end = self.user_here as usize; + data[..end].to_vec() + } + + /// Return all IR-based word bodies, sorted by `WordId`. + pub fn ir_words(&self) -> Vec<(WordId, Vec)> { + let mut words: Vec<(WordId, Vec)> = self + .ir_bodies + .iter() + .map(|(&id, body)| (id, body.clone())) + .collect(); + words.sort_by_key(|(id, _)| id.0); + words + } + + /// Map of host-function `WordId`s to their Forth names. + pub fn host_function_names(&self) -> &HashMap { + &self.host_word_names + } + + /// Resolve a word name to its `WordId`. Returns `None` if not found. + pub fn resolve_word(&self, name: &str) -> Option { + self.dictionary + .find(&name.to_ascii_uppercase()) + .map(|(_, id, _)| id) + } + + /// Current function table size. + pub fn current_table_size(&self) -> u32 { + self.table.size(&self.store) as u32 + } + + /// Initial stack pointer values: (dsp, rsp, fsp). + pub fn stack_pointer_inits(&self) -> (u32, u32, u32) { + (DATA_STACK_TOP, RETURN_STACK_TOP, FLOAT_STACK_TOP) + } + // ----------------------------------------------------------------------- // Internal: tokenizer // ----------------------------------------------------------------------- @@ -674,6 +727,9 @@ impl ForthVM { return self.execute_does_defining(word_id); } self.execute_word(word_id)?; + if self.recording_toplevel && self.state == 0 { + self.toplevel_ir.push(IrOp::Call(word_id)); + } return Ok(()); } @@ -681,18 +737,28 @@ impl ForthVM { if let Some((lo, hi)) = self.parse_double_number(token) { self.push_data_stack(lo)?; self.push_data_stack(hi)?; + if self.recording_toplevel && self.state == 0 { + self.toplevel_ir.push(IrOp::PushI32(lo)); + self.toplevel_ir.push(IrOp::PushI32(hi)); + } return Ok(()); } // Try to parse as number if let Some(n) = self.parse_number(token) { self.push_data_stack(n)?; + if self.recording_toplevel && self.state == 0 { + self.toplevel_ir.push(IrOp::PushI32(n)); + } return Ok(()); } // Try to parse as float literal (contains 'E' or 'e') if let Some(f) = self.parse_float_literal(token) { self.fpush(f)?; + if self.recording_toplevel && self.state == 0 { + self.toplevel_ir.push(IrOp::PushF64(f)); + } return Ok(()); } @@ -1949,6 +2015,8 @@ impl ForthVM { self.dictionary.reveal(); self.sync_word_lookup(name, word_id, immediate); self.next_table_index = self.next_table_index.max(word_id.0 + 1); + self.host_word_names + .insert(word_id, name.to_ascii_uppercase()); Ok(word_id) } @@ -2260,19 +2328,18 @@ impl ForthVM { &mut self.store, FuncType::new(&self.engine, [], []), move |mut caller, _params, _results| { - // Read top of data stack let sp = dsp.get(&mut caller).unwrap_i32() as u32; + if sp >= DATA_STACK_TOP { + return Err(wasmtime::Error::msg("stack underflow")); + } let data = memory.data(&caller); let b: [u8; 4] = data[sp as usize..sp as usize + 4].try_into().unwrap(); let value = i32::from_le_bytes(b); - // Read BASE from WASM memory let b: [u8; 4] = data[SYSVAR_BASE_VAR as usize..SYSVAR_BASE_VAR as usize + 4] .try_into() .unwrap(); let base_val = u32::from_le_bytes(b); - // Increment dsp (pop) dsp.set(&mut caller, Val::I32((sp + CELL_SIZE) as i32))?; - // Format number in current base let s = format_signed(value, base_val); output.lock().unwrap().push_str(&s); Ok(()) @@ -2294,9 +2361,13 @@ impl ForthVM { FuncType::new(&self.engine, [], []), move |mut caller, _params, _results| { let sp = dsp.get(&mut caller).unwrap_i32() as u32; + let mut out = output.lock().unwrap(); + if sp >= DATA_STACK_TOP { + out.push_str("<0> "); + return Ok(()); + } let data = memory.data(&caller); let depth = (DATA_STACK_TOP - sp) / CELL_SIZE; - let mut out = output.lock().unwrap(); out.push_str(&format!("<{depth}> ")); // Print from bottom to top let mut addr = DATA_STACK_TOP - CELL_SIZE; @@ -2449,6 +2520,7 @@ 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(), @@ -2480,6 +2552,7 @@ 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(), @@ -2516,6 +2589,7 @@ 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(), @@ -2562,6 +2636,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(), @@ -2606,6 +2681,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(), @@ -2644,6 +2720,7 @@ 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(), @@ -2676,6 +2753,7 @@ impl ForthVM { // Stub: marker word does nothing when executed let ir_body = vec![]; + self.ir_bodies.insert(word_id, ir_body.clone()); let config = CodegenConfig { base_fn_index: word_id.0, table_size: self.table_size(), @@ -4146,6 +4224,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(), @@ -6674,6 +6753,7 @@ impl ForthVM { self.dictionary.reveal(); 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(), @@ -6704,6 +6784,7 @@ impl ForthVM { self.dictionary.reveal(); 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(), @@ -6747,6 +6828,7 @@ impl ForthVM { IrOp::PushI32((addr + 4) as i32), IrOp::Fetch, ]; + self.ir_bodies.insert(word_id, ir.clone()); let config = CodegenConfig { base_fn_index: word_id.0, table_size: self.table_size(), @@ -8074,6 +8156,7 @@ 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(), diff --git a/crates/core/src/runner.rs b/crates/core/src/runner.rs new file mode 100644 index 0000000..5dac34d --- /dev/null +++ b/crates/core/src/runner.rs @@ -0,0 +1,304 @@ +//! WASM runner: execute a pre-compiled `.wasm` module produced by `wafer build`. +//! +//! Provides the six imports the module expects (emit, memory, dsp, rsp, fsp, +//! table) and registers host-function stubs for known Forth words. + +use std::sync::{Arc, Mutex}; + +use wasmtime::{ + Engine, Func, FuncType, Global, GlobalType, Memory, MemoryType, Module, Ref, Store, Table, + TableType, Val, ValType, +}; + +use crate::export::deserialize_metadata; +use crate::memory::{CELL_SIZE, DATA_STACK_TOP, SYSVAR_BASE_VAR}; + +/// Host state for the runner (currently unused by wasmtime `Store` but +/// required as the generic parameter). +struct RunnerHost {} + +/// Execute a pre-compiled `.wasm` module and return its output. +pub fn run_wasm_file(path: &str) -> anyhow::Result { + let wasm_bytes = std::fs::read(path)?; + run_wasm_bytes(&wasm_bytes) +} + +/// Execute WASM bytes directly (used by tests and the CLI). +pub fn run_wasm_bytes(wasm_bytes: &[u8]) -> anyhow::Result { + // Parse the "wafer" custom section for metadata. + let metadata_json = extract_custom_section(wasm_bytes, "wafer")?; + let metadata = deserialize_metadata(&metadata_json)?; + + // Set up wasmtime runtime. + let mut config = wasmtime::Config::new(); + config.cranelift_nan_canonicalization(false); + let engine = Engine::new(&config)?; + + let output = Arc::new(Mutex::new(String::new())); + let mut store = Store::new(&engine, RunnerHost {}); + + // Create the 6 imports the module expects. + let memory_pages = metadata.memory_size.div_ceil(65536).max(16); // at least 16 pages like the VM + let memory = Memory::new(&mut store, MemoryType::new(memory_pages, None))?; + + let dsp = Global::new( + &mut store, + GlobalType::new(ValType::I32, wasmtime::Mutability::Var), + Val::I32(metadata.dsp_init as i32), + )?; + let rsp = Global::new( + &mut store, + GlobalType::new(ValType::I32, wasmtime::Mutability::Var), + Val::I32(metadata.rsp_init as i32), + )?; + let fsp = Global::new( + &mut store, + GlobalType::new(ValType::I32, wasmtime::Mutability::Var), + Val::I32(metadata.fsp_init as i32), + )?; + + // Determine table size from the module's import. + let parsed = wasmparser::Parser::new(0).parse_all(wasm_bytes); + let mut table_min: u64 = 256; + for payload in parsed { + if let wasmparser::Payload::ImportSection(reader) = payload? { + for import in reader { + let import = import?; + if import.name == "table" + && let wasmparser::TypeRef::Table(t) = import.ty + { + table_min = t.initial; + } + } + } + } + let table = Table::new( + &mut store, + TableType::new(wasmtime::RefType::FUNCREF, table_min as u32, None), + Ref::Func(None), + )?; + + // Create the emit function. + let out_ref = Arc::clone(&output); + let emit_func = Func::new( + &mut store, + FuncType::new(&engine, [ValType::I32], []), + move |_caller, params, _results| { + let code = params[0].unwrap_i32(); + if let Some(ch) = char::from_u32(code as u32) { + out_ref.lock().unwrap().push(ch); + } + Ok(()) + }, + ); + + // Instantiate the module. + let module = Module::new(&engine, wasm_bytes)?; + let instance = wasmtime::Instance::new( + &mut store, + &module, + &[ + emit_func.into(), + memory.into(), + dsp.into(), + rsp.into(), + fsp.into(), + table.into(), + ], + )?; + + // Register host functions in the table at the metadata-specified indices. + for (idx, name) in &metadata.host_functions { + let func = create_host_func(&mut store, &engine, memory, dsp, &output, name); + table.set(&mut store, *idx as u64, Ref::Func(Some(func)))?; + } + + // Call _start if it exists. + if let Some(start) = instance.get_func(&mut store, "_start") { + start.call(&mut store, &[], &mut [])?; + } + + let result = output.lock().unwrap().clone(); + Ok(result) +} + +/// Create a host function implementation for a known Forth word. +fn create_host_func( + store: &mut Store, + engine: &Engine, + memory: Memory, + dsp: Global, + output: &Arc>, + name: &str, +) -> Func { + let void_type = FuncType::new(engine, [], []); + + match name { + "." => { + // ( n -- ) print number followed by space + let out = Arc::clone(output); + Func::new(store, void_type, move |mut caller, _params, _results| { + let sp = dsp.get(&mut caller).unwrap_i32() as u32; + // Read all values from memory before mutable borrow. + let (n, base) = { + let data = memory.data(&caller); + let n = + i32::from_le_bytes(data[sp as usize..sp as usize + 4].try_into().unwrap()); + let base = u32::from_le_bytes( + data[SYSVAR_BASE_VAR as usize..SYSVAR_BASE_VAR as usize + 4] + .try_into() + .unwrap(), + ); + (n, base) + }; + dsp.set(&mut caller, Val::I32((sp + CELL_SIZE) as i32))?; + let s = if base == 16 { + format!("{n:X} ") + } else { + format!("{n} ") + }; + out.lock().unwrap().push_str(&s); + Ok(()) + }) + } + + "U." => { + // ( u -- ) print unsigned number followed by space + let out = Arc::clone(output); + Func::new(store, void_type, move |mut caller, _params, _results| { + let sp = dsp.get(&mut caller).unwrap_i32() as u32; + let (n, base) = { + let data = memory.data(&caller); + let n = + u32::from_le_bytes(data[sp as usize..sp as usize + 4].try_into().unwrap()); + let base = u32::from_le_bytes( + data[SYSVAR_BASE_VAR as usize..SYSVAR_BASE_VAR as usize + 4] + .try_into() + .unwrap(), + ); + (n, base) + }; + dsp.set(&mut caller, Val::I32((sp + CELL_SIZE) as i32))?; + let s = if base == 16 { + format!("{n:X} ") + } else { + format!("{n} ") + }; + out.lock().unwrap().push_str(&s); + Ok(()) + }) + } + + "TYPE" => { + // ( c-addr u -- ) output u characters from memory at c-addr + let out = Arc::clone(output); + Func::new(store, void_type, move |mut caller, _params, _results| { + let sp = dsp.get(&mut caller).unwrap_i32() as u32; + let text = { + let data = memory.data(&caller); + let len = + i32::from_le_bytes(data[sp as usize..sp as usize + 4].try_into().unwrap()) + as u32; + let addr = i32::from_le_bytes( + data[sp as usize + 4..sp as usize + 8].try_into().unwrap(), + ) as u32; + let end = (addr + len) as usize; + String::from_utf8_lossy(&data[addr as usize..end]).to_string() + }; + dsp.set(&mut caller, Val::I32((sp + 2 * CELL_SIZE) as i32))?; + out.lock().unwrap().push_str(&text); + Ok(()) + }) + } + + "SPACES" => { + // ( n -- ) output n spaces + let out = Arc::clone(output); + Func::new(store, void_type, move |mut caller, _params, _results| { + let sp = dsp.get(&mut caller).unwrap_i32() as u32; + let n = { + let data = memory.data(&caller); + i32::from_le_bytes(data[sp as usize..sp as usize + 4].try_into().unwrap()) + }; + dsp.set(&mut caller, Val::I32((sp + CELL_SIZE) as i32))?; + if n > 0 { + let spaces: String = std::iter::repeat_n(' ', n as usize).collect(); + out.lock().unwrap().push_str(&spaces); + } + Ok(()) + }) + } + + ".S" => { + // ( -- ) print stack contents non-destructively (no mutable borrow needed) + let out = Arc::clone(output); + Func::new(store, void_type, move |mut caller, _params, _results| { + let sp = dsp.get(&mut caller).unwrap_i32() as u32; + let data = memory.data(&caller); + let depth = (DATA_STACK_TOP - sp) / CELL_SIZE; + let mut buf = format!("<{depth}> "); + let mut addr = DATA_STACK_TOP - CELL_SIZE; + while addr >= sp { + let n = i32::from_le_bytes( + data[addr as usize..addr as usize + 4].try_into().unwrap(), + ); + buf.push_str(&format!("{n} ")); + if addr < CELL_SIZE { + break; + } + addr -= CELL_SIZE; + } + out.lock().unwrap().push_str(&buf); + Ok(()) + }) + } + + "DEPTH" => { + // ( -- n ) push current stack depth + Func::new(store, void_type, move |mut caller, _params, _results| { + let sp = dsp.get(&mut caller).unwrap_i32() as u32; + let depth = (DATA_STACK_TOP - sp) / CELL_SIZE; + let new_sp = sp - CELL_SIZE; + memory.data_mut(&mut caller)[new_sp as usize..new_sp as usize + 4] + .copy_from_slice(&(depth as i32).to_le_bytes()); + dsp.set(&mut caller, Val::I32(new_sp as i32))?; + Ok(()) + }) + } + + _ => { + // Unimplemented host function: trap with a clear message. + let name_owned = name.to_string(); + Func::new(store, void_type, move |_caller, _params, _results| { + anyhow::bail!("host function '{name_owned}' is not available in standalone mode") + }) + } + } +} + +/// Extract a named custom section from raw WASM bytes. +fn extract_custom_section(wasm_bytes: &[u8], section_name: &str) -> anyhow::Result { + let parsed = wasmparser::Parser::new(0).parse_all(wasm_bytes); + for payload in parsed { + if let wasmparser::Payload::CustomSection(reader) = payload? + && reader.name() == section_name + { + return Ok(String::from_utf8_lossy(reader.data()).to_string()); + } + } + anyhow::bail!("no '{section_name}' custom section found in WASM module") +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn extract_missing_section_is_error() { + // Minimal valid WASM module (magic + version only won't validate, + // but we can test with a trivial module). + let empty_module = wasm_encoder::Module::new().finish(); + let result = extract_custom_section(&empty_module, "wafer"); + assert!(result.is_err()); + } +} diff --git a/crates/web/Cargo.toml b/crates/web/Cargo.toml deleted file mode 100644 index 5e6c7b2..0000000 --- a/crates/web/Cargo.toml +++ /dev/null @@ -1,15 +0,0 @@ -[package] -name = "wafer-web" -description = "WAFER: WebAssembly Forth Engine in Rust - browser bindings" -version.workspace = true -edition.workspace = true -license.workspace = true - -[package.metadata.cargo-machete] -ignored = ["wafer-core"] - -[lints] -workspace = true - -[dependencies] -wafer-core = { path = "../core", version = "0.1.0" } diff --git a/crates/web/src/lib.rs b/crates/web/src/lib.rs deleted file mode 100644 index 3a82bdb..0000000 --- a/crates/web/src/lib.rs +++ /dev/null @@ -1,6 +0,0 @@ -//! WAFER Web: Browser bindings for WAFER Forth. -//! -//! This crate will provide wasm-bindgen bindings for running WAFER -//! in the browser with a web REPL. - -// TODO: Phase 5 - Browser target implementation