diff --git a/.github/workflows/checks.yml b/.github/workflows/checks.yml index 9f577fb2..2717b200 100644 --- a/.github/workflows/checks.yml +++ b/.github/workflows/checks.yml @@ -23,7 +23,6 @@ jobs: key: ${{ runner.os }}-check-${{ hashFiles('**/Cargo.lock') }} - run: RUSTFLAGS="-D warnings" cargo check --all-targets - run: RUSTFLAGS="-D warnings" cargo check --all-targets --features trace - - run: RUSTFLAGS="-D warnings" cargo check --all-targets --features _fuzz - run: RUSTFLAGS="-D warnings" cargo check --all-targets --no-default-features clippy: runs-on: ubuntu-latest @@ -40,7 +39,6 @@ jobs: key: ${{ runner.os }}-clippy-${{ hashFiles('**/Cargo.lock') }} - run: RUSTFLAGS="-D warnings" cargo clippy --all-targets - run: RUSTFLAGS="-D warnings" cargo clippy --all-targets --features trace - - run: RUSTFLAGS="-D warnings" cargo clippy --all-targets --features _fuzz - run: RUSTFLAGS="-D warnings" cargo clippy --all-targets --no-default-features test: runs-on: ubuntu-latest @@ -56,7 +54,6 @@ jobs: target key: ${{ runner.os }}-test-${{ hashFiles('**/Cargo.lock') }} - run: cargo test --release - - run: cargo test --release --features _fuzz --test fuzz fmt: runs-on: ubuntu-latest timeout-minutes: 10 diff --git a/Cargo.lock b/Cargo.lock index 15271e8b..6fdc86c2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4,65 +4,66 @@ version = 3 [[package]] name = "TSPL" -version = "0.0.9" +version = "0.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a9423b1e6e2d6c0bbc03660f58f9c30f55359e13afea29432e6e767c0f7dc25" +checksum = "52dfd6238b1461b99635b26585a85b4e7b9c786cc0481b3c540ae5f590b6dfb6" dependencies = [ "highlight_error", ] [[package]] name = "aho-corasick" -version = "1.1.2" +version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2969dcb958b36655471fc61f7e416fa76033bdd4bfed0678d8fee1e2d07a1f0" +checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" dependencies = [ "memchr", ] [[package]] name = "anstream" -version = "0.6.13" +version = "0.6.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d96bd03f33fe50a863e394ee9718a706f988b9079b20c3784fb726e7678b62fb" +checksum = "418c75fa768af9c03be99d17643f93f79bbba589895012a80e3452a19ddda15b" dependencies = [ "anstyle", "anstyle-parse", "anstyle-query", "anstyle-wincon", "colorchoice", + "is_terminal_polyfill", "utf8parse", ] [[package]] name = "anstyle" -version = "1.0.6" +version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8901269c6307e8d93993578286ac0edf7f195079ffff5ebdeea6a59ffb7e36bc" +checksum = "038dfcf04a5feb68e9c60b21c9625a54c2c0616e79b72b0fd87075a056ae1d1b" [[package]] name = "anstyle-parse" -version = "0.2.3" +version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c75ac65da39e5fe5ab759307499ddad880d724eed2f6ce5b5e8a26f4f387928c" +checksum = "c03a11a9034d92058ceb6ee011ce58af4a9bf61491aa7e1e59ecd24bd40d22d4" dependencies = [ "utf8parse", ] [[package]] name = "anstyle-query" -version = "1.0.2" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e28923312444cdd728e4738b3f9c9cac739500909bb3d3c94b43551b16517648" +checksum = "a64c907d4e79225ac72e2a354c9ce84d50ebb4586dee56c82b3ee73004f537f5" dependencies = [ "windows-sys", ] [[package]] name = "anstyle-wincon" -version = "3.0.2" +version = "3.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1cd54b81ec8d6180e24654d0b371ad22fc3dd083b6ff8ba325b72e00c87660a7" +checksum = "61a38449feb7068f52bb06c12759005cf459ee52bb4adc1d5a7c4322d716fb19" dependencies = [ "anstyle", "windows-sys", @@ -76,15 +77,15 @@ checksum = "96d30a06541fbafbc7f82ed10c06164cfbd2c401138f6addd8404629c4b16711" [[package]] name = "autocfg" -version = "1.1.0" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" +checksum = "f1fdabc7756949593fe60f30ec81974b613357de856987752631dea1e3394c80" [[package]] name = "bitflags" -version = "1.3.2" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" +checksum = "cf4b9d6a944f767f8e5e0db018570623c85f3d925ac718db4e06d0187adb21c1" [[package]] name = "bstr" @@ -98,9 +99,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.0.88" +version = "1.0.96" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02f341c093d19155a6e41631ce5971aac4e9a868262212153124c15fa22d1cdc" +checksum = "065a29261d53ba54260972629f9ca6bffa69bac13cd1fed61420f7fa68b9f8bd" [[package]] name = "cfg-if" @@ -110,9 +111,9 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" [[package]] name = "clap" -version = "4.5.1" +version = "4.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c918d541ef2913577a0f9566e9ce27cb35b6df072075769e0b26cb5a554520da" +checksum = "90bc066a67923782aa8515dbaea16946c5bcc5addbd668bb80af688e53e548a0" dependencies = [ "clap_builder", "clap_derive", @@ -120,9 +121,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.1" +version = "4.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f3e7391dad68afb0c2ede1bf619f579a3dc9c2ec67f089baa397123a2f3d1eb" +checksum = "ae129e2e766ae0ec03484e609954119f123cc1fe650337e155d03b022f24f7b4" dependencies = [ "anstream", "anstyle", @@ -132,9 +133,9 @@ dependencies = [ [[package]] name = "clap_derive" -version = "4.5.0" +version = "4.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "307bc0538d5f0f83b8248db3087aa92fe504e4691294d0c96c0eabc33f47ba47" +checksum = "528131438037fd55894f62d6e9f068b8f45ac57ffa77517819645d10aed04f64" dependencies = [ "heck", "proc-macro2", @@ -150,9 +151,9 @@ checksum = "98cc8fbded0c607b7ba9dd60cd98df59af97e84d24e49c8557331cfc26d301ce" [[package]] name = "colorchoice" -version = "1.0.0" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7" +checksum = "0b6a852b24ab71dffc585bcb46eaf7959d175cb865a7152e35b348d1b2960422" [[package]] name = "console" @@ -166,19 +167,6 @@ dependencies = [ "windows-sys", ] -[[package]] -name = "dashmap" -version = "5.5.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "978747c1d849a7d2ee5e8adc0159961c48fb7e5db2f06af6723b80123bb53856" -dependencies = [ - "cfg-if", - "hashbrown", - "lock_api", - "once_cell", - "parking_lot_core", -] - [[package]] name = "encode_unicode" version = "0.3.6" @@ -275,17 +263,11 @@ dependencies = [ "regex-syntax", ] -[[package]] -name = "hashbrown" -version = "0.14.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "290f1a1d9242c78d09ce40a5e87e7554ee637af1351968159f4952f028f75604" - [[package]] name = "heck" -version = "0.4.1" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" [[package]] name = "highlight_error" @@ -294,27 +276,79 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "809e18805660d7b6b2e2b9f316a5099521b5998d5cba4dda11b5157a21aaef03" [[package]] -name = "hvm-core" +name = "hvmc" version = "0.2.26" dependencies = [ - "TSPL", - "arrayvec", "clap", + "hvmc-ast", + "hvmc-host", + "hvmc-runtime", + "hvmc-transform", + "hvmc-util", "insta", "libloading", - "nohash-hasher", "ordered-float", "parking_lot", "serial_test", - "stacker", "thiserror", ] +[[package]] +name = "hvmc-ast" +version = "0.1.0" +dependencies = [ + "TSPL", + "arrayvec", + "hvmc-util", + "ordered-float", + "thiserror", +] + +[[package]] +name = "hvmc-host" +version = "0.1.0" +dependencies = [ + "hvmc-ast", + "hvmc-runtime", + "hvmc-util", + "parking_lot", +] + +[[package]] +name = "hvmc-runtime" +version = "0.1.0" +dependencies = [ + "hvmc-util", + "nohash-hasher", + "parking_lot", +] + +[[package]] +name = "hvmc-transform" +version = "0.1.0" +dependencies = [ + "hvmc-ast", + "hvmc-host", + "hvmc-runtime", + "hvmc-util", + "ordered-float", + "parking_lot", + "thiserror", +] + +[[package]] +name = "hvmc-util" +version = "0.1.0" +dependencies = [ + "arrayvec", + "stacker", +] + [[package]] name = "insta" -version = "1.35.1" +version = "1.38.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7c985c1bef99cf13c58fade470483d81a2bfe846ebde60ed28cc2dddec2df9e2" +checksum = "3eab73f58e59ca6526037208f0e98851159ec1633cf17b6cd2e1f2c3fd5d53cc" dependencies = [ "console", "globset", @@ -322,9 +356,14 @@ dependencies = [ "linked-hash-map", "similar", "walkdir", - "yaml-rust", ] +[[package]] +name = "is_terminal_polyfill" +version = "1.70.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8478577c03552c21db0e2724ffb8986a5ce7af88107e6be5d2ee6e158c12800" + [[package]] name = "lazy_static" version = "1.4.0" @@ -344,7 +383,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0c2a198fb6b0eada2a8df47933734e6d35d350665a33a3593d7164fa52c75c19" dependencies = [ "cfg-if", - "windows-targets 0.52.3", + "windows-targets", ] [[package]] @@ -355,9 +394,9 @@ checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f" [[package]] name = "lock_api" -version = "0.4.11" +version = "0.4.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c168f8615b12bc01f9c17e2eb0cc07dcae1940121185446edc3744920e8ef45" +checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17" dependencies = [ "autocfg", "scopeguard", @@ -365,15 +404,15 @@ dependencies = [ [[package]] name = "log" -version = "0.4.20" +version = "0.4.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f" +checksum = "90ed8c1e510134f979dbc4f070f87d4313098b704861a105fe34231c70a3901c" [[package]] name = "memchr" -version = "2.7.1" +version = "2.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "523dc4f511e55ab87b694dc30d0f820d60906ef06413f93d4d7a1385599cc149" +checksum = "6c8640c5d730cb13ebd907d8d04b52f55ac9a2eec55b440c8892f40d56c76c1d" [[package]] name = "nohash-hasher" @@ -407,9 +446,9 @@ dependencies = [ [[package]] name = "parking_lot" -version = "0.12.1" +version = "0.12.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f" +checksum = "7e4af0ca4f6caed20e900d564c242b8e5d4903fdacf31d3daf527b66fe6f42fb" dependencies = [ "lock_api", "parking_lot_core", @@ -417,22 +456,22 @@ dependencies = [ [[package]] name = "parking_lot_core" -version = "0.9.9" +version = "0.9.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c42a9226546d68acdd9c0a280d17ce19bfe27a46bf68784e4066115788d008e" +checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" dependencies = [ "cfg-if", "libc", "redox_syscall", "smallvec", - "windows-targets 0.48.5", + "windows-targets", ] [[package]] name = "pin-project-lite" -version = "0.2.13" +version = "0.2.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8afb450f006bf6385ca15ef45d71d2288452bc3683ce2e2cacc0d18e4be60b58" +checksum = "bda66fc9667c18cb2758a2ac84d1167245054bcf85d5d1aaa6923f45801bdd02" [[package]] name = "pin-utils" @@ -442,9 +481,9 @@ checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" [[package]] name = "proc-macro2" -version = "1.0.78" +version = "1.0.81" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2422ad645d89c99f8f3e6b88a9fdeca7fabeac836b1002371c4367c8f984aae" +checksum = "3d1597b0c024618f09a9c3b8655b7e430397a36d23fdafec26d6965e9eec3eba" dependencies = [ "unicode-ident", ] @@ -460,27 +499,27 @@ dependencies = [ [[package]] name = "quote" -version = "1.0.35" +version = "1.0.36" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "291ec9ab5efd934aaf503a6466c5d5251535d108ee747472c3977cc5acc868ef" +checksum = "0fa76aaf39101c457836aec0ce2316dbdc3ab723cdda1c6bd4e6ad4208acaca7" dependencies = [ "proc-macro2", ] [[package]] name = "redox_syscall" -version = "0.4.1" +version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4722d768eff46b75989dd134e5c353f0d6296e5aaa3132e776cbdb56be7731aa" +checksum = "469052894dcb553421e483e4209ee581a45100d31b4018de03e5a7ad86374a7e" dependencies = [ "bitflags", ] [[package]] name = "regex-automata" -version = "0.4.5" +version = "0.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5bb987efffd3c6d0d8f5f89510bb458559eab11e4f869acb20bf845e016259cd" +checksum = "86b83b8b9847f9bf95ef68afb0b8e6cdb80f498442f5179a29fad448fcc1eaea" dependencies = [ "aho-corasick", "memchr", @@ -489,9 +528,9 @@ dependencies = [ [[package]] name = "regex-syntax" -version = "0.8.2" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c08c74e62047bb2de4ff487b251e4a92e24f48745648451635cec7d591162d9f" +checksum = "adad44e29e4c806119491a7f06f03de4d1af22c3a680dd47f1e6e179439d1f56" [[package]] name = "same-file" @@ -502,26 +541,41 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "scc" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76ad2bbb0ae5100a07b7a6f2ed7ab5fd0045551a4c507989b7a620046ea3efdc" +dependencies = [ + "sdd", +] + [[package]] name = "scopeguard" version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" +[[package]] +name = "sdd" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b84345e4c9bd703274a082fb80caaa99b7612be48dfaa1dd9266577ec412309d" + [[package]] name = "serde" -version = "1.0.197" +version = "1.0.200" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3fb1c873e1b9b056a4dc4c0c198b24c3ffa059243875552b2bd0933b1aee4ce2" +checksum = "ddc6f9cc94d67c0e21aaf7eda3a010fd3af78ebf6e096aa6e2e13c79749cce4f" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.197" +version = "1.0.200" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7eb0b34b42edc17f6b7cac84a52a1c5f0e1bb2227e997ca9011ea3dd34e8610b" +checksum = "856f046b9400cee3c8c94ed572ecdb752444c24528c035cd35882aad6f492bcb" dependencies = [ "proc-macro2", "quote", @@ -530,23 +584,23 @@ dependencies = [ [[package]] name = "serial_test" -version = "3.0.0" +version = "3.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "953ad9342b3aaca7cb43c45c097dd008d4907070394bd0751a0aa8817e5a018d" +checksum = "4b4b487fe2acf240a021cf57c6b2b4903b1e78ca0ecd862a71b71d2a51fed77d" dependencies = [ - "dashmap", "futures", - "lazy_static", "log", + "once_cell", "parking_lot", + "scc", "serial_test_derive", ] [[package]] name = "serial_test_derive" -version = "3.0.0" +version = "3.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b93fb4adc70021ac1b47f7d45e8cc4169baaa7ea58483bc5b721d19a26202212" +checksum = "82fe9db325bcef1fbcde82e078a5cc4efdf787e96b3b9cf45b50b529f2083d67" dependencies = [ "proc-macro2", "quote", @@ -555,9 +609,9 @@ dependencies = [ [[package]] name = "similar" -version = "2.4.0" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32fea41aca09ee824cc9724996433064c89f7777e60762749a4170a14abbfa21" +checksum = "fa42c91313f1d05da9b26f267f931cf178d4aba455b4c4622dd7355eb80c6640" [[package]] name = "slab" @@ -570,9 +624,9 @@ dependencies = [ [[package]] name = "smallvec" -version = "1.13.1" +version = "1.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6ecd384b10a64542d77071bd64bd7b231f4ed5940fba55e98c3de13824cf3d7" +checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" [[package]] name = "stacker" @@ -589,15 +643,15 @@ dependencies = [ [[package]] name = "strsim" -version = "0.11.0" +version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ee073c9e4cd00e28217186dbe12796d692868f432bf2e97ee73bed0c56dfa01" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" [[package]] name = "syn" -version = "2.0.51" +version = "2.0.60" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ab617d94515e94ae53b8406c628598680aa0c9587474ecbe58188f7b345d66c" +checksum = "909518bc7b1c9b779f1bbf07f2929d35af9f0f37e47c6e9ef7f9dddc1e1821f3" dependencies = [ "proc-macro2", "quote", @@ -606,18 +660,18 @@ dependencies = [ [[package]] name = "thiserror" -version = "1.0.58" +version = "1.0.59" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03468839009160513471e86a034bb2c5c0e4baae3b43f79ffc55c4a5427b3297" +checksum = "f0126ad08bff79f29fc3ae6a55cc72352056dfff61e3ff8bb7129476d44b23aa" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.58" +version = "1.0.59" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c61f3ba182994efc43764a46c018c347bc492c79f024e705f46567b418f6d4f7" +checksum = "d1cd413b5d558b4c5bf3680e324a6fa5014e7b7c067a51e69dbdf47eb7148b66" dependencies = [ "proc-macro2", "quote", @@ -638,9 +692,9 @@ checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a" [[package]] name = "walkdir" -version = "2.4.0" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d71d857dc86794ca4c280d616f7da00d2dbfd8cd788846559a6813e6aa4b54ee" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" dependencies = [ "same-file", "winapi-util", @@ -664,11 +718,11 @@ checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" [[package]] name = "winapi-util" -version = "0.1.6" +version = "0.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f29e6f9198ba0d26b4c9f07dbe6f9ed633e1f3d5b8b414090084349e46a52596" +checksum = "4d4cc384e1e73b93bafa6fb4f1df8c41695c8a91cf9c4c64358067d15a7b6c6b" dependencies = [ - "winapi", + "windows-sys", ] [[package]] @@ -683,128 +737,69 @@ version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" dependencies = [ - "windows-targets 0.52.3", -] - -[[package]] -name = "windows-targets" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" -dependencies = [ - "windows_aarch64_gnullvm 0.48.5", - "windows_aarch64_msvc 0.48.5", - "windows_i686_gnu 0.48.5", - "windows_i686_msvc 0.48.5", - "windows_x86_64_gnu 0.48.5", - "windows_x86_64_gnullvm 0.48.5", - "windows_x86_64_msvc 0.48.5", + "windows-targets", ] [[package]] name = "windows-targets" -version = "0.52.3" +version = "0.52.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d380ba1dc7187569a8a9e91ed34b8ccfc33123bbacb8c0aed2d1ad7f3ef2dc5f" +checksum = "6f0713a46559409d202e70e28227288446bf7841d3211583a4b53e3f6d96e7eb" dependencies = [ - "windows_aarch64_gnullvm 0.52.3", - "windows_aarch64_msvc 0.52.3", - "windows_i686_gnu 0.52.3", - "windows_i686_msvc 0.52.3", - "windows_x86_64_gnu 0.52.3", - "windows_x86_64_gnullvm 0.52.3", - "windows_x86_64_msvc 0.52.3", + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_gnullvm", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", ] [[package]] name = "windows_aarch64_gnullvm" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" - -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.52.3" +version = "0.52.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68e5dcfb9413f53afd9c8f86e56a7b4d86d9a2fa26090ea2dc9e40fba56c6ec6" +checksum = "7088eed71e8b8dda258ecc8bac5fb1153c5cffaf2578fc8ff5d61e23578d3263" [[package]] name = "windows_aarch64_msvc" -version = "0.48.5" +version = "0.52.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" - -[[package]] -name = "windows_aarch64_msvc" -version = "0.52.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8dab469ebbc45798319e69eebf92308e541ce46760b49b18c6b3fe5e8965b30f" - -[[package]] -name = "windows_i686_gnu" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" +checksum = "9985fd1504e250c615ca5f281c3f7a6da76213ebd5ccc9561496568a2752afb6" [[package]] name = "windows_i686_gnu" -version = "0.52.3" +version = "0.52.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a4e9b6a7cac734a8b4138a4e1044eac3404d8326b6c0f939276560687a033fb" +checksum = "88ba073cf16d5372720ec942a8ccbf61626074c6d4dd2e745299726ce8b89670" [[package]] -name = "windows_i686_msvc" -version = "0.48.5" +name = "windows_i686_gnullvm" +version = "0.52.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" +checksum = "87f4261229030a858f36b459e748ae97545d6f1ec60e5e0d6a3d32e0dc232ee9" [[package]] name = "windows_i686_msvc" -version = "0.52.3" +version = "0.52.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28b0ec9c422ca95ff34a78755cfa6ad4a51371da2a5ace67500cf7ca5f232c58" +checksum = "db3c2bf3d13d5b658be73463284eaf12830ac9a26a90c717b7f771dfe97487bf" [[package]] name = "windows_x86_64_gnu" -version = "0.48.5" +version = "0.52.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" - -[[package]] -name = "windows_x86_64_gnu" -version = "0.52.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "704131571ba93e89d7cd43482277d6632589b18ecf4468f591fbae0a8b101614" +checksum = "4e4246f76bdeff09eb48875a0fd3e2af6aada79d409d33011886d3e1581517d9" [[package]] name = "windows_x86_64_gnullvm" -version = "0.48.5" +version = "0.52.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" - -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.52.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42079295511643151e98d61c38c0acc444e52dd42ab456f7ccfd5152e8ecf21c" - -[[package]] -name = "windows_x86_64_msvc" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" +checksum = "852298e482cd67c356ddd9570386e2862b5673c85bd5f88df9ab6802b334c596" [[package]] name = "windows_x86_64_msvc" -version = "0.52.3" +version = "0.52.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0770833d60a970638e989b3fa9fd2bb1aaadcf88963d1659fd7d9990196ed2d6" - -[[package]] -name = "yaml-rust" -version = "0.4.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56c1936c4cc7a1c9ab21a1ebb602eb942ba868cbd44a99cb7cdc5892335e1c85" -dependencies = [ - "linked-hash-map", -] +checksum = "bec47e5bfd1bff0eeaf6d8b485cc1074891a197ab4225d504cb7a1ab88b02bf0" diff --git a/Cargo.toml b/Cargo.toml index ea9511fd..ccddddcd 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,5 +1,19 @@ +[workspace] +resolver = "2" + +members = ["ast", "host", "runtime", "transform", "util"] + +[workspace.lints.clippy] +alloc_instead_of_core = "warn" +std_instead_of_core = "warn" +std_instead_of_alloc = "warn" +absolute_paths = "warn" +field_reassign_with_default = "allow" +missing_safety_doc = "allow" +new_ret_no_self = "allow" + [package] -name = "hvm-core" +name = "hvmc" version = "0.2.26" edition = "2021" description = "HVM-Core is a massively parallel Interaction Combinator evaluator." @@ -8,43 +22,27 @@ license = "MIT" [[bin]] name = "hvmc" path = "src/main.rs" -required-features = ["cli"] -bench = false - -[lib] -name = "hvmc" -path = "src/lib.rs" bench = false - -[profile.release] -codegen-units = 1 -lto = "fat" -opt-level = 3 -panic = "abort" -debug = "full" +required-features = ["std"] [dependencies] -TSPL = "0.0.9" -arrayvec = "0.7.4" -clap = { version = "4.5.1", features = ["derive"], optional = true } -libloading = "0.8.3" -nohash-hasher = { version = "0.2.0" } -ordered-float = "4.2.0" -parking_lot = "0.12.1" -stacker = "0.1.15" +clap = { version = "4.5.4", features = ["derive"] } +libloading = { version = "0.8.3", default-features = false } +ordered-float = { version = "4.2.0" } +parking_lot = "0.12.2" thiserror = "1.0.58" -##--COMPILER-CUTOFF--## - -[features] -default = ["cli", "_full_cli"] -std = [] -cli = ["std", "dep:clap"] -trace = [] -_full_cli = [] -_fuzz = ["std"] -_fuzz_no_free = ["_fuzz"] +hvmc-ast = { path = "./ast" } +hvmc-runtime = { path = "./runtime" } +hvmc-transform = { path = "./transform" } +hvmc-util = { path = "./util" } +hvmc-host = { path = "./host" } [dev-dependencies] insta = { version = "1.34.0", features = ["glob"] } serial_test = "3.0.0" + +[features] +default = ["std"] +std = [] +trace = ["hvmc-runtime/trace"] diff --git a/ast/Cargo.toml b/ast/Cargo.toml new file mode 100644 index 00000000..242213d9 --- /dev/null +++ b/ast/Cargo.toml @@ -0,0 +1,23 @@ +[package] +name = "hvmc-ast" +version = "0.1.0" +edition = "2021" + +[lib] +path = "src/ast.rs" + +[dependencies] +arrayvec = { version = "0.7.4", default-features = false } +ordered-float = { version = "4.2.0", default-features = false } +thiserror = { version = "1.0.59", optional = true } +TSPL = { version = "0.0.12", optional = true } + +hvmc-util = { path = "../util", default-features = false } + +[features] +default = ["std", "parser"] +std = ["hvmc-util/std", "ordered-float/std", "dep:thiserror"] +parser = ["dep:TSPL"] + +[lints] +workspace = true diff --git a/src/ast.rs b/ast/src/ast.rs similarity index 58% rename from src/ast.rs rename to ast/src/ast.rs index c777d2cb..2061dcce 100644 --- a/src/ast.rs +++ b/ast/src/ast.rs @@ -9,19 +9,24 @@ //! The AST is based on the [interaction calculus]. //! //! [interaction calculus]: https://en.wikipedia.org/wiki/Interaction_nets#Interaction_calculus +#![cfg_attr(not(feature = "std"), no_std)] +#![allow(incomplete_features)] +#![feature(generic_const_exprs)] -use crate::{ - ops::TypedOp as Op, - prelude::*, - run::Lab, - util::{array_vec, deref, maybe_grow}, -}; +include!("../../prelude.rs"); + +#[cfg(feature = "parser")] +mod parser; use alloc::collections::BTreeMap; + +use crate::prelude::*; +use hvmc_util::{array_vec, create_var, deref, maybe_grow, ops::TypedOp as Op, var_to_num}; + use arrayvec::ArrayVec; -use core::str::FromStr; use ordered_float::OrderedFloat; -use TSPL::{new_parser, Parser}; + +pub type Lab = u16; /// The top level AST node, representing a collection of named nets. /// @@ -154,9 +159,36 @@ impl Net { pub fn trees(&self) -> impl Iterator { iter::once(&self.root).chain(self.redexes.iter().flat_map(|(x, y)| [x, y])) } + pub fn trees_mut(&mut self) -> impl Iterator { iter::once(&mut self.root).chain(self.redexes.iter_mut().flat_map(|(x, y)| [x, y])) } + + /// Transforms the net `x & ...` into `y & x ~ (arg y) & ...` + /// + /// The result is equivalent a λ-calculus application. Thus, + /// if the net is a λ-calculus term, then this function will + /// apply an argument to it. + pub fn apply_tree(&mut self, arg: Tree) { + let mut fresh = 0usize; + self.ensure_no_conflicts(&mut fresh); + arg.ensure_no_conflicts(&mut fresh); + + let fresh_str = create_var(fresh + 1); + + let fun = mem::take(&mut self.root); + let app = Tree::Ctr { lab: 0, ports: vec![arg, Tree::Var { nam: fresh_str.clone() }] }; + self.root = Tree::Var { nam: fresh_str }; + self.redexes.push((fun, app)); + } + + pub(crate) fn ensure_no_conflicts(&self, fresh: &mut usize) { + self.root.ensure_no_conflicts(fresh); + for (a, b) in &self.redexes { + a.ensure_no_conflicts(fresh); + b.ensure_no_conflicts(fresh); + } + } } impl Tree { @@ -186,7 +218,7 @@ impl Tree { }) } - pub(crate) fn lab(&self) -> Option { + pub fn lab(&self) -> Option { match self { Tree::Ctr { lab, ports } if ports.len() >= 2 => Some(*lab), Tree::Adt { lab, .. } => Some(*lab), @@ -202,221 +234,65 @@ impl Tree { let succ = Box::new(succ); Some(Tree::Mat { zero, succ, out: Box::new(out) }) } -} -new_parser!(HvmcParser); - -impl<'i> HvmcParser<'i> { - /// Book = ("@" Name "=" Net)* - fn parse_book(&mut self) -> Result { - maybe_grow(move || { - let mut book = BTreeMap::new(); - while self.consume("@").is_ok() { - let name = self.parse_name()?; - self.consume("=")?; - let net = self.parse_net()?; - book.insert(name, net); + /// Increases `fresh` until `create_var(*fresh)` does not conflict + /// with a [`Tree::Var`] in `tree` + /// + /// This function can be called multiple times with many trees to + /// ensure that `fresh` does not conflict with any of them. + pub(crate) fn ensure_no_conflicts(&self, fresh: &mut usize) { + if let Tree::Var { nam } = self { + if let Some(var_num) = var_to_num(nam) { + *fresh = (*fresh).max(var_num); } - Ok(Book { nets: book }) - }) - } - - /// Net = Tree ("&" Tree "~" Tree)* - fn parse_net(&mut self) -> Result { - let mut redexes = Vec::new(); - let root = self.parse_tree()?; - while self.consume("&").is_ok() { - let tree1 = self.parse_tree()?; - self.consume("~")?; - let tree2 = self.parse_tree()?; - redexes.push((tree1, tree2)); } - Ok(Net { root, redexes }) + self.children().for_each(|child| child.ensure_no_conflicts(fresh)); } +} - fn parse_tree(&mut self) -> Result { - maybe_grow(move || { - self.skip_trivia(); - match self.peek_one() { - // Era = "*" - Some('*') => { - self.advance_one(); - Ok(Tree::Era) - } - // Ctr = "(" Tree Tree ")" | "[" Tree Tree "]" | "{" Int Tree Tree "}" - Some(char @ ('(' | '[' | '{')) => { - self.advance_one(); - let lab = match char { - '(' => 0, - '[' => 1, - '{' => self.parse_u64()? as Lab, - _ => unreachable!(), - }; - let close = match char { - '(' => ')', - '[' => ']', - '{' => '}', - _ => unreachable!(), - }; - self.skip_trivia(); - if self.peek_one().is_some_and(|x| x == ':') { - self.advance_one(); - let variant_index = self.parse_u64()?; - self.consume(":")?; - let variant_count = self.parse_u64()?; - let mut fields = Vec::new(); - self.skip_trivia(); - while self.peek_one() != Some(close) { - fields.push(self.parse_tree()?); - self.skip_trivia(); - } - self.advance_one(); - if variant_count == 0 { - Err("variant count cannot be zero".to_owned())?; - } - if variant_count > (MAX_ADT_VARIANTS as u64) { - Err("adt has too many variants".to_owned())?; - } - if variant_index >= variant_count { - Err("variant index out of range".to_owned())?; - } - let variant_index = variant_index as usize; - let variant_count = variant_count as usize; - if fields.len() > MAX_ADT_FIELDS { - Err("adt has too many fields".to_owned())?; - } - Ok(Tree::Adt { lab, variant_index, variant_count, fields }) - } else { - let mut ports = Vec::new(); - self.skip_trivia(); - while self.peek_one() != Some(close) { - ports.push(self.parse_tree()?); - self.skip_trivia(); - } - self.advance_one(); - if ports.len() > MAX_ARITY { - Err("ctr has too many ports".to_owned())?; - } - Ok(Tree::Ctr { lab, ports }) - } - } - // Ref = "@" Name - Some('@') => { - self.advance_one(); - self.skip_trivia(); - let nam = self.parse_name()?; - Ok(Tree::Ref { nam }) - } - // Int = "#" [-] Int - // F32 = "#" [-] ( Int "." Int | "NaN" | "inf" ) - Some('#') => { - self.advance_one(); - let is_neg = self.consume("-").is_ok(); - let num = self.take_while(|c| c.is_alphanumeric() || c == '.'); - - if num.contains('.') || num.contains("NaN") || num.contains("inf") { - let mut val: f32 = num.parse().map_err(|err| format!("{err:?}"))?; - if is_neg { - val = -val; - } - Ok(Tree::F32 { val: val.into() }) - } else { - let mut val: i64 = parse_int(num)? as i64; - if is_neg { - val = -val; - } - Ok(Tree::Int { val }) - } - } - // Op = "<" Op Tree Tree ">" - Some('<') => { - self.advance_one(); - let op = self.parse_op()?; - let rhs = Box::new(self.parse_tree()?); - let out = Box::new(self.parse_tree()?); - self.consume(">")?; - Ok(Tree::Op { op, rhs, out }) - } - // Mat = "?<" Tree Tree ">" - Some('?') => { - self.advance_one(); - self.consume("<")?; - let zero = self.parse_tree()?; - let succ = self.parse_tree()?; - self.skip_trivia(); - if self.peek_one() == Some('>') { - self.advance_one(); - Tree::legacy_mat(zero, succ).ok_or_else(|| "invalid legacy match".to_owned()) - } else { - let zero = Box::new(zero); - let succ = Box::new(succ); - let out = Box::new(self.parse_tree()?); - self.consume(">")?; - Ok(Tree::Mat { zero, succ, out }) - } - } - // Var = Name - _ => Ok(Tree::Var { nam: self.parse_name()? }), +// Manually implemented to avoid stack overflows. +impl Clone for Tree { + fn clone(&self) -> Tree { + maybe_grow(|| match self { + Tree::Era => Tree::Era, + Tree::Int { val } => Tree::Int { val: *val }, + Tree::F32 { val } => Tree::F32 { val: *val }, + Tree::Ref { nam } => Tree::Ref { nam: nam.clone() }, + Tree::Ctr { lab, ports } => Tree::Ctr { lab: *lab, ports: ports.clone() }, + Tree::Op { op, rhs, out } => Tree::Op { op: *op, rhs: rhs.clone(), out: out.clone() }, + Tree::Mat { zero, succ, out } => Tree::Mat { zero: zero.clone(), succ: succ.clone(), out: out.clone() }, + Tree::Adt { lab, variant_index, variant_count, fields } => { + Tree::Adt { lab: *lab, variant_index: *variant_index, variant_count: *variant_count, fields: fields.clone() } } + Tree::Var { nam } => Tree::Var { nam: nam.clone() }, }) } - - /// Name = /[a-zA-Z0-9_.$]+/ - fn parse_name(&mut self) -> Result { - let name = self.take_while(|c| c.is_alphanumeric() || c == '_' || c == '.' || c == '$'); - if name.is_empty() { - return self.expected("name"); - } - Ok(name.to_owned()) - } - - /// See `ops.rs` for the available operators. - fn parse_op(&mut self) -> Result { - let op = self.take_while(|c| c.is_alphanumeric() || ".+-=*/%<>|&^!?$".contains(c)); - op.parse().map_err(|_| format!("Unknown operator: {op:?}")) - } -} - -/// Parses an unsigned integer with an optional radix prefix. -fn parse_int(input: &str) -> Result { - if let Some(rest) = input.strip_prefix("0x") { - u64::from_str_radix(rest, 16).map_err(|err| format!("{err:?}")) - } else if let Some(rest) = input.strip_prefix("0b") { - u64::from_str_radix(rest, 2).map_err(|err| format!("{err:?}")) - } else { - input.parse::().map_err(|err| format!("{err:?}")) - } -} - -/// Parses the input with the callback, ensuring that the whole input is -/// consumed. -fn parse_eof<'i, T>(input: &'i str, parse_fn: impl Fn(&mut HvmcParser<'i>) -> Result) -> Result { - let mut parser = HvmcParser::new(input); - let out = parse_fn(&mut parser)?; - if parser.index != parser.input.len() { - return Err("Unable to parse the whole input. Is this not an hvmc file?".to_owned()); - } - Ok(out) } -impl FromStr for Book { - type Err = String; - fn from_str(str: &str) -> Result { - parse_eof(str, HvmcParser::parse_book) - } -} - -impl FromStr for Net { - type Err = String; - fn from_str(str: &str) -> Result { - parse_eof(str, HvmcParser::parse_net) - } -} - -impl FromStr for Tree { - type Err = String; - fn from_str(str: &str) -> Result { - parse_eof(str, HvmcParser::parse_tree) +// Drops non-recursively to avoid stack overflows. +impl Drop for Tree { + fn drop(&mut self) { + loop { + let mut i = self.children_mut().filter(|x| x.children().len() != 0); + let Some(x) = i.next() else { break }; + if { i }.next().is_none() { + // There's only one child; move it up to be the new root. + *self = mem::take(x); + continue; + } + // Rotate the tree right: + // ```text + // a b + // / \ / \ + // b e -> c a + // / \ / \ + // c d d e + // ``` + let d = mem::take(x.children_mut().next_back().unwrap()); + let b = mem::replace(x, d); + let a = mem::replace(self, b); + mem::forget(mem::replace(self.children_mut().next_back().unwrap(), a)); + } } } @@ -426,6 +302,7 @@ impl fmt::Display for Book { if i != 0 { f.write_str("\n\n")?; } + write!(f, "@{name} = {net}")?; } Ok(()) @@ -435,9 +312,11 @@ impl fmt::Display for Book { impl fmt::Display for Net { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "{}", &self.root)?; + for (a, b) in &self.redexes { write!(f, "\n & {a} ~ {b}")?; } + Ok(()) } } @@ -494,70 +373,24 @@ impl fmt::Display for Tree { } } -// Manually implemented to avoid stack overflows. -impl Clone for Tree { - fn clone(&self) -> Tree { - maybe_grow(|| match self { - Tree::Era => Tree::Era, - Tree::Int { val } => Tree::Int { val: *val }, - Tree::F32 { val } => Tree::F32 { val: *val }, - Tree::Ref { nam } => Tree::Ref { nam: nam.clone() }, - Tree::Ctr { lab, ports } => Tree::Ctr { lab: *lab, ports: ports.clone() }, - Tree::Op { op, rhs, out } => Tree::Op { op: *op, rhs: rhs.clone(), out: out.clone() }, - Tree::Mat { zero, succ, out } => Tree::Mat { zero: zero.clone(), succ: succ.clone(), out: out.clone() }, - Tree::Adt { lab, variant_index, variant_count, fields } => { - Tree::Adt { lab: *lab, variant_index: *variant_index, variant_count: *variant_count, fields: fields.clone() } - } - Tree::Var { nam } => Tree::Var { nam: nam.clone() }, - }) - } -} - -// Drops non-recursively to avoid stack overflows. -impl Drop for Tree { - fn drop(&mut self) { - loop { - let mut i = self.children_mut().filter(|x| x.children().len() != 0); - let Some(x) = i.next() else { break }; - if { i }.next().is_none() { - // There's only one child; move it up to be the new root. - *self = mem::take(x); - continue; - } - // Rotate the tree right: - // ```text - // a b - // / \ / \ - // b e -> c a - // / \ / \ - // c d d e - // ``` - let d = mem::take(x.children_mut().next_back().unwrap()); - let b = mem::replace(x, d); - let a = mem::replace(self, b); - mem::forget(mem::replace(self.children_mut().next_back().unwrap(), a)); - } - } -} - -#[test] -fn test_tree_drop() { - use alloc::vec; - - drop(Tree::from_str("((* (* *)) (* *))")); - - let mut long_tree = Tree::Era; - let mut cursor = &mut long_tree; - for _ in 0 .. 100_000 { - *cursor = Tree::Ctr { lab: 0, ports: vec![Tree::Era, Tree::Era] }; - let Tree::Ctr { ports, .. } = cursor else { unreachable!() }; - cursor = &mut ports[0]; - } - drop(long_tree); - - let mut big_tree = Tree::Era; - for _ in 0 .. 16 { - big_tree = Tree::Ctr { lab: 0, ports: vec![big_tree.clone(), big_tree] }; - } - drop(big_tree); -} +// #[test] +// fn test_tree_drop() { +// use alloc::vec; + +// drop(Tree::from_str("((* (* *)) (* *))")); + +// let mut long_tree = Tree::Era; +// let mut cursor = &mut long_tree; +// for _ in 0 .. 100_000 { +// *cursor = Tree::Ctr { lab: 0, ports: vec![Tree::Era, Tree::Era] }; +// let Tree::Ctr { ports, .. } = cursor else { unreachable!() }; +// cursor = &mut ports[0]; +// } +// drop(long_tree); + +// let mut big_tree = Tree::Era; +// for _ in 0 .. 16 { +// big_tree = Tree::Ctr { lab: 0, ports: vec![big_tree.clone(), big_tree] }; +// } +// drop(big_tree); +// } diff --git a/ast/src/parser.rs b/ast/src/parser.rs new file mode 100644 index 00000000..c151ccea --- /dev/null +++ b/ast/src/parser.rs @@ -0,0 +1,225 @@ +use crate::prelude::*; + +use alloc::collections::BTreeMap; +use core::str::FromStr; + +use crate::{Book, Lab, Net, Tree, MAX_ADT_FIELDS, MAX_ADT_VARIANTS, MAX_ARITY}; +use hvmc_util::{maybe_grow, ops::TypedOp as Op}; + +use TSPL::{new_parser, Parser}; + +new_parser!(HvmcParser); + +impl<'i> HvmcParser<'i> { + /// Book = ("@" Name "=" Net)* + fn parse_book(&mut self) -> Result { + maybe_grow(move || { + let mut book = BTreeMap::new(); + while self.consume("@").is_ok() { + let name = self.parse_name()?; + self.consume("=")?; + let net = self.parse_net()?; + book.insert(name, net); + } + Ok(Book { nets: book }) + }) + } + + /// Net = Tree ("&" Tree "~" Tree)* + fn parse_net(&mut self) -> Result { + let mut redexes = Vec::new(); + let root = self.parse_tree()?; + while self.consume("&").is_ok() { + let tree1 = self.parse_tree()?; + self.consume("~")?; + let tree2 = self.parse_tree()?; + redexes.push((tree1, tree2)); + } + Ok(Net { root, redexes }) + } + + fn parse_tree(&mut self) -> Result { + maybe_grow(move || { + self.skip_trivia(); + match self.peek_one() { + // Era = "*" + Some('*') => { + self.advance_one(); + Ok(Tree::Era) + } + // Ctr = "(" Tree Tree ")" | "[" Tree Tree "]" | "{" Int Tree Tree "}" + Some(char @ ('(' | '[' | '{')) => { + self.advance_one(); + let lab = match char { + '(' => 0, + '[' => 1, + '{' => self.parse_u64()? as Lab, + _ => unreachable!(), + }; + let close = match char { + '(' => ')', + '[' => ']', + '{' => '}', + _ => unreachable!(), + }; + self.skip_trivia(); + if self.peek_one().is_some_and(|x| x == ':') { + self.advance_one(); + let variant_index = self.parse_u64()?; + self.consume(":")?; + let variant_count = self.parse_u64()?; + let mut fields = Vec::new(); + self.skip_trivia(); + while self.peek_one() != Some(close) { + fields.push(self.parse_tree()?); + self.skip_trivia(); + } + self.advance_one(); + if variant_count == 0 { + Err("variant count cannot be zero".to_owned())?; + } + if variant_count > (MAX_ADT_VARIANTS as u64) { + Err("adt has too many variants".to_owned())?; + } + if variant_index >= variant_count { + Err("variant index out of range".to_owned())?; + } + let variant_index = variant_index as usize; + let variant_count = variant_count as usize; + if fields.len() > MAX_ADT_FIELDS { + Err("adt has too many fields".to_owned())?; + } + Ok(Tree::Adt { lab, variant_index, variant_count, fields }) + } else { + let mut ports = Vec::new(); + self.skip_trivia(); + while self.peek_one() != Some(close) { + ports.push(self.parse_tree()?); + self.skip_trivia(); + } + self.advance_one(); + if ports.len() > MAX_ARITY { + Err("ctr has too many ports".to_owned())?; + } + Ok(Tree::Ctr { lab, ports }) + } + } + // Ref = "@" Name + Some('@') => { + self.advance_one(); + self.skip_trivia(); + let nam = self.parse_name()?; + Ok(Tree::Ref { nam }) + } + // Int = "#" [-] Int + // F32 = "#" [-] ( Int "." Int | "NaN" | "inf" ) + Some('#') => { + self.advance_one(); + let is_neg = self.consume("-").is_ok(); + let num = self.take_while(|c| c.is_alphanumeric() || c == '.'); + + if num.contains('.') || num.contains("NaN") || num.contains("inf") { + let mut val: f32 = num.parse().map_err(|err| format!("{err:?}"))?; + if is_neg { + val = -val; + } + Ok(Tree::F32 { val: val.into() }) + } else { + let mut val: i64 = parse_int(num)? as i64; + if is_neg { + val = -val; + } + Ok(Tree::Int { val }) + } + } + // Op = "<" Op Tree Tree ">" + Some('<') => { + self.advance_one(); + let op = self.parse_op()?; + let rhs = Box::new(self.parse_tree()?); + let out = Box::new(self.parse_tree()?); + self.consume(">")?; + Ok(Tree::Op { op, rhs, out }) + } + // Mat = "?<" Tree Tree ">" + Some('?') => { + self.advance_one(); + self.consume("<")?; + let zero = self.parse_tree()?; + let succ = self.parse_tree()?; + self.skip_trivia(); + if self.peek_one() == Some('>') { + self.advance_one(); + Tree::legacy_mat(zero, succ).ok_or_else(|| "invalid legacy match".to_owned()) + } else { + let zero = Box::new(zero); + let succ = Box::new(succ); + let out = Box::new(self.parse_tree()?); + self.consume(">")?; + Ok(Tree::Mat { zero, succ, out }) + } + } + // Var = Name + _ => Ok(Tree::Var { nam: self.parse_name()? }), + } + }) + } + + /// Name = /[a-zA-Z0-9_.$]+/ + fn parse_name(&mut self) -> Result { + let name = self.take_while(|c| c.is_alphanumeric() || c == '_' || c == '.' || c == '$'); + if name.is_empty() { + return self.expected("name"); + } + Ok(name.to_owned()) + } + + /// See `ops.rs` for the available operators. + fn parse_op(&mut self) -> Result { + let op = self.take_while(|c| c.is_alphanumeric() || ".+-=*/%<>|&^!?$".contains(c)); + op.parse().map_err(|_| format!("Unknown operator: {op:?}")) + } +} + +/// Parses an unsigned integer with an optional radix prefix. +fn parse_int(input: &str) -> Result { + if let Some(rest) = input.strip_prefix("0x") { + u64::from_str_radix(rest, 16).map_err(|err| format!("{err:?}")) + } else if let Some(rest) = input.strip_prefix("0b") { + u64::from_str_radix(rest, 2).map_err(|err| format!("{err:?}")) + } else { + input.parse::().map_err(|err| format!("{err:?}")) + } +} + +/// Parses the input with the callback, ensuring that the whole input is +/// consumed. +fn parse_eof<'i, T>(input: &'i str, parse_fn: impl Fn(&mut HvmcParser<'i>) -> Result) -> Result { + let mut parser = HvmcParser::new(input); + let out = parse_fn(&mut parser)?; + if parser.index != parser.input.len() { + return Err("Unable to parse the whole input. Is this not an hvmc file?".to_owned()); + } + Ok(out) +} + +impl FromStr for Book { + type Err = String; + fn from_str(str: &str) -> Result { + parse_eof(str, HvmcParser::parse_book) + } +} + +impl FromStr for Net { + type Err = String; + fn from_str(str: &str) -> Result { + parse_eof(str, HvmcParser::parse_net) + } +} + +impl FromStr for Tree { + type Err = String; + fn from_str(str: &str) -> Result { + parse_eof(str, HvmcParser::parse_tree) + } +} diff --git a/host/Cargo.toml b/host/Cargo.toml new file mode 100644 index 00000000..90bdeae6 --- /dev/null +++ b/host/Cargo.toml @@ -0,0 +1,20 @@ +[package] +name = "hvmc-host" +version = "0.1.0" +edition = "2021" + +[lib] +path = "src/host.rs" + +[dependencies] +parking_lot = "0.12.2" + +hvmc-ast = { path = "../ast", default-features = false } +hvmc-util = { path = "../util", default-features = false } +hvmc-runtime = { path = "../runtime", default-features = false } + +[features] +std = ["hvmc-ast/std", "hvmc-util/std", "hvmc-runtime/std"] + +[lints] +workspace = true diff --git a/src/host/calc_labels.rs b/host/src/calc_labels.rs similarity index 98% rename from src/host/calc_labels.rs rename to host/src/calc_labels.rs index a521c5c0..192e05c8 100644 --- a/src/host/calc_labels.rs +++ b/host/src/calc_labels.rs @@ -1,7 +1,6 @@ -use crate::prelude::*; - use super::*; -use crate::util::maybe_grow; + +use hvmc_util::maybe_grow; /// Calculates the labels used in each definition of a book. /// @@ -132,7 +131,7 @@ impl<'b, F: FnMut(&'b str) -> LabSet> State<'b, F> { self.calc_def(key, depth, out) } Entry::Occupied(mut e) => match e.get_mut() { - LabelState::Done(labs) => { + LabelState::Done(ref labs) => { if let Some(out) = out { out.union(labs); } diff --git a/src/host/encode.rs b/host/src/encode.rs similarity index 92% rename from src/host/encode.rs rename to host/src/encode.rs index bc1bf83f..7822f5dd 100644 --- a/src/host/encode.rs +++ b/host/src/encode.rs @@ -1,13 +1,15 @@ use crate::prelude::*; -use super::*; -use crate::{ops::TypedOp as Op, run::Lab, util::maybe_grow}; +use crate::Host; +use hvmc_ast::{Lab, Net as AstNet, Tree}; +use hvmc_runtime::{Instruction, InterpretedDef, Mode, Net, Port, Trg, TrgId}; +use hvmc_util::{maybe_grow, ops::TypedOp as Op}; impl Host { /// Converts an ast net to a list of instructions to create the net. /// /// `get_def` must return the `Port` corresponding to a given `Ref` name. - pub(crate) fn encode_def(&self, net: &Net) -> InterpretedDef { + pub fn encode_def(&self, net: &AstNet) -> InterpretedDef { let mut def = InterpretedDef { instr: Vec::new(), trgs: 1 }; let mut state = State { host: self, encoder: &mut def, scope: Default::default() }; state.visit_net(net, TrgId::new(0)); @@ -17,7 +19,7 @@ impl Host { /// Encode `tree` directly into `trg`, skipping the intermediate `Def` /// representation. - pub fn encode_tree(&self, net: &mut run::Net, trg: run::Trg, tree: &Tree) { + pub fn encode_tree(&self, net: &mut Net, trg: Trg, tree: &Tree) { let mut state = State { host: self, encoder: net, scope: Default::default() }; state.visit_tree(tree, trg); state.finish(); @@ -25,7 +27,7 @@ impl Host { /// Encode the root of `ast_net` directly into `trg` and encode its redexes /// into `net` redex list. - pub fn encode_net(&self, net: &mut run::Net, trg: run::Trg, ast_net: &Net) { + pub fn encode_net(&self, net: &mut Net, trg: Trg, ast_net: &AstNet) { let mut state = State { host: self, encoder: net, scope: Default::default() }; state.visit_net(ast_net, trg); state.finish(); @@ -42,7 +44,7 @@ impl<'a, E: Encoder> State<'a, E> { fn finish(self) { assert!(self.scope.is_empty(), "unbound variables: {:?}", self.scope.keys()); } - fn visit_net(&mut self, net: &'a Net, trg: E::Trg) { + fn visit_net(&mut self, net: &'a AstNet, trg: E::Trg) { self.visit_tree(&net.root, trg); net.redexes.iter().for_each(|(a, b)| self.visit_redex(a, b)); } @@ -145,14 +147,6 @@ trait Encoder { fn wires(&mut self) -> (Self::Trg, Self::Trg, Self::Trg, Self::Trg); } -impl InterpretedDef { - fn new_trg_id(&mut self) -> TrgId { - let index = self.trgs; - self.trgs += 1; - TrgId::new(index) - } -} - impl Encoder for InterpretedDef { type Trg = TrgId; fn link_const(&mut self, trg: Self::Trg, port: Port) { @@ -199,8 +193,9 @@ impl Encoder for InterpretedDef { } } -impl<'a, M: Mode> Encoder for run::Net<'a, M> { - type Trg = run::Trg; +impl<'a, M: Mode> Encoder for Net<'a, M> { + type Trg = Trg; + fn link_const(&mut self, trg: Self::Trg, port: Port) { self.link_trg_port(trg, port) } @@ -208,7 +203,7 @@ impl<'a, M: Mode> Encoder for run::Net<'a, M> { self.link_trg(a, b) } fn make_const(&mut self, port: Port) -> Self::Trg { - run::Trg::port(port) + Trg::port(port) } fn ctr(&mut self, lab: Lab, trg: Self::Trg) -> (Self::Trg, Self::Trg) { self.do_ctr(lab, trg) diff --git a/src/host.rs b/host/src/host.rs similarity index 88% rename from src/host.rs rename to host/src/host.rs index 440d68e1..3b00db83 100644 --- a/src/host.rs +++ b/host/src/host.rs @@ -1,15 +1,17 @@ //! The runtime's host, which acts as a translation layer between the AST and //! the runtime. +#![cfg_attr(not(feature = "std"), no_std)] +#![feature(inline_const)] + +include!("../../prelude.rs"); use crate::prelude::*; +use hvmc_ast::{Book, Tree}; +use hvmc_runtime::{Addr, Def, InterpretedDef, LabSet, Mode, Port, Tag, Wire}; + +use core::ops::{Deref, DerefMut}; -use crate::{ - ast::{Book, Net, Tree}, - run::{self, Addr, Def, Instruction, InterpretedDef, LabSet, Mode, Port, Tag, TrgId, Wire}, - stdlib::HostedDef, - util::create_var, -}; -use core::ops::{Deref, DerefMut, RangeFrom}; +pub mod stdlib; mod calc_labels; mod encode; @@ -83,7 +85,7 @@ impl Host { }) .into_iter() { - let def = unsafe { HostedDef::new_hosted(labs, InterpretedDef::default()) }; + let def = DefRef::Owned(Box::new(Def::new(labs, InterpretedDef::default()))); self.insert_def(name, def); } @@ -91,7 +93,7 @@ impl Host { // each of the new defs. for (nam, net) in book.iter() { let data = self.encode_def(net); - self.get_mut::>(nam).data.0 = data; + self.get_mut::(nam).data = data; } } diff --git a/src/host/readback.rs b/host/src/readback.rs similarity index 90% rename from src/host/readback.rs rename to host/src/readback.rs index 032453f3..4dfc1acd 100644 --- a/src/host/readback.rs +++ b/host/src/readback.rs @@ -1,11 +1,15 @@ use crate::prelude::*; -use super::*; -use crate::util::maybe_grow; +use super::{Addr, Host, Mode, Port, Tag, Wire}; + +use core::ops::RangeFrom; + +use hvmc_ast::{Net, Tree}; +use hvmc_util::{create_var, maybe_grow}; impl Host { /// Creates an ast tree from a wire in a runtime net. - pub fn readback_tree(&self, wire: &run::Wire) -> Tree { + pub fn readback_tree(&self, wire: &Wire) -> Tree { ReadbackState { host: self, vars: Default::default(), var_id: 0 .. }.read_wire(wire.clone()) } @@ -15,7 +19,7 @@ impl Host { /// resulting ast net, as it is impossible to read these back from the runtime /// net representation. In the case of vicious circles, this may result in /// unbound variables. - pub fn readback(&self, rt_net: &run::Net) -> Net { + pub fn readback(&self, rt_net: &hvmc_runtime::Net) -> Net { let mut state = ReadbackState { host: self, vars: Default::default(), var_id: 0 .. }; let mut net = Net::default(); @@ -27,6 +31,7 @@ impl Host { net } } + /// See [`Host::readback`]. struct ReadbackState<'a> { host: &'a Host, @@ -40,6 +45,7 @@ impl<'a> ReadbackState<'a> { let port = wire.load_target(); self.read_port(port, Some(wire)) } + /// Reads a tree out from a given `port`. If this is a var port, the /// `wire` this port was reached from must be supplied to key into the /// `vars` map. diff --git a/src/stdlib.rs b/host/src/stdlib.rs similarity index 97% rename from src/stdlib.rs rename to host/src/stdlib.rs index 79d66e3a..8160ad47 100644 --- a/src/stdlib.rs +++ b/host/src/stdlib.rs @@ -5,14 +5,13 @@ use core::{ marker::PhantomData, sync::atomic::{AtomicUsize, Ordering}, }; + use parking_lot::Mutex; -use crate::{ - dispatch_dyn_net, - host::{DefRef, Host}, - run::{AsDef, Def, DynNetMut, LabSet, Mode, Net, Port, Tag, Trg}, - util::create_var, -}; +use crate::{DefRef, Host}; +use hvmc_ast::Tree; +use hvmc_runtime::{dispatch_dyn_net, AsDef, Def, DynNetMut, LabSet, Mode, Net, Port, Tag, Trg}; +use hvmc_util::create_var; /// `@IDENTITY = (x x)` pub const IDENTITY: *const Def = const { &Def::new(LabSet::from_bits(&[1]), (call_identity, call_identity)) }.upcast(); @@ -61,7 +60,7 @@ impl AsHostedDef for LogDef { /// Create a `Host` from a `Book`, including `hvm-core`'s built-in definitions #[cfg(feature = "std")] #[allow(clippy::absolute_paths)] -pub fn create_host(book: &crate::ast::Book) -> Arc> { +pub fn create_host(book: &hvmc_ast::Book) -> Arc> { let host: Arc> = Default::default(); insert_stdlib(host.clone()); @@ -172,8 +171,6 @@ impl AsDef for HostedDef { } } -use crate::ast::Tree; - #[derive(Copy, Clone)] pub struct UniqueTreePtr(*mut Tree); unsafe impl Send for UniqueTreePtr {} diff --git a/paper/atomic_subst.js b/paper/atomic_subst.js deleted file mode 100644 index ccff0f79..00000000 --- a/paper/atomic_subst.js +++ /dev/null @@ -1,116 +0,0 @@ -// This is, as far as I can tell, the most efficient solution to the atomic -// substitution problem: https://www.youtube.com/watch?v=PCAfVdmBYXQ -// -// It is an hybrid of the locking and lock-free approach that will try to -// perform the link with 2 atomic CAS. For each CAS that fails, we place a -// redirection node on the corresponding port. We then start a soft 'link(x)' -// procedure that will *try* to remove redirections. If 'x' is an aux port, we -// do not own it, so, some redirections may remain. If 'x' is a main port (not -// implemented here), we own it, so we will successfully remove all redirections -// after it, and either insert it in an aux port, or find a new active pair. -// This algorithm is superior because, in most cases, the 2 initial CAS will -// succeed, allowing us to avoid storing redirections and calling 'link()'. Both -// Oliver, Derenash and I independently concluded this is the best solution. - -// Atomic Primitives -// ----------------- - -function* atomic_read(idx) { - yield null; - return D[idx]; -} - -function* atomic_swap(idx, val) { - yield null; - let got = D[idx]; - D[idx] = val; - return got; -} - -function* atomic_cas(idx, from, to) { - yield null; - let old = D[idx]; - if (old === from) { - D[idx] = to; - } - return old; -} - -// Atomic Substitution -// ------------------- - -const RED = 0x10; // redirection wire -const GOT = 0xFF; // "taken" placeholder - -const is_red = (x) => x >= RED && x < RED*2; -const is_var = (x) => x >= 0 && x < RED; - -function* atomic_rewrite(tid, a_dir, b_dir) { - var a_ptr = yield* atomic_swap(a_dir, GOT); - var b_ptr = yield* atomic_swap(b_dir, GOT); - yield* atomic_subst(tid, a_ptr, a_dir, b_ptr); - yield* atomic_subst(tid, b_ptr, b_dir, a_ptr); -} - -function* atomic_subst(tid, a_ptr, a_dir, b_ptr) { - var a_got = yield* atomic_cas(a_ptr, a_dir, b_ptr); - yield* atomic_swap(a_dir, a_got == a_dir ? -1 : b_ptr+RED); - yield* atomic_link(tid, a_ptr, b_ptr); -} - -function* atomic_link(tid, dir, val) { - var ptr = yield* atomic_read(dir); - if (is_var(ptr)) { - while (1) { - var trg = yield* atomic_read(ptr); - if (is_red(trg)) { - var got = yield* atomic_cas(dir, ptr, trg-RED); - if (got == ptr) { - yield* atomic_swap(ptr, -1); - ptr = trg - RED; - } - } else if (trg == GOT) { - continue; - } else { - break; - } - } - } -} - -// Utils -// ----- - -function progress(ticks) { - for (var i = 0; i < ticks; ++i) { - var tid = Math.floor(Math.random() * 3); - if (T[tid]) { - let result = T[tid].next(); - if (result.done) { - T[tid] = null; - } - if (!T[0] && !T[1] && !T[2]) { - break; - } - } - } -} - -function show(x) { - if (typeof x == "number") { - return x == -1 ? " " : x == 0xFF ? "::" : ("00"+x.toString(16)).slice(-2); - } else { - return x.map(show).join("|"); - } -} - -// Tests -// ----- - -var I = [0,1,2,3,4,5,6,7]; -var D = [1,0,3,2,5,4,7,6]; -var T = [atomic_rewrite(0,1,2), atomic_rewrite(1,3,4), atomic_rewrite(2,5,6)]; -progress(256); - -console.log(show(I)); -console.log(show(D)); diff --git a/paper/draft.pdf b/paper/draft.pdf deleted file mode 100644 index ddd77470..00000000 Binary files a/paper/draft.pdf and /dev/null differ diff --git a/paper/draft.tex b/paper/draft.tex deleted file mode 100644 index 250811a6..00000000 --- a/paper/draft.tex +++ /dev/null @@ -1,366 +0,0 @@ -\documentclass{article} -\usepackage[utf8]{inputenc} -\usepackage{amsmath, amsthm, amssymb} -\usepackage{graphicx} -\usepackage{algorithm} -\usepackage{algpseudocode} -\usepackage{listings} -\usepackage{xcolor} -\usepackage[export]{adjustbox} - -\definecolor{codegreen}{rgb}{0,0.6,0} -\definecolor{codegray}{rgb}{0.5,0.5,0.5} -\definecolor{codepurple}{rgb}{0.58,0,0.82} -\definecolor{backcolour}{rgb}{0.95,0.95,0.92} - -\lstset{ - language=Python, - basicstyle=\ttfamily\small, - commentstyle=\color{codegreen}, - keywordstyle=\color{magenta}, - numberstyle=\tiny\color{codegray}, - stringstyle=\color{codepurple}, - breakatwhitespace=false, - breaklines=true, - captionpos=b, - keepspaces=true, - numbers=left, - numbersep=5pt, - showspaces=false, - showstringspaces=false, - showtabs=false, - tabsize=2, - frame=single, - backgroundcolor=\color{white} -} - -\title{A Lock-Free Interaction Combinator Evaluator} -\author{Victor Taelin} -\date{} - -\begin{document} - -\maketitle - -\begin{abstract} -We present a lock-free evaluator for interaction combinators, a simple yet powerful model of distributed computation. The proposed algorithm is based on the concept of implicit ownership regions and redirectors, allowing for efficient parallel evaluation without race conditions or deadlocks. We provide detailed illustrations, and the pseudocode for the core procedures can be found in the appendix. We have implemented our lock-free evaluator in CUDA and achieved successful reductions of large graphs on GPUs with thousands of concurrent threads, demonstrating the practicality and efficiency of our approach. -\end{abstract} - -\section{Introduction}\label{s:introduction} - -Interaction nets are a graphical model of computation designed as a generalization of the proof structures of linear logic~\cite{girard1987linear}. They consist of graph-like structures composed of agents and edges, where each agent has a type and an arity connected to other agents via its ports. Interaction nets are inherently distributed, allowing computations to take place simultaneously in many parts of a net without synchronization. This makes them suitable for modeling massive parallelism. - -A specific type of interaction nets is the system called \textit{interaction combinators}, introduced by Lafont~\cite{lafont1997interaction}. These are built from three types of nodes: constructor (CON), duplicator (DUP), and eraser (ERA). Each node has a principal port and two auxiliary ports. The nodes interact according to six rules, divided into two categories: commutation and annihilation. Commutation rules apply when two different types of nodes are connected through their principal ports, while annihilation rules apply when two nodes of the same type are connected through their principal ports. In this paper, we use the symmetric interaction combinator variant, which has been described by Mazza~\cite{mazza2007denotational}, and is illustrated below. - -\begin{figure}[h!] -\centering -\includegraphics[width=1.0\textwidth,margin=4 4 4 4]{interaction_rules.png} -\caption{Interaction rules} -\end{figure} - -Interaction combinators possess several key properties such as determinism, locality, strong confluence (diamond property), and universality. These properties make them particularly suitable for modeling distributed and parallel computations~\cite{lafont1997interaction}. Yet, despite that theoretical appeal, there has been limited practical exploration of their potential for massive parallel computation on modern hardware. In this paper, we propose a lock-free evaluator for interaction combinators that achieves near-ideal speedup on modern GPUs, with several thousand concurrent threads. The heart of our implementation is a lock-free \texttt{link()} procedure, which we describe in detail. - -The paper is organized as follows: Section 2 discusses the limitations of a lock-based evaluator. Section 3 introduces our proposed lock-free evaluator. Section 4 presents an optimization to further increase parallelism. Section 5 discusses the implementation details and the performance of our CUDA-based evaluator~\cite{taelin2022lock}. Finally, Section 6 concludes the paper and discusses future work. - -\section{Former Work}\label{s:former_work} - - -A lock-based evaluator for interaction nets was developed by Sato et al. in their work on Inpla: Interaction Nets as a Programming Language~\cite{hassan2010implementation, sato2014implementation}. Inpla is a multi-threaded parallel interpreter for interaction nets that aims to provide efficient execution of programs in both sequential and parallel environments. Inpla, based on the Lightweight calculus, focuses on the representation, calculus, data structures, and low-level language for implementing interaction nets. Their approach involves using compare-and-swap atomic operations and locking mechanisms, which can be sub-optimal when dealing with parallelism. The locking mechanisms used in Inpla can potentially lead to performance bottlenecks, especially when the checking process for locks spreads globally across the net. - -Another approach, the ingpu evaluator~\cite{jiresch2014towards} was introduced as a GPU-based implementation of interaction nets. The ingpu evaluator, implemented using the CUDA/Thrust library, focused on the interaction and communication phases of the lightweight interaction calculus rather than maintaining an array of redexes and performing explicit graph rewrites. However, the ingpu method faced performance bottlenecks due to sorting and merging arrays, difficulty in representing the irregular graph structure, and varying output size of a reduction. - -\section{Lock-Based Evaluator}\label{s:lockfree} - -The naive approach to reducing interaction combinators in parallel is to represent nets as vectors of nodes, with ports holding pointers to their destinations, and maintain a set of redexes (active pairs) to process concurrently. However, this can lead to race conditions due to overlapping regions. To address this, the affected regions, which include two active nodes and up to 4 surrounding nodes, must be locked. This approach is illustrated below: - -\begin{figure}[h!] -\centering -\includegraphics[width=1.0\textwidth,margin=4 4 4 4]{lock_based_evaluator_1.png} -\caption{Lock-based evaluator step 1} -\end{figure} - -\begin{figure}[h!] -\centering -\includegraphics[width=1.0\textwidth,margin=4 4 4 4]{lock_based_evaluator_2.png} -\caption{Lock-based evaluator step 2} -\end{figure} - -\begin{figure}[h!] -\centering -\includegraphics[width=1.0\textwidth,margin=4 4 4 4]{lock_based_evaluator_3.png} -\caption{Lock-based evaluator step 3} -\end{figure} - -While this strategy works, locking the 6 surrounding nodes can be challenging, potentially leading to a deadlock. For example, consider thread R locking active nodes A and B, while thread G locks active nodes C and D. To proceed with the reduction, thread R must lock C, and thread G must lock B, which is impossible in this situation. - -To avoid deadlocks, one could attempt to lock nodes in an established order, such as from left to right. This prevents deadlocks but may cause a thread to lock an irrelevant region. For instance, suppose thread R locks X and Y immediately before another thread reduces the X node, replacing it with K. Thread R then locks A, B, and C, incorrectly locking the region X, Y, A, B, and C, instead of including the new node K. Consequently, thread R cannot proceed with its reduction. - -Any viable solution to this issue will inevitably involve back-offs or temporary deadlocks, causing the algorithm to rely on the scheduler. This dependence can have negative implications for performance, particularly in architectures such as GPUs, where warps operate in lockstep. The tight synchronization in such systems can exacerbate performance issues, leading to less efficient execution of the algorithm. - -\section{Lock-Free Evaluator}\label{s:lockfree} - -In this section, we propose an efficient lock-free reduction algorithm based on implicit ownership regions and redirection wires. The idea is to process a set of redexes in parallel, but perform rewrites only within implicitly owned regions, eliminating the need for locks. - -When a thread obtains a redex, it assumes ownership of its two active nodes. It then replaces these nodes with redirection wires, allowing the redex to be semantically reduced without affecting the surrounding region. Next, the thread expands its ownership region to include surrounding nodes connected to it via main ports. Finally, the thread invokes a linking procedure that starts from these surrounding main ports and traverses the graph, clearing redirection wires until arriving at a port outside its ownership region. At this point, there are two cases to consider: - -1. If the outside port is an auxiliary port, the thread attempts to connect the surrounding main port to it using an atomic compare-exchange operation and clears the backward path. - -2. If it is a main port, the thread has found a new active pair and coordinates with the opposing thread — which will traverse the backward path — to create a new redex. - -The following illustrations showcase this process: - -\begin{figure}[h!] -\centering -\includegraphics[width=1.0\textwidth,margin=4 4 4 4]{lock_free_evaluator_1.png} -\caption{Lock-free evaluator step 1} -\end{figure} - -\begin{figure}[h!] -\centering -\includegraphics[width=1.0\textwidth,margin=4 4 4 4]{lock_free_evaluator_2.png} -\caption{Lock-free evaluator step 2} -\end{figure} - -\begin{figure}[h!] -\centering -\includegraphics[width=1.0\textwidth,margin=4 4 4 4]{lock_free_evaluator_3.png} -\caption{Lock-free evaluator step 3} -\end{figure} - -\begin{figure}[h!] -\centering -\includegraphics[width=1.0\textwidth,margin=4 4 4 4]{lock_free_evaluator_4.png} -\caption{Lock-free evaluator step 4} -\end{figure} - -\begin{figure}[h!] -\centering -\includegraphics[width=1.0\textwidth,margin=4 4 4 4]{lock_free_evaluator_5.png} -\caption{Lock-free evaluator step 5} -\end{figure} - -\begin{figure}[h!] -\centering -\includegraphics[width=1.0\textwidth,margin=4 4 4 4]{lock_free_evaluator_6.png} -\caption{Lock-free evaluator step 6} -\end{figure} - -\begin{figure}[h!] -\centering -\includegraphics[width=1.0\textwidth,margin=4 4 4 4]{lock_free_evaluator_7.png} -\caption{Lock-free evaluator step 7} -\end{figure} - -Note that in Image 2, the reduction is semantically complete, with ports X1-W0 and Y0-Z0 indirectly connected. Each thread independently performs this reduction, without impacting surrounding nodes, by converting the auxiliary ports of their owned nodes to redirectors and then swapping opposing ports (e.g., A1 goes to B1, B2 goes to A2, etc.). This approach entirely avoids locks. The link() procedure in the appendix is then only needed to clear the memory occupied by redirectors and to identify new active pairs. As this clean-up process may cross ownership regions, caution is required to deploy atomics. - -\section{Optimization: 1/4 rewrites}\label{s:lockfree} - -The lock-free evaluator procedure above reveals a symmetry that can be exploited to further increase the parallelism of the algorithm. Notice how thread R performs the same logic 4 times: - -\begin{itemize} - \item Move A1 as redirector to B1; if target is main, link B1 towards A1. - \item Move A2 as redirector to B2; if target is main, link B2 towards A2. - \item Move B1 as redirector to A1; if target is main, link A1 towards B1. - \item Move B2 as redirector to A2; if target is main, link A2 towards B2. -\end{itemize} - -These 4 segments are, themselves, independent, and can be performed in parallel. This allows a single interaction to be processed by 4 threads, each one performing 1/4 of a rewrite. According to Amdahl's law, the maximum speedup achievable through parallelization is limited by the fraction of the program that must be executed sequentially. By dividing the rewrite process into 4 smaller sequential chunks, we effectively reduce the sequential portion of the algorithm, increasing the maximum potential speedup by 4x. The pseudocode for this optimized approach can be found in the appendix, completing the algorithm and allowing for efficient parallel execution of interaction combinators. - -\section{Implementation}\label{s:implementation} - -The lock-free evaluator algorithm has been implemented in CUDA~\cite{taelin2022lock} and has successfully reduced large graphs on GPUs with thousands of concurrent threads, suggesting the algorithm's correctness, even though a formal proof is not provided yet. Moreover, our CUDA implementation demonstrates efficient performance, reaching 900 million rewrites per second on an RTX 4090, which is 15 times more than a single-core CPU evaluator. This significant speedup confirms the practical applicability and efficiency of our approach. - -\section{Conclusion}\label{s:conclusion} - -In this paper, we have presented a lock-free evaluator for interaction combinators that achieves near-ideal speedup on modern GPUs. Our approach leverages the inherent parallelism of interaction combinators and avoids the pitfalls of lock-based evaluators. We also introduced an optimization that further increases the potential for parallelism by breaking down rewrites into smaller sequential chunks. Our CUDA implementation demonstrates the practicality and efficiency of our approach, as well as suggesting its correctness. Future work includes further optimizations, exploring other applications of our lock-free evaluator, and providing formal proofs of correctness. - -\begin{thebibliography}{9} -\bibitem{girard1987linear} -Girard, J. Y. (1987). Linear logic. Theoretical computer science, 50(1), 1-101. - -\bibitem{lafont1997interaction} -Lafont, Y. (1997). Interaction combinators. Information and Computation, 137(1), 69-101. - -\bibitem{jiresch2014towards} -Jiresch, E. (2014). Towards a GPU-based implementation of interaction nets. arXiv preprint arXiv:1404.0076. - -\bibitem{mazza2007denotational} -Mazza, D. (2007). A denotational semantics for the symmetric interaction combinators. Mathematical Structures in Computer Science, 17(3), 527-562. - -\bibitem{sato2014implementation} -Sato, S. (2014). Design and implementation of a low-level language for interaction nets. Ph.D. Thesis, University of Sussex. - -\bibitem{taelin2022lock} -Taelin, V. (2022). Higher-order Virtual Machine [Online]. Available at: https://github.com/HigherOrderCO/hvm-core -\end{thebibliography} - -\appendix -\section{Pseudocode}\label{app:pseudocode} - -In this appendix, we provide the complete pseudocode for the lock-free evaluator algorithm and the 1/4 rewrites optimization. - -\begin{lstlisting} -# Atomically links the node in 'src_ref' towards 'dir_ptr'. -def link(src_ref: &Pointer, dir_ptr: Pointer): - while True: - # Peek the target, which may not be owned by us. - trg_ref = dir_ptr.target() - trg_ptr = trg_ref.read() - - # If target is a redirection, clear and move forward. - if trg_ptr.is_red(): - - # We own the redirection, so we can mutate it. - trg_ref.write(0) - dir_ptr = trg_ptr - continue - - # If target is an aux port, try replacing it with the node. - elif trg_ptr.is_aux(): - - # Peeks the source node. - src_ptr = src_ref.read() - - # We don't own the port, so we try replacing it. - if trg_ref.compare_exchange(trg_ptr, src_ptr) == trg_ptr: - # Collect the orphaned backward path. - trg_ref = dir_ptr.target() - trg_ptr = trg_ref.read() - while trg_ptr.is_red(): - trg_ref.write(0) - trg_ref = trg_ptr.target() - trg_ptr = trg_ref.read() - - # Clear source location. - src_ref.write(0) - return - - # If the compare_exchange failed, we try again. - else: - continue - - # If target is a main port, two threads reach this branch. - elif trg_ptr.is_nod() or trg_ptr.is_tmp(): - - # Sort references, to avoid deadlocks. - fst_ref, snd_ref = sort(src_ref, trg_ref) - - # Swap the first reference by TMP placeholder. - fst_ptr = fst_ref.exchange(TMP) - - # First to arrive creates a redex. - if !fst_ptr.is_tmp(): - snd_ptr = snd_ref.exchange(TMP) - add_redex(fst_ptr, snd_ptr) - return - - # Second to arrive clears up the memory. - else: - fst_ref.write(0) - while snd_ref.compare_exchange(snd_ptr, 0) != snd_ptr: - continue - return - - # If it is taken, we wait. - else: - continue -\end{lstlisting} - -\begin{lstlisting} -# Performs 1/4 con-con interaction -def con_con(a_ptr: &Pointer, b_ptr: &Pointer, port: Port): - - # Gets a reference current and opposing ports - a_aux_ref = &a_ptr.port[port] - b_aux_ref = &b_ptr.port[port] - - # Takes the current port and casts to redirector - a_aux_ptr = a_aux_ref.exchange(0).as_redirector() - - # Synchronizes local threads - local_threads.sync() - - # Sends current port to opposing port - b_aux_ref.write(a_aux_ptr) - - # Synchronizes local threads - local_threads.sync() - - # If the current port targeted a main port... - if a_aux_ptr.targets_main(): - - # Link the opposing port towards the current port - link(b_aux_ref, new_ptr_to(a_aux_ptr)) - -# Performs 1/4 con-dup interaction -def con_dup(a_ptr: &Pointer, b_ptr: &Pointer, port: Port): - - # Gets reference to both aux ptrs - a_aux_ref = &a_ptr.port[port] - b_aux_ref = &b_ptr.port[port] - - # Takes my aux ptr - a_aux_ptr = a_aux_ref.exchange(0).as_redirector() - - # Allocates a new clone - clone_loc = malloc(1 * size(Ptr)) - clone_ptr = mkptr(a_ptr.tag(), clone_loc) - - # Synchronizes local threads - local_threads.sync() - - # Communicates this clone's loc to local threads - local_threads.send_clone_loc(clone_loc) - - # Synchronizes local threads - local_threads.sync() - - # Gets opposing clone locs - clone_x_loc, clone_y_loc = local_threads.receive_clone_locs() - - # Fills clone inner wires - clone_loc[1] = clone_x_loc - clone_loc[2] = clone_y_loc - - # Sends clone to opposing port - b_aux_ref.write(clone_ptr) - - # Synchronizes local threads - local_threads.sync() - - # If the current port targeted an aux port... - if a_aux_ptr.targets_aux(): - - # Link the current port towards its former target - link(a_aux_ref, a_aux_ptr) - - # If the current port targeted a main port... - if a_aux_ptr.is_main(): - - # Form a new redex between its former and current target - create_redex(a_aux_ptr, a_aux_ref.exchange(0)) -\end{lstlisting} - -\begin{lstlisting} -def rewrite(net): - # Performs each interaction, in parallel - p-for (a,b) in net.redexes: - - # Sets up the 1/4 interactions - quarters = [(a,b,1), (a,b,2), (b,a,1), (b,a,2)] - - # Performs each 1/4 interaction, in parallel - p-for (a, b, port) in quarters: - match (a_ptr.tag(), b_ptr.tag()): - (CON, CON) => con_con(a, b, port) - (CON, DUP) => con_dup(a, b, port) - (CON, ERA) => con_era(a, b, port) - (DUP, CON) => dup_con(b, a, port) - (DUP, DUP) => dup_dup(b, b, port) - (DUP, ERA) => dup_era(a, b, port) - (ERA, CON) => era_con(b, a, port) - (ERA, DUP) => era_dup(b, b, port) - (ERA, ERA) => era_era(a, b, port) -\end{lstlisting} - -\end{document} diff --git a/paper/draft_lol.pdf b/paper/draft_lol.pdf deleted file mode 100644 index 23871862..00000000 Binary files a/paper/draft_lol.pdf and /dev/null differ diff --git a/paper/images/interaction_rules.drawio b/paper/images/interaction_rules.drawio deleted file mode 100644 index 6b38e515..00000000 --- a/paper/images/interaction_rules.drawio +++ /dev/null @@ -1,518 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/paper/images/interaction_rules.png b/paper/images/interaction_rules.png deleted file mode 100644 index c416a63f..00000000 Binary files a/paper/images/interaction_rules.png and /dev/null differ diff --git a/paper/images/lock_based_evaluator.drawio b/paper/images/lock_based_evaluator.drawio deleted file mode 100644 index f2348afd..00000000 --- a/paper/images/lock_based_evaluator.drawio +++ /dev/null @@ -1,676 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/paper/images/lock_based_evaluator.png b/paper/images/lock_based_evaluator.png deleted file mode 100644 index a02d1872..00000000 Binary files a/paper/images/lock_based_evaluator.png and /dev/null differ diff --git a/paper/images/lock_based_evaluator_1.png b/paper/images/lock_based_evaluator_1.png deleted file mode 100644 index edff2f3f..00000000 Binary files a/paper/images/lock_based_evaluator_1.png and /dev/null differ diff --git a/paper/images/lock_based_evaluator_2.png b/paper/images/lock_based_evaluator_2.png deleted file mode 100644 index 234cd919..00000000 Binary files a/paper/images/lock_based_evaluator_2.png and /dev/null differ diff --git a/paper/images/lock_based_evaluator_3.png b/paper/images/lock_based_evaluator_3.png deleted file mode 100644 index 1de85090..00000000 Binary files a/paper/images/lock_based_evaluator_3.png and /dev/null differ diff --git a/paper/images/lock_free_evaluator.drawio b/paper/images/lock_free_evaluator.drawio deleted file mode 100644 index a7905408..00000000 --- a/paper/images/lock_free_evaluator.drawio +++ /dev/null @@ -1,1816 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/paper/images/lock_free_evaluator.png b/paper/images/lock_free_evaluator.png deleted file mode 100644 index f7300295..00000000 Binary files a/paper/images/lock_free_evaluator.png and /dev/null differ diff --git a/paper/images/lock_free_evaluator_1.png b/paper/images/lock_free_evaluator_1.png deleted file mode 100644 index 80e13d37..00000000 Binary files a/paper/images/lock_free_evaluator_1.png and /dev/null differ diff --git a/paper/images/lock_free_evaluator_2.png b/paper/images/lock_free_evaluator_2.png deleted file mode 100644 index 29b83cec..00000000 Binary files a/paper/images/lock_free_evaluator_2.png and /dev/null differ diff --git a/paper/images/lock_free_evaluator_3.png b/paper/images/lock_free_evaluator_3.png deleted file mode 100644 index 9d7d26a0..00000000 Binary files a/paper/images/lock_free_evaluator_3.png and /dev/null differ diff --git a/paper/images/lock_free_evaluator_4.png b/paper/images/lock_free_evaluator_4.png deleted file mode 100644 index f0495ba0..00000000 Binary files a/paper/images/lock_free_evaluator_4.png and /dev/null differ diff --git a/paper/images/lock_free_evaluator_5.png b/paper/images/lock_free_evaluator_5.png deleted file mode 100644 index 305608f1..00000000 Binary files a/paper/images/lock_free_evaluator_5.png and /dev/null differ diff --git a/paper/images/lock_free_evaluator_6.png b/paper/images/lock_free_evaluator_6.png deleted file mode 100644 index 80a9474c..00000000 Binary files a/paper/images/lock_free_evaluator_6.png and /dev/null differ diff --git a/paper/images/lock_free_evaluator_7.png b/paper/images/lock_free_evaluator_7.png deleted file mode 100644 index 51747c2f..00000000 Binary files a/paper/images/lock_free_evaluator_7.png and /dev/null differ diff --git a/prelude.rs b/prelude.rs new file mode 100644 index 00000000..cf38a55a --- /dev/null +++ b/prelude.rs @@ -0,0 +1,22 @@ +extern crate alloc; + +#[allow(unused)] +mod prelude { + + pub use alloc::{ + borrow::ToOwned, + boxed::Box, + format, + string::{String, ToString}, + vec, + vec::Vec, + }; + + pub use core::{fmt, hint, iter, mem, ptr}; + + #[cfg(feature = "std")] + pub use std::collections::{hash_map::Entry, HashMap as Map, HashSet as Set}; + + #[cfg(not(feature = "std"))] + pub use alloc::collections::{btree_map::Entry, BTreeMap as Map, BTreeSet as Set}; +} diff --git a/runtime/Cargo.toml b/runtime/Cargo.toml new file mode 100644 index 00000000..74cd9295 --- /dev/null +++ b/runtime/Cargo.toml @@ -0,0 +1,20 @@ +[package] +name = "hvmc-runtime" +version = "0.1.0" +edition = "2021" + +[lib] +path = "src/runtime.rs" + +[dependencies] +parking_lot = "0.12.2" +nohash-hasher = { version = "0.2.0", optional = true } + +hvmc-util = { path = "../util", default-features = false } + +[features] +std = ["hvmc-util/std", "dep:nohash-hasher"] +trace = [] + +[lints] +workspace = true diff --git a/src/run/addr.rs b/runtime/src/addr.rs similarity index 95% rename from src/run/addr.rs rename to runtime/src/addr.rs index c27eac8d..b0adf117 100644 --- a/src/run/addr.rs +++ b/runtime/src/addr.rs @@ -12,7 +12,8 @@ use super::*; #[must_use] pub struct Addr(pub usize); -impl IsEnabled for Addr {} +#[cfg(feature = "std")] +impl nohash_hasher::IsEnabled for Addr {} impl fmt::Debug for Addr { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { diff --git a/src/run/allocator.rs b/runtime/src/allocator.rs similarity index 100% rename from src/run/allocator.rs rename to runtime/src/allocator.rs diff --git a/src/run/def.rs b/runtime/src/def.rs similarity index 94% rename from src/run/def.rs rename to runtime/src/def.rs index 1fbf3eb8..652c056e 100644 --- a/src/run/def.rs +++ b/runtime/src/def.rs @@ -1,5 +1,3 @@ -use crate::stdlib::AsHostedDef; - use super::*; /// A bitset representing the set of labels used in a def. @@ -48,6 +46,10 @@ impl LabSet { } } + pub fn bits(&self) -> &[u64] { + &self.bits + } + pub const fn from_bits(bits: &'static [u64]) -> Self { if bits.is_empty() { return LabSet::NONE; @@ -199,14 +201,30 @@ impl<'a, M: Mode> Net<'a, M> { /// [`Def`]s, when not pre-compiled, are represented as lists of instructions. #[derive(Debug, Default, Clone)] pub struct InterpretedDef { - pub(crate) instr: Vec, + pub instr: Vec, /// The number of targets used in the def; must be greater than all of the /// `TrgId` indices in `instr`. - pub(crate) trgs: usize, + pub trgs: usize, +} + +impl InterpretedDef { + #[inline(always)] + pub fn instructions(&self) -> &[Instruction] { + &self.instr + } +} + +impl InterpretedDef { + pub fn new_trg_id(&mut self) -> TrgId { + let index = self.trgs; + self.trgs += 1; + TrgId::new(index) + } } -impl AsHostedDef for InterpretedDef { - fn call(def: &Def, net: &mut Net, trg: Port) { +impl AsDef for InterpretedDef { + unsafe fn call(def: *const Def, net: &mut Net, trg: Port) { + let def = unsafe { &*def }; let def = &def.data; let instructions = &def.instr; diff --git a/src/run/dyn_net.rs b/runtime/src/dyn_net.rs similarity index 94% rename from src/run/dyn_net.rs rename to runtime/src/dyn_net.rs index ec80a0b5..52e2a49b 100644 --- a/src/run/dyn_net.rs +++ b/runtime/src/dyn_net.rs @@ -52,8 +52,8 @@ impl<'h, M: Mode> Net<'h, M> { macro_rules! dispatch_dyn_net { ($pat:pat = $expr:expr => $body:expr) => { match $expr { - $crate::run::DynNetInner::Lazy($pat) => $body, - $crate::run::DynNetInner::Strict($pat) => $body, + $crate::DynNetInner::Lazy($pat) => $body, + $crate::DynNetInner::Strict($pat) => $body, } }; ($net:ident => $body:expr) => { diff --git a/src/run/instruction.rs b/runtime/src/instruction.rs similarity index 94% rename from src/run/instruction.rs rename to runtime/src/instruction.rs index 1574d182..61d46d76 100644 --- a/src/run/instruction.rs +++ b/runtime/src/instruction.rs @@ -110,7 +110,7 @@ impl fmt::Debug for TrgId { impl<'a, M: Mode> Net<'a, M> { /// `trg ~ {#lab x y}` #[inline(always)] - pub(crate) fn do_ctr(&mut self, lab: Lab, trg: Trg) -> (Trg, Trg) { + pub fn do_ctr(&mut self, lab: Lab, trg: Trg) -> (Trg, Trg) { let port = trg.target(); #[allow(clippy::overly_complex_bool_expr)] if !M::LAZY && port.tag() == Ctr && port.lab() == lab { @@ -133,7 +133,7 @@ impl<'a, M: Mode> Net<'a, M> { /// `trg ~ ` #[inline(always)] - pub(crate) fn do_op(&mut self, op: Op, trg: Trg) -> (Trg, Trg) { + pub fn do_op(&mut self, op: Op, trg: Trg) -> (Trg, Trg) { trace!(self.tracer, op, trg); let port = trg.target(); if !M::LAZY && port.is_num() { @@ -153,7 +153,7 @@ impl<'a, M: Mode> Net<'a, M> { /// `trg ~ ` #[inline(always)] - pub(crate) fn do_op_num(&mut self, op: Op, trg: Trg, rhs: Port) -> Trg { + pub fn do_op_num(&mut self, op: Op, trg: Trg, rhs: Port) -> Trg { let port = trg.target(); if !M::LAZY && port.is_num() { self.rwts.oper += 1; @@ -175,7 +175,7 @@ impl<'a, M: Mode> Net<'a, M> { /// `trg ~ ?` #[inline(always)] - pub(crate) fn do_mat(&mut self, trg: Trg) -> (Trg, Trg) { + pub fn do_mat(&mut self, trg: Trg) -> (Trg, Trg) { let port = trg.target(); if !M::LAZY && port.tag() == Int { self.rwts.oper += 1; @@ -204,7 +204,7 @@ impl<'a, M: Mode> Net<'a, M> { } #[inline(always)] - pub(crate) fn do_wires(&mut self) -> (Trg, Trg, Trg, Trg) { + pub fn do_wires(&mut self) -> (Trg, Trg, Trg, Trg) { let a = self.alloc(); let b = a.other_half(); (Trg::port(Port::new_var(a)), Trg::wire(Wire::new(a)), Trg::port(Port::new_var(b)), Trg::wire(Wire::new(b))) @@ -213,7 +213,7 @@ impl<'a, M: Mode> Net<'a, M> { /// `trg ~ ?<(x (y z)) out>` #[inline(always)] #[allow(unused)] // TODO: emit this instruction - pub(crate) fn do_mat_con_con(&mut self, trg: Trg, out: Trg) -> (Trg, Trg, Trg) { + pub fn do_mat_con_con(&mut self, trg: Trg, out: Trg) -> (Trg, Trg, Trg) { let port = trg.target(); if !M::LAZY && trg.target().tag() == Int { self.rwts.oper += 1; @@ -241,7 +241,7 @@ impl<'a, M: Mode> Net<'a, M> { /// `trg ~ ?<(x y) out>` #[inline(always)] #[allow(unused)] // TODO: emit this instruction - pub(crate) fn do_mat_con(&mut self, trg: Trg, out: Trg) -> (Trg, Trg) { + pub fn do_mat_con(&mut self, trg: Trg, out: Trg) -> (Trg, Trg) { let port = trg.target(); if trg.target().tag() == Int { self.rwts.oper += 1; diff --git a/src/run/interact.rs b/runtime/src/interact.rs similarity index 99% rename from src/run/interact.rs rename to runtime/src/interact.rs index 358e576e..973b3068 100644 --- a/src/run/interact.rs +++ b/runtime/src/interact.rs @@ -115,6 +115,7 @@ impl<'a, M: Mode> Net<'a, M> { /// | | /// b1 | | b2 /// ``` + #[allow(non_snake_case)] #[inline(never)] pub fn comm22(&mut self, a: Port, b: Port) { trace!(self.tracer, a, b); diff --git a/src/run/linker.rs b/runtime/src/linker.rs similarity index 99% rename from src/run/linker.rs rename to runtime/src/linker.rs index f909831c..61da3084 100644 --- a/src/run/linker.rs +++ b/runtime/src/linker.rs @@ -1,5 +1,10 @@ use super::*; +#[cfg(not(feature = "std"))] +use crate::prelude::Map as IntMap; +#[cfg(feature = "std")] +use nohash_hasher::IntMap; + /// Stores extra data needed about the nodes when in lazy mode. (In strict mode, /// this is unused.) pub(super) struct Header { diff --git a/src/run/net.rs b/runtime/src/net.rs similarity index 100% rename from src/run/net.rs rename to runtime/src/net.rs diff --git a/src/run/node.rs b/runtime/src/node.rs similarity index 100% rename from src/run/node.rs rename to runtime/src/node.rs diff --git a/src/run/parallel.rs b/runtime/src/parallel.rs similarity index 100% rename from src/run/parallel.rs rename to runtime/src/parallel.rs diff --git a/src/run/port.rs b/runtime/src/port.rs similarity index 100% rename from src/run/port.rs rename to runtime/src/port.rs diff --git a/src/run.rs b/runtime/src/runtime.rs similarity index 83% rename from src/run.rs rename to runtime/src/runtime.rs index cdcb3e92..98a2faff 100644 --- a/src/run.rs +++ b/runtime/src/runtime.rs @@ -17,15 +17,18 @@ //! the *auxiliary ports* of the net (as managed by the linker); the target of //! the principal port is left implicit //! - active pairs are thus stored in a dedicated vector, `net.redexes` +#![feature(const_type_id, extern_types, inline_const, new_uninit)] +#![cfg_attr(feature = "trace", feature(const_type_name))] + +include!("../../prelude.rs"); use crate::prelude::*; -use crate::{ - ops::TypedOp as Op, - trace, - trace::Tracer, - util::{bi_enum, deref}, -}; +pub use hvmc_util::ops; + +use hvmc_util::{bi_enum, deref, pretty_num}; + +use self::trace::Tracer; use alloc::borrow::Cow; use core::{ alloc::Layout, @@ -35,16 +38,10 @@ use core::{ mem::size_of, ops::{Add, AddAssign, Deref, DerefMut}, }; -use nohash_hasher::{IntMap, IsEnabled}; +use ops::TypedOp as Op; -#[cfg(feature = "_fuzz")] -use crate::fuzz as atomic; -#[cfg(not(feature = "_fuzz"))] use core::sync::atomic; -#[cfg(feature = "_fuzz")] -use crate::fuzz::spin_loop; -#[cfg(not(feature = "_fuzz"))] fn spin_loop() {} // this could use `std::hint::spin_loop`, but in practice it hurts performance use atomic::{AtomicU64, Ordering::Relaxed}; @@ -62,6 +59,7 @@ mod net; mod node; mod parallel; mod port; +pub mod trace; mod wire; pub use addr::*; @@ -150,6 +148,7 @@ impl Add for Rewrites { } } } + impl AddAssign for Rewrites { fn add_assign(&mut self, rhs: Self) { self.anni += rhs.anni; @@ -159,3 +158,15 @@ impl AddAssign for Rewrites { self.oper += rhs.oper; } } + +impl fmt::Display for Rewrites { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + writeln!(f, "RWTS : {:>15}", pretty_num(self.total()))?; + writeln!(f, "- ANNI : {:>15}", pretty_num(self.anni))?; + writeln!(f, "- COMM : {:>15}", pretty_num(self.comm))?; + writeln!(f, "- ERAS : {:>15}", pretty_num(self.eras))?; + writeln!(f, "- DREF : {:>15}", pretty_num(self.dref))?; + writeln!(f, "- OPER : {:>15}", pretty_num(self.oper))?; + Ok(()) + } +} diff --git a/src/trace.rs b/runtime/src/trace.rs similarity index 97% rename from src/trace.rs rename to runtime/src/trace.rs index 4c8ba867..60a9cbb4 100644 --- a/src/trace.rs +++ b/runtime/src/trace.rs @@ -61,21 +61,21 @@ //! [`_reset_traces()`] before each iteration, to discard the traces of the //! previous iteration. +#![allow(non_snake_case)] #![cfg_attr(not(feature = "trace"), allow(unused))] -use crate::prelude::*; - use core::{ cell::UnsafeCell, fmt::{self, Debug, Formatter, Write}, sync::atomic::{AtomicBool, AtomicU64, Ordering}, }; + use parking_lot::{Mutex, Once}; -use crate::{ - ops::TypedOp as Op, - run::{Addr, Port, Trg, Wire}, -}; +use crate::prelude::*; +use hvmc_util::ops::TypedOp as Op; + +use crate::{Addr, Port, Trg, Wire}; #[cfg(not(feature = "trace"))] #[derive(Default)] @@ -99,7 +99,7 @@ macro_rules! trace { impl $crate::trace::TraceSourceBearer for __ { const SOURCE: $crate::trace::TraceSource = $crate::trace::TraceSource { func: { - #[cfg(feature = "trace")] { core::any::type_name::<__>() } + #[cfg(feature = "trace")] { $crate::trace::type_name::<__>() } #[cfg(not(feature = "trace"))] { "" } }, file: file!(), @@ -475,3 +475,9 @@ pub fn set_hook() { }) } } + +#[cfg(feature = "trace")] +#[allow(clippy::absolute_paths)] +pub const fn type_name() -> &'static str { + core::any::type_name::() +} diff --git a/src/run/wire.rs b/runtime/src/wire.rs similarity index 100% rename from src/run/wire.rs rename to runtime/src/wire.rs diff --git a/src/args.rs b/src/args.rs new file mode 100644 index 00000000..bd13f75b --- /dev/null +++ b/src/args.rs @@ -0,0 +1,143 @@ +use clap::Args; +use hvmc_transform::TransformPasses; +use std::path::PathBuf; + +#[derive(Args, Clone, Debug)] +pub struct RunArgs { + /// Name of the definition that will get reduced. + #[arg(short, default_value = "main")] + pub entry_point: String, + + /// List of arguments to pass to the program. + /// + /// Arguments are passed using the lambda-calculus interpretation + /// of interaction combinators. So, for example, if the arguments are + /// "#1" "#2" "#3", then the expression that will get reduced is + /// `r & @main ~ (#1 (#2 (#3 r)))`. + pub args: Vec, +} + +#[derive(Args, Clone, Debug)] +pub struct RuntimeOpts { + /// Show performance statistics. + #[arg(short, long = "stats")] + pub show_stats: bool, + + /// Single-core mode (no parallelism). + #[arg(short = '1', long = "single")] + pub single_core: bool, + + /// Lazy mode. + /// + /// Lazy mode only expands references that are reachable + /// by a walk from the root of the net. This leads to a dramatic slowdown, + /// but allows running programs that would expand indefinitely otherwise. + #[arg(short, long = "lazy")] + pub lazy_mode: bool, + + /// How much memory to allocate on startup. + /// + /// Supports abbreviations such as '4G' or '400M'. + #[arg(short, long, value_parser = hvmc_util::parse_abbrev_number::)] + pub memory: Option, + + /// Dynamic library hvm-core files to include. + /// + /// hvm-core files can be compiled as dylibs with the `--dylib` option. + #[arg(short, long, value_delimiter = ' ', action = clap::ArgAction::Append)] + pub include: Vec, +} + +#[derive(Clone, Debug, Args)] +#[non_exhaustive] +pub struct TransformOpts { + /// Names of the definitions that should not get pre-reduced. + /// + /// For programs that don't take arguments and don't have side effects this is + /// usually the entry point of the program (otherwise, the whole program will + /// get reduced to normal form). + #[arg(long = "pre-reduce-skip", value_delimiter = ' ', action = clap::ArgAction::Append)] + pub pre_reduce_skip: Vec, + + /// How much memory to allocate when pre-reducing. + /// + /// Supports abbreviations such as '4G' or '400M'. + #[arg(long = "pre-reduce-memory", value_parser = hvmc_util::parse_abbrev_number::)] + pub pre_reduce_memory: Option, + + /// Maximum amount of rewrites to do when pre-reducing. + /// + /// Supports abbreviations such as '4G' or '400M'. + #[arg(long = "pre-reduce-rewrites", default_value = "100M", value_parser = hvmc_util::parse_abbrev_number::)] + pub pre_reduce_rewrites: u64, + + /// Names of the definitions that should not get pruned. + #[arg(long = "prune-entrypoints", default_value = "main")] + pub prune_entrypoints: Vec, +} + +#[derive(Args, Clone, Debug)] +pub struct TransformArgs { + /// Enables or disables transformation passes. + #[arg(short = 'O', value_delimiter = ' ', action = clap::ArgAction::Append)] + pub transform_passes: Vec, + + #[command(flatten)] + pub transform_opts: TransformOpts, +} + +macro_rules! transform_passes { + ($($pass:ident: $name:literal $(| $alias:literal)*),* $(,)?) => { + #[derive(Debug, Clone, Copy)] + #[allow(non_camel_case_types)] + pub enum TransformPass { + all(bool), + $($pass(bool),)* + } + + impl clap::ValueEnum for TransformPass { + fn value_variants<'a>() -> &'a [Self] { + &[ + Self::all(true), Self::all(false), + $(Self::$pass(true), Self::$pass(false),)* + ] + } + + fn to_possible_value(&self) -> Option { + use TransformPass::*; + Some(match self { + all(true) => clap::builder::PossibleValue::new("all"), + all(false) => clap::builder::PossibleValue::new("no-all"), + $( + $pass(true) => clap::builder::PossibleValue::new($name)$(.alias($alias))*, + $pass(false) => clap::builder::PossibleValue::new(concat!("no-", $name))$(.alias(concat!("no-", $alias)))*, + )* + }) + } + } + + impl TransformPass { + pub fn to_passes(args: &[TransformPass]) -> TransformPasses { + use TransformPass::*; + let mut opts = TransformPasses::NONE; + for arg in args { + match arg { + all(true) => opts = TransformPasses::ALL, + all(false) => opts = TransformPasses::NONE, + $(&$pass(b) => opts.$pass = b,)* + } + } + opts + } + } + }; +} + +transform_passes! { + pre_reduce: "pre-reduce" | "pre", + coalesce_ctrs: "coalesce-ctrs" | "coalesce", + encode_adts: "encode-adts" | "adts", + eta_reduce: "eta-reduce" | "eta", + inline: "inline", + prune: "prune", +} diff --git a/src/bare.rs b/src/bare.rs new file mode 100644 index 00000000..8c368d9b --- /dev/null +++ b/src/bare.rs @@ -0,0 +1,12 @@ +use crate::{RunArgs, RuntimeOpts}; + +use clap::Parser; + +#[derive(Parser, Debug)] +#[command(author, version)] +pub struct BareCli { + #[command(flatten)] + pub opts: RuntimeOpts, + #[command(flatten)] + pub args: RunArgs, +} diff --git a/src/compile.rs b/src/compile.rs index ab2eadd1..9855fb6b 100644 --- a/src/compile.rs +++ b/src/compile.rs @@ -1,20 +1,20 @@ -#![cfg(feature = "std")] +mod include_files; -use alloc::collections::{BTreeMap, BTreeSet}; +pub use include_files::*; -use crate::prelude::*; - -use crate::{ - host::Host, - run::{Def, Instruction, InterpretedDef, LabSet, Port, Tag}, - stdlib::HostedDef, +use std::{ + collections::{BTreeMap, BTreeSet}, + fmt::Write, + hash::{DefaultHasher, Hasher}, }; -use core::{fmt::Write, hash::Hasher}; -use std::hash::DefaultHasher; + +use crate::prelude::*; +use hvmc_host::Host; +use hvmc_runtime::{Def, Instruction, InterpretedDef, LabSet, Port, Tag}; struct DefInfo<'a> { rust_name: String, - def: &'a Def>, + def: &'a Def, refs: BTreeSet<&'a str>, } @@ -23,63 +23,78 @@ pub fn compile_host(host: &Host) -> String { _compile_host(host).unwrap() } +const HVMC_VERSION: &str = env!("CARGO_PKG_VERSION"); +const RUST_VERSION: &str = env!("RUSTC_VERSION"); + +/// Compiles a [`Host`] to Rust, returning a file to replace `gen.rs`. +/// Unlike [`compile_host`], this returns a [`Result`] instead of panicking. fn _compile_host(host: &Host) -> Result { let mut code = String::default(); let mut def_infos: BTreeMap<&str, DefInfo<'_>> = BTreeMap::new(); for (hvmc_name, def) in &host.defs { - if let Some(def) = def.downcast_ref::>() { + if let Some(def) = def.downcast_ref::() { def_infos.insert(hvmc_name, DefInfo { rust_name: sanitize_name(hvmc_name), - refs: refs(host, &def.data.0.instr), + refs: refs(host, def.data.instructions()), def, }); } } - writeln!(code, "#![allow(warnings)]")?; - writeln!( + write!( code, - "use crate::{{host::Host, stdlib::{{AsHostedDef, HostedDef}}, run::*, ops::{{TypedOp, Ty::*, Op::*}}}};" - )?; - writeln!(code)?; + " +#![no_std] +#![allow(warnings)] - writeln!(code, "pub fn insert_into_host(host: &mut Host) {{")?; +extern crate alloc; - // insert empty defs - for (hvmc_name, DefInfo { rust_name, def, .. }) in &def_infos { - let labs = compile_lab_set(&def.labs)?; - writeln!( - code, - r##" host.insert_def(r#"{hvmc_name}"#, unsafe {{ HostedDef::::new({labs}) }});"##, - )?; - } - writeln!(code)?; +use hvmc_runtime::{{*, ops::{{TypedOp, Ty::*, Op::*}}}}; +use core::ops::DerefMut; +use alloc::boxed::Box; + +#[no_mangle] +pub fn hvmc_dylib_v0__hvmc_version() -> &'static str {{ + {HVMC_VERSION:?} +}} - // hoist all unique refs present in the right hand side of some def - for hvmc_name in def_infos.values().flat_map(|info| &info.refs).collect::>() { - let rust_name = &def_infos[hvmc_name].rust_name; +#[no_mangle] +pub fn hvmc_dylib_v0__rust_version() -> &'static str {{ + {RUST_VERSION:?} +}} - writeln!(code, r##" let def_{rust_name} = Port::new_ref(&host.defs[r#"{hvmc_name}"#]);"##)?; +#[no_mangle] +pub fn hvmc_dylib_v0__insert_into(insert: &mut dyn FnMut(&str, Box + Send + Sync>)) {{ +" + )?; + + // create empty defs + for DefInfo { rust_name, def, .. } in def_infos.values() { + let labs = compile_lab_set(&def.labs)?; + writeln!(code, " let mut def_{rust_name} = Box::new(Def::new({labs}, Def_{rust_name}::default()));")?; } writeln!(code)?; // initialize defs that have refs - for (hvmc_name, DefInfo { rust_name, refs, .. }) in &def_infos { + for DefInfo { rust_name, refs, .. } in def_infos.values() { if refs.is_empty() { continue; } let fields = refs .iter() - .map(|r| format!("def_{rust_name}: def_{rust_name}.clone()", rust_name = sanitize_name(r))) + .map(|r| format!("def_{rust_name}: Port::new_ref(&def_{rust_name})", rust_name = sanitize_name(r))) .collect::>() .join(", "); - writeln!( - code, - r##" host.get_mut::>(r#"{hvmc_name}"#).data.0 = Def_{rust_name} {{ {fields} }};"## - )?; + writeln!(code, r##" def_{rust_name}.data = Def_{rust_name} {{ {fields} }};"##)?; + } + writeln!(code)?; + + // insert them + for (hvmc_name, DefInfo { rust_name, .. }) in &def_infos { + writeln!(code, r##" insert(r#"{hvmc_name}"#, def_{rust_name});"##)?; } writeln!(code, "}}")?; @@ -92,27 +107,9 @@ fn _compile_host(host: &Host) -> Result { Ok(code) } -fn refs<'a>(host: &'a Host, instructions: &'a [Instruction]) -> BTreeSet<&'a str> { - let mut refs = BTreeSet::new(); - - for instr in instructions { - if let Instruction::Const { port, .. } | Instruction::LinkConst { port, .. } = instr { - if port.tag() == Tag::Ref && !port.is_era() { - refs.insert(host.back[&port.addr()].as_str()); - } - } - } - - refs -} - -fn compile_struct( - code: &mut String, - host: &Host, - rust_name: &str, - def: &Def>, -) -> fmt::Result { - let refs = refs(host, &def.data.0.instr) +/// Compiles a def into a structure. +fn compile_struct(code: &mut String, host: &Host, rust_name: &str, def: &Def) -> fmt::Result { + let refs = refs(host, def.data.instructions()) .iter() .map(|r| format!("def_{}: Port", sanitize_name(r))) .collect::>() @@ -124,11 +121,12 @@ fn compile_struct( writeln!(code, "}}")?; writeln!(code)?; - writeln!(code, "impl AsHostedDef for Def_{rust_name} {{")?; - writeln!(code, " fn call(slf: &Def, net: &mut Net, port: Port) {{")?; + writeln!(code, "impl AsDef for Def_{rust_name} {{")?; + writeln!(code, " unsafe fn call(slf: *const Def, net: &mut Net, port: Port) {{")?; + writeln!(code, " let slf = unsafe {{ &*slf }};")?; writeln!(code, " let t0 = Trg::port(port);")?; - for instr in &def.data.0.instr { + for instr in def.data.instructions() { write!(code, " ")?; match instr { Instruction::Const { trg, port } => { @@ -188,6 +186,24 @@ fn compile_port(host: &Host, port: &Port) -> String { } } +fn compile_lab_set(labs: &LabSet) -> Result { + if labs == &LabSet::ALL { + return Ok("LabSet::ALL".to_owned()); + } + if labs == &LabSet::NONE { + return Ok("LabSet::NONE".to_owned()); + } + let mut str = "LabSet::from_bits(&[".to_owned(); + for (i, word) in labs.bits().iter().enumerate() { + if i != 0 { + write!(str, ", ")?; + } + write!(str, "0x{:x}", word)?; + } + str.push_str("])"); + Ok(str) +} + /// Adapts `name` to be a valid suffix for a rust identifier, if necessary. fn sanitize_name(name: &str) -> String { if !name.contains('.') && !name.contains('$') { @@ -204,20 +220,17 @@ fn sanitize_name(name: &str) -> String { } } -fn compile_lab_set(labs: &LabSet) -> Result { - if labs == &LabSet::ALL { - return Ok("LabSet::ALL".to_owned()); - } - if labs == &LabSet::NONE { - return Ok("LabSet::NONE".to_owned()); - } - let mut str = "LabSet::from_bits(&[".to_owned(); - for (i, word) in labs.bits.iter().enumerate() { - if i != 0 { - write!(str, ", ")?; +/// Returns the names of all references occurring in `instructions`. +fn refs<'a>(host: &'a Host, instructions: &'a [Instruction]) -> BTreeSet<&'a str> { + let mut refs = BTreeSet::new(); + + for instr in instructions { + if let Instruction::Const { port, .. } | Instruction::LinkConst { port, .. } = instr { + if port.tag() == Tag::Ref && !port.is_era() { + refs.insert(host.back[&port.addr()].as_str()); + } } - write!(str, "0x{:x}", word)?; } - str.push_str("])"); - Ok(str) + + refs } diff --git a/src/compile/include_files.rs b/src/compile/include_files.rs new file mode 100644 index 00000000..5fa9976a --- /dev/null +++ b/src/compile/include_files.rs @@ -0,0 +1,112 @@ +use std::{fs, io, path::Path, sync::Arc}; + +use hvmc_host::Host; + +use parking_lot::Mutex; + +macro_rules! include_files { + ($([$($prefix:ident)*])? crate $name:ident {$($sub:tt)*} $($rest:tt)*) => { + include_files!([$($($prefix)*)?] $name/ { Cargo.toml src/ { $($sub)* } }); + include_files!([$($($prefix)*)?] $($rest)*); + }; + + ($([$($prefix:ident)*])? $mod:ident/ {$($sub:tt)*} $($rest:tt)*) => { + fs::create_dir_all(concat!(".hvm/", $($(stringify!($prefix), "/",)*)? stringify!($mod)))?; + include_files!([$($($prefix)* $mod)?] $($sub)*); + include_files!([$($($prefix)*)?] $($rest)*); + }; + + ($([$($prefix:ident)*])? $mod:ident {$($sub:tt)*} $($rest:tt)*) => { + include_files!([$($($prefix)*)?] $mod/ {$($sub)*} $($rest)*); + include_files!([$($($prefix)*)?] $mod $($rest)*); + }; + + ($([$($prefix:ident)*])? $file:ident.$ext:ident $($rest:tt)*) => { + fs::write( + concat!(".hvm/", $($(stringify!($prefix), "/",)*)* stringify!($file), ".", stringify!($ext)), + include_str!(concat!("../../", $($(stringify!($prefix), "/",)*)* stringify!($file), ".", stringify!($ext))), + )?; + include_files!([$($($prefix)*)?] $($rest)*); + }; + + ($([$($prefix:ident)*])? $file:ident $($rest:tt)*) => { + include_files!([$($($prefix)*)?] $file.rs $($rest)*); + }; + + ($([$($prefix:ident)*])?) => {}; +} + +/// Copies the `hvm-core` source to a temporary `.hvm` directory. +/// Only a subset of `Cargo.toml` is included. +pub fn create_temp_hvm(host: Arc>) -> Result<(), io::Error> { + let lib = super::compile_host(&host.lock()); + let outdir = ".hvm"; + if Path::new(&outdir).exists() { + fs::remove_dir_all(outdir)?; + } + + fs::create_dir_all(".hvm/gen/src/")?; + fs::write( + ".hvm/Cargo.toml", + r#" +[workspace] +resolver = "2" + +members = ["util", "runtime", "gen"] + +[workspace.lints] + "#, + )?; + fs::write( + ".hvm/gen/Cargo.toml", + r#" +[package] +name = "hvmc-gen" +version = "0.0.0" +edition = "2021" + +[lib] +crate-type = ["dylib"] + +[dependencies] +hvmc-runtime = { path = "../runtime", default-features = false } +"#, + )?; + fs::write(".hvm/gen/src/lib.rs", lib)?; + + include_files! { + prelude + crate util { + lib + array_vec + bi_enum + create_var + deref + maybe_grow + ops { + num + word + } + parse_abbrev_number + pretty_num + } + crate runtime { + runtime + addr + allocator + def + dyn_net + instruction + interact + linker + net + node + parallel + port + trace + wire + } + } + + Ok(()) +} diff --git a/src/full.rs b/src/full.rs new file mode 100644 index 00000000..40a117d7 --- /dev/null +++ b/src/full.rs @@ -0,0 +1,85 @@ +use std::path::PathBuf; + +use clap::{Parser, Subcommand}; + +use crate::{args::TransformArgs, RunArgs, RuntimeOpts}; + +#[derive(Parser, Debug)] +#[command( + author, + version, + about = "A massively parallel Interaction Combinator evaluator", + long_about = r##" +A massively parallel Interaction Combinator evaluator + +Examples: +$ hvmc run examples/church_encoding/church.hvm +$ hvmc run examples/addition.hvmc "#16" "#3" +$ hvmc compile examples/addition.hvmc +$ hvmc reduce examples/addition.hvmc -- "a & @mul ~ (#3 (#4 a))" +$ hvmc reduce -- "a & #3 ~ <* #4 a>""## +)] +pub struct FullCli { + #[command(subcommand)] + pub mode: CliMode, +} + +#[derive(Subcommand, Clone, Debug)] +#[command(author, version)] +pub enum CliMode { + /// Compile a hvm-core program into a Rust crate. + Compile { + /// hvm-core file to compile. + file: PathBuf, + /// Output path; defaults to the input file with `.hvmc` stripped. + #[arg(short, long)] + output: Option, + #[command(flatten)] + transform_args: TransformArgs, + }, + /// Run a program, optionally passing a list of arguments to it. + Run { + /// Name of the file to load. + file: PathBuf, + #[command(flatten)] + args: RunArgs, + #[command(flatten)] + run_opts: RuntimeOpts, + #[command(flatten)] + transform_args: TransformArgs, + }, + /// Reduce hvm-core expressions to their normal form. + /// + /// The expressions are passed as command-line arguments. + /// It is also possible to load files before reducing the expression, + /// which makes it possible to reference definitions from the file + /// in the expression. + Reduce { + /// Files to load before reducing the expressions. + /// + /// Multiple files will act as if they're concatenated together. + #[arg(required = false)] + files: Vec, + /// Expressions to reduce. + /// + /// The normal form of each expression will be + /// printed on a new line. This list must be separated from the file list + /// with a double dash ('--'). + #[arg(required = false, last = true)] + exprs: Vec, + #[command(flatten)] + run_opts: RuntimeOpts, + #[command(flatten)] + transform_args: TransformArgs, + }, + /// Transform a hvm-core program using one of the optimization passes. + Transform { + /// Files to load before reducing the expressions. + /// + /// Multiple files will act as if they're concatenated together. + #[arg(required = true)] + files: Vec, + #[command(flatten)] + transform_args: TransformArgs, + }, +} diff --git a/src/fuzz.rs b/src/fuzz.rs deleted file mode 100644 index a945c39b..00000000 --- a/src/fuzz.rs +++ /dev/null @@ -1,501 +0,0 @@ -#![cfg(feature = "std")] -//! An 'atomic fuzzer' to exhaustively test an atomic algorithm for correctness. -//! -//! This fuzzer can test an algorithm with every possible ordering of parallel -//! instructions / update propagation. -//! -//! This module implements a subset of the [`std::sync::atomic`] api. To test an -//! algorithm with the fuzzer, it must first be compiled to use this module's -//! atomics, rather than std's. Then, it can be called from within -//! [`Fuzzer::fuzz`]. -//! -//! For example, here's a test of the ['nomicon][nomicon-example]'s example of -//! atomic ordering: -//! -//! [nomicon-example]: -//! https://doc.rust-lang.org/nomicon/atomics.html#hardware-reordering -//! -//! ``` -//! # use std::collections::HashSet; -//! use hvmc::fuzz::{Fuzzer, AtomicU64, Ordering}; -//! -//! let mut results = HashSet::new(); -//! Fuzzer::default().fuzz(|f| { -//! let x = AtomicU64::new(0); -//! let y = AtomicU64::new(1); -//! f.scope(|s| { // use `f.scope` instead of `std::thread::scope` -//! s.spawn(|| { -//! y.store(3, Ordering::Relaxed); -//! x.store(1, Ordering::Relaxed); -//! }); -//! s.spawn(|| { -//! if x.load(Ordering::Relaxed) == 1 { -//! y.store(y.load(Ordering::Relaxed) * 2, Ordering::Relaxed); -//! } -//! }); -//! }); -//! results.insert(y.read()); -//! }); -//! -//! assert_eq!(results, [6, 3, 2].into_iter().collect()); -//! ``` -//! -//! Note that the atomic types exposed by this module will panic if used outside -//! of a thread spawned by [`Fuzzer::fuzz`]. -//! -//! Internally, the fuzzer works by iterating through *decision paths*, -//! sequences of integers indicating which branch is taken at each -//! non-deterministic instruction. This might, for example, correspond to which -//! thread is switched to at a given yield point. -//! -//! The shape of the decision tree is not known ahead of time, and is instead -//! progressively discovered through execution. Thus, when a path is first -//! executed, it may not be the full path -- there may be branches that are not -//! specified. In the extreme case, at the very beginning, the path starts as -//! `[]`, with no branches specified. -//! -//! When a branching point is reached that is not specified in the path, the -//! *last* branch is automatically chosen, and this decision is appended to the -//! path. For example, executing path `[]` might disambiguate it to `[1, 2, 1]`. -//! The last path is chosen because this alleviates the need to store the number -//! of branches at any given point -- instead, branches are selected in -//! decreasing order, so when the index reaches `0`, we know that there are no -//! more branches to explore. -//! -//! By default, fuzzing starts at path `[]`, which will test the full decision -//! tree. Alternatively, one can specify a different path to start at with -//! [`Fuzzer::with_path`] -- this is useful, for example, to debug a failing -//! path late in the tree. (It's important to note, though, that the semantics -//! of the path are very dependent on the specifics of the algorithm, so if the -//! algorithm is changed (particularly, the atomic instructions it executes), -//! old paths may no longer be valid.) - -use crate::prelude::*; - -use alloc::sync::Arc; -use core::{ - any::Any, - cell::{OnceCell, RefCell}, - fmt::Debug, - marker::PhantomData, - ops::Add, - sync::atomic, -}; -use std::{ - sync::{Condvar, Mutex}, - thread::{self, Scope, ThreadId}, - thread_local, -}; - -use nohash_hasher::IntMap; - -#[repr(transparent)] -pub struct Atomic { - value: T::Atomic, -} - -impl Default for Atomic { - fn default() -> Self { - Atomic::new(T::default()) - } -} - -impl Atomic { - pub fn new(value: T) -> Self { - Atomic { value: T::new_atomic(value) } - } - /// Reads the final value of this atomic -- should only be called in a - /// non-parallel context; i.e., at the end of a test. - pub fn read(&self) -> T { - T::load(&self.value) - } - fn with(&self, f: impl FnOnce(&Fuzzer, &mut Vec, &mut usize) -> (bool, R)) -> R { - ThreadContext::with(|ctx| { - if !ctx.just_started { - ctx.fuzzer.yield_point(); - } - ctx.just_started = false; - let key = &self.value as *const _ as usize; - let view = ctx.views.entry(key).or_insert_with(|| AtomicView { - history: ctx - .fuzzer - .atomics - .lock() - .unwrap() - .entry(key) - .or_insert_with(|| Arc::new(Mutex::new(vec![T::load(&self.value)]))) - .clone(), - index: 0, - }); - let history: &AtomicHistory = view.history.to_any().downcast_ref().unwrap(); - let mut history = history.lock().unwrap(); - let (changed, r) = f(&ctx.fuzzer, &mut history, &mut view.index); - if changed { - ctx.fuzzer.unblock_threads(); - } - r - }) - } - pub fn load(&self, _: Ordering) -> T { - self.with(|fuzzer, history, index| { - let delta = fuzzer.decide(history.len() - *index); - *index += delta; - let value = history[*index]; - (false, value) - }) - } - pub fn store(&self, value: T, _: Ordering) { - self.with(|_, history, index| { - *index = history.len(); - history.push(value); - T::store(&self.value, value); - (true, ()) - }) - } - pub fn swap(&self, new: T, _: Ordering) -> T { - self.with(|_, history, index| { - *index = history.len(); - let old = *history.last().unwrap(); - history.push(new); - T::store(&self.value, new); - (true, old) - }) - } - pub fn compare_exchange(&self, expected: T, new: T, _: Ordering, _: Ordering) -> Result { - self.with(|_, history, index| { - let old = *history.last().unwrap(); - if old == expected { - *index = history.len(); - history.push(new); - T::store(&self.value, new); - (true, Ok(old)) - } else { - *index = history.len() - 1; - (false, Err(old)) - } - }) - } - pub fn compare_exchange_weak(&self, expected: T, new: T, _: Ordering, _: Ordering) -> Result { - self.with(|fuzzer, history, index| { - let old = *history.last().unwrap(); - if old == expected && fuzzer.decide(2) == 1 { - *index = history.len(); - history.push(new); - T::store(&self.value, new); - (true, Ok(old)) - } else { - *index = history.len() - 1; - (false, Err(old)) - } - }) - } - pub fn fetch_add(&self, delta: T, _: Ordering) -> T { - self.with(|_, history, index| { - *index = history.len(); - let old = *history.last().unwrap(); - let new = old + delta; - history.push(new); - T::store(&self.value, new); - (true, old) - }) - } -} - -pub fn spin_loop() { - ThreadContext::with(|ctx| { - let mut unsynced = ctx - .views - .values_mut() - .map(|x| { - let l = x.history.len(); - (x, l) - }) - .filter(|x| x.0.index + 1 < x.1) - .collect::>(); - if !unsynced.is_empty() { - ctx.fuzzer.yield_point(); - let idx = ctx.fuzzer.decide(unsynced.len()); - let unsynced = &mut unsynced[idx]; - let amount = 1 + ctx.fuzzer.decide(unsynced.1 - unsynced.0.index - 1); - unsynced.0.index += amount; - } else { - ctx.fuzzer.block_thread(); - ctx.fuzzer.yield_point(); - ctx.just_started = true; - } - }) -} - -/// One thread's view of an atomic variable -- a reference to the history, and -/// an index into it that denotes the currently propagated value. -struct AtomicView { - history: Arc, - index: usize, -} - -/// The history of an atomic variable (a vector of the vales it has held). -type AtomicHistory = Mutex>; - -/// Currently, only `Ordering::Relaxed` is supported; other orderings could -/// theoretically be supported, but this has not been implemented, as HVM -/// currently only uses `Relaxed` operations. -pub enum Ordering { - Relaxed, -} - -struct ThreadContext { - fuzzer: Arc, - views: IntMap>, - just_started: bool, -} - -impl ThreadContext { - fn init(fuzzer: Arc) { - CONTEXT.with(|ctx| { - assert!(ctx.get().is_none(), "thread context already initialized"); - ctx.get_or_init(|| RefCell::new(ThreadContext { fuzzer, views: Default::default(), just_started: true })); - }); - } - fn with(f: impl FnOnce(&mut ThreadContext) -> T) -> T { - CONTEXT.with(|ctx| f(&mut ctx.get().expect("cannot use fuzz atomics outside of Fuzzer::fuzz").borrow_mut())) - } -} - -impl Clone for AtomicView { - fn clone(&self) -> Self { - AtomicView { history: self.history.clone(), index: self.index } - } -} - -trait AnyAtomicHistory: Any + Send + Sync { - fn len(&self) -> usize; - fn to_any(&self) -> &dyn Any; -} - -impl AnyAtomicHistory for AtomicHistory { - fn len(&self) -> usize { - self.lock().unwrap().len() - } - fn to_any(&self) -> &dyn Any { - self - } -} - -thread_local! { - static CONTEXT: OnceCell> = const { OnceCell::new() }; -} - -#[derive(Default)] -struct DecisionPath { - path: Vec, - index: usize, -} - -impl DecisionPath { - fn decide(&mut self, options: usize) -> usize { - if options == 1 { - return 0; - } - if options == 0 { - panic!("you left me no choice"); - } - if self.index == self.path.len() { - self.path.push(options - 1); - } - let choice = self.path[self.index]; - self.index += 1; - choice - } - fn next_path(&mut self) -> bool { - self.index = 0; - while self.path.last() == Some(&0) || self.path.len() > 100 { - self.path.pop(); - } - if let Some(branch) = self.path.last_mut() { - *branch -= 1; - true - } else { - false - } - } -} - -#[derive(Default)] -pub struct Fuzzer { - path: Mutex, - atomics: Mutex>>, - current_thread: Mutex>, - active_threads: Mutex>, - blocked_threads: Mutex>, - condvar: Condvar, - main: Option, -} - -impl Fuzzer { - pub fn with_path(path: Vec) -> Self { - Fuzzer { path: Mutex::new(DecisionPath { path, index: 0 }), ..Default::default() } - } - - pub fn fuzz(mut self, mut f: impl FnMut(&Arc) + Send) { - thread::scope(move |s| { - s.spawn(move || { - self.main = Some(thread::current().id()); - let fuzzer = Arc::new(self); - ThreadContext::init(fuzzer.clone()); - let mut i = 0; - loop { - println!("{:6} {:?}", i, &fuzzer.path.lock().unwrap().path); - fuzzer.atomics.lock().unwrap().clear(); - ThreadContext::with(|ctx| ctx.views.clear()); - f(&fuzzer); - i += 1; - if !fuzzer.path.lock().unwrap().next_path() { - break; - } - } - println!("checked all {} paths", i); - }); - }); - } - - /// Makes a non-deterministic decision, returning an integer within - /// `0..options`. - pub fn decide(&self, options: usize) -> usize { - self.path.lock().unwrap().decide(options) - } - - pub fn maybe_swap(&self, a: T, b: T) -> (T, T) { - if self.decide(2) == 1 { (b, a) } else { (a, b) } - } - - /// Yields to the "scheduler", potentially switching to another thread. - pub fn yield_point(&self) { - if self.switch_thread() { - self.pause_thread(); - } - } - - fn unblock_threads(&self) { - let mut active_threads = self.active_threads.lock().unwrap(); - let mut blocked_threads = self.blocked_threads.lock().unwrap(); - active_threads.extend(blocked_threads.drain(..)); - } - - fn block_thread(&self) { - let thread_id = thread::current().id(); - let mut active_threads = self.active_threads.lock().unwrap(); - let mut blocked_threads = self.blocked_threads.lock().unwrap(); - let idx = active_threads.iter().position(|x| x == &thread_id).unwrap(); - active_threads.swap_remove(idx); - blocked_threads.push(thread_id); - } - - fn switch_thread(&self) -> bool { - let thread_id = thread::current().id(); - let active_threads = self.active_threads.lock().unwrap(); - if active_threads.is_empty() { - if self.main != Some(thread_id) { - panic!("deadlock"); - } - return false; - } - let new_idx = self.decide(active_threads.len()); - let new_thread = active_threads[new_idx]; - if new_thread == thread_id { - return false; - } - let mut current_thread = self.current_thread.lock().unwrap(); - *current_thread = Some(new_thread); - self.condvar.notify_all(); - true - } - - fn pause_thread(&self) { - let thread_id = thread::current().id(); - let mut current_thread = self.current_thread.lock().unwrap(); - while *current_thread != Some(thread_id) { - current_thread = self.condvar.wait(current_thread).unwrap(); - } - } - - pub fn scope<'e>(self: &Arc, f: impl for<'s, 'p> FnOnce(&FuzzScope<'s, 'p, 'e>)) { - thread::scope(|scope| { - let scope = FuzzScope { fuzzer: self.clone(), scope, __: PhantomData }; - f(&scope); - assert_eq!(*self.current_thread.lock().unwrap(), None); - self.switch_thread(); - }); - } -} - -pub struct FuzzScope<'s, 'p: 's, 'e: 'p> { - fuzzer: Arc, - scope: &'s Scope<'s, 'p>, - __: PhantomData<&'e mut &'e ()>, -} - -impl<'s, 'p: 's, 'e: 's> FuzzScope<'s, 'p, 'e> { - pub fn spawn(&self, f: F) { - let fuzzer = self.fuzzer.clone(); - let ready = Arc::new(atomic::AtomicBool::new(false)); - let views = ThreadContext::with(|ctx| ctx.views.clone()); - self.scope.spawn({ - let ready = ready.clone(); - move || { - ThreadContext::init(fuzzer.clone()); - ThreadContext::with(|ctx| ctx.views = views); - let thread_id = thread::current().id(); - fuzzer.active_threads.lock().unwrap().push(thread_id); - ready.store(true, atomic::Ordering::Relaxed); - fuzzer.pause_thread(); - f(); - let mut active_threads = fuzzer.active_threads.lock().unwrap(); - let i = active_threads.iter().position(|&t| t == thread_id).unwrap(); - active_threads.swap_remove(i); - if !active_threads.is_empty() { - drop(active_threads); - fuzzer.switch_thread(); - } else { - *fuzzer.current_thread.lock().unwrap() = None; - } - } - }); - while !ready.load(atomic::Ordering::Relaxed) { - hint::spin_loop() - } - } -} - -pub trait HasAtomic: 'static + Copy + Eq + Send + Add + Debug { - type Atomic; - fn new_atomic(value: Self) -> Self::Atomic; - fn load(atomic: &Self::Atomic) -> Self; - fn store(atomic: &Self::Atomic, value: Self); -} - -macro_rules! decl_atomic { - ($($T:ident: $A:ident),* $(,)?) => {$( - pub type $A = Atomic<$T>; - - impl HasAtomic for $T { - type Atomic = atomic::$A; - fn new_atomic(value: Self) -> Self::Atomic { - atomic::$A::new(value) - } - fn load(atomic: &Self::Atomic) -> Self { - atomic.load(atomic::Ordering::SeqCst) - } - fn store(atomic: &Self::Atomic, value: Self) { - atomic.store(value, atomic::Ordering::SeqCst) - } - } - )*}; -} - -decl_atomic! { - u8: AtomicU8, - u16: AtomicU16, - u32: AtomicU32, - u64: AtomicU64, - usize: AtomicUsize, -} diff --git a/src/gen.rs b/src/gen.rs deleted file mode 100644 index e831b223..00000000 --- a/src/gen.rs +++ /dev/null @@ -1,5 +0,0 @@ -//! Replaced by `compile.rs` with compiled defs. - -use crate::host::Host; - -pub fn insert_into_host(_: &mut Host) {} diff --git a/src/lib.rs b/src/lib.rs deleted file mode 100644 index f1213e9e..00000000 --- a/src/lib.rs +++ /dev/null @@ -1,37 +0,0 @@ -#![feature(const_type_id, extern_types, inline_const, generic_const_exprs, new_uninit)] -#![cfg_attr(feature = "trace", feature(const_type_name))] -#![cfg_attr(not(feature = "std"), no_std)] -#![allow( - non_snake_case, - incomplete_features, - clippy::field_reassign_with_default, - clippy::missing_safety_doc, - clippy::new_ret_no_self -)] -#![warn( - clippy::alloc_instead_of_core, - clippy::std_instead_of_core, - clippy::std_instead_of_alloc, - clippy::absolute_paths -)] - -extern crate alloc; - -mod prelude; - -pub mod ast; -pub mod compile; -pub mod host; -pub mod ops; -pub mod run; -pub mod stdlib; -pub mod transform; -pub mod util; - -#[doc(hidden)] // not public api -pub mod fuzz; -#[doc(hidden)] // not public api -pub mod trace; - -#[doc(hidden)] // shim for compilation -pub mod gen; diff --git a/src/main.rs b/src/main.rs index b2bceb35..d449812e 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,243 +1,90 @@ -#![cfg_attr(feature = "trace", feature(const_type_name))] +include!("../prelude.rs"); -use clap::{Args, Parser, Subcommand}; -use hvmc::{ - ast::{Book, Net, Tree}, - host::Host, - run::{DynNet, Mode, Trg}, - stdlib::{create_host, insert_stdlib}, - transform::{TransformOpts, TransformPass, TransformPasses}, - *, -}; +mod compile; + +mod args; +mod full; -use parking_lot::Mutex; use std::{ env::consts::{DLL_PREFIX, DLL_SUFFIX}, ffi::OsStr, - fmt::Write, - fs::{self, File}, - io::{self, BufRead}, - path::{Path, PathBuf}, + fs, io, + ops::DerefMut, + path::PathBuf, process::{self, Stdio}, - str::FromStr, sync::Arc, time::{Duration, Instant}, }; +use parking_lot::Mutex; + +use self::full::{CliMode, FullCli}; + +use args::{RunArgs, RuntimeOpts, TransformArgs, TransformPass}; +use clap::Parser; + +use hvmc_ast::{Book, Net, Tree}; +use hvmc_host::{ + stdlib::{create_host, insert_stdlib}, + DefRef, Host, +}; +use hvmc_runtime::{dispatch_dyn_net, Def, DynNet, Heap, Mode, Port, Trg}; +use hvmc_transform::Transform; + fn main() { if cfg!(feature = "trace") { - trace::set_hook(); + hvmc_runtime::trace::set_hook(); } - if cfg!(feature = "_full_cli") { - let cli = FullCli::parse(); - - match cli.mode { - CliMode::Compile { file, dylib, transform_args, output } => { - let output = if let Some(output) = output { - output - } else if let Some("hvmc") = file.extension().and_then(OsStr::to_str) { - file.with_extension("") - } else { - eprintln!("file missing `.hvmc` extension; explicitly specify an output path with `--output`."); - - process::exit(1); - }; - - let host = create_host(&load_book(&[file], &transform_args)); - create_temp_hvm(host).unwrap(); - - if dylib { - prepare_temp_hvm_dylib().unwrap(); - compile_temp_hvm(&["--lib"]).unwrap(); - - fs::copy(format!(".hvm/target/release/{DLL_PREFIX}hvmc{DLL_SUFFIX}"), output).unwrap(); - } else { - compile_temp_hvm(&[]).unwrap(); - - fs::copy(".hvm/target/release/hvmc", output).unwrap(); - } - } - CliMode::Run { run_opts, mut transform_args, file, args } => { - // Don't pre-reduce or prune the entry point - transform_args.transform_opts.pre_reduce_skip.push(args.entry_point.clone()); - transform_args.transform_opts.prune_entrypoints.push(args.entry_point.clone()); - let host: Arc> = Default::default(); - load_dylibs(host.clone(), &run_opts.include); - insert_stdlib(host.clone()); - host.lock().insert_book(&load_book(&[file], &transform_args)); + let cli = FullCli::parse(); - run(host, run_opts, args); - } - CliMode::Reduce { run_opts, transform_args, files, exprs } => { - let host = load_host(&files, &transform_args, &run_opts.include); - let exprs: Vec<_> = exprs.iter().map(|x| Net::from_str(x).unwrap()).collect(); - reduce_exprs(host, &exprs, &run_opts); - } - CliMode::Transform { transform_args, files } => { - let book = load_book(&files, &transform_args); - println!("{}", book); - } - } - } else { - let cli = BareCli::parse(); - let host = create_host(&Book::default()); - gen::insert_into_host(&mut host.lock()); - run(host, cli.opts, cli.args); - } - if cfg!(feature = "trace") { - hvmc::trace::_read_traces(usize::MAX); - } -} + match cli.mode { + CliMode::Compile { file, transform_args, output } => { + let output = if let Some(output) = output { + output + } else if let Some("hvmc") = file.extension().and_then(OsStr::to_str) { + file.with_extension("") + } else { + eprintln!("file missing `.hvmc` extension; explicitly specify an output path with `--output`."); -#[derive(Parser, Debug)] -#[command( - author, - version, - about = "A massively parallel Interaction Combinator evaluator", - long_about = r##" -A massively parallel Interaction Combinator evaluator - -Examples: -$ hvmc run examples/church_encoding/church.hvm -$ hvmc run examples/addition.hvmc "#16" "#3" -$ hvmc compile examples/addition.hvmc -$ hvmc reduce examples/addition.hvmc -- "a & @mul ~ (#3 (#4 a))" -$ hvmc reduce -- "a & #3 ~ <* #4 a>""## -)] -struct FullCli { - #[command(subcommand)] - pub mode: CliMode, -} + process::exit(1); + }; -#[derive(Parser, Debug)] -#[command(author, version)] -struct BareCli { - #[command(flatten)] - pub opts: RuntimeOpts, - #[command(flatten)] - pub args: RunArgs, -} + let host = create_host(&load_book(&[file], transform_args)); + compile::create_temp_hvm(host).unwrap(); -#[derive(Subcommand, Clone, Debug)] -#[command(author, version)] -enum CliMode { - /// Compile a hvm-core program into a Rust crate. - Compile { - /// hvm-core file to compile. - file: PathBuf, - /// Compile this hvm-core file to a dynamic library. - /// - /// These can be included when running with the `--include` option. - #[arg(short, long)] - dylib: bool, - /// Output path; defaults to the input file with `.hvmc` stripped. - #[arg(short, long)] - output: Option, - #[command(flatten)] - transform_args: TransformArgs, - }, - /// Run a program, optionally passing a list of arguments to it. - Run { - /// Name of the file to load. - file: PathBuf, - #[command(flatten)] - args: RunArgs, - #[command(flatten)] - run_opts: RuntimeOpts, - #[command(flatten)] - transform_args: TransformArgs, - }, - /// Reduce hvm-core expressions to their normal form. - /// - /// The expressions are passed as command-line arguments. - /// It is also possible to load files before reducing the expression, - /// which makes it possible to reference definitions from the file - /// in the expression. - Reduce { - /// Files to load before reducing the expressions. - /// - /// Multiple files will act as if they're concatenated together. - #[arg(required = false)] - files: Vec, - /// Expressions to reduce. - /// - /// The normal form of each expression will be - /// printed on a new line. This list must be separated from the file list - /// with a double dash ('--'). - #[arg(required = false, last = true)] - exprs: Vec, - #[command(flatten)] - run_opts: RuntimeOpts, - #[command(flatten)] - transform_args: TransformArgs, - }, - /// Transform a hvm-core program using one of the optimization passes. - Transform { - /// Files to load before reducing the expressions. - /// - /// Multiple files will act as if they're concatenated together. - #[arg(required = true)] - files: Vec, - #[command(flatten)] - transform_args: TransformArgs, - }, -} + compile_temp_hvm().unwrap(); -#[derive(Args, Clone, Debug)] -struct TransformArgs { - /// Enables or disables transformation passes. - #[arg(short = 'O', value_delimiter = ' ', action = clap::ArgAction::Append)] - transform_passes: Vec, - #[command(flatten)] - transform_opts: TransformOpts, -} + fs::copy(format!(".hvm/target/release/{DLL_PREFIX}hvmc_gen{DLL_SUFFIX}"), output).unwrap(); + } + CliMode::Run { run_opts, mut transform_args, file, args } => { + // Don't pre-reduce or prune the entry point + transform_args.transform_opts.pre_reduce_skip.push(args.entry_point.clone()); + transform_args.transform_opts.prune_entrypoints.push(args.entry_point.clone()); -#[derive(Args, Clone, Debug)] -struct RuntimeOpts { - /// Show performance statistics. - #[arg(short, long = "stats")] - show_stats: bool, - /// Single-core mode (no parallelism). - #[arg(short = '1', long = "single")] - single_core: bool, - /// Lazy mode. - /// - /// Lazy mode only expands references that are reachable - /// by a walk from the root of the net. This leads to a dramatic slowdown, - /// but allows running programs that would expand indefinitely otherwise. - #[arg(short, long = "lazy")] - lazy_mode: bool, - /// How much memory to allocate on startup. - /// - /// Supports abbreviations such as '4G' or '400M'. - #[arg(short, long, value_parser = util::parse_abbrev_number::)] - memory: Option, - /// Dynamic library hvm-core files to include. - /// - /// hvm-core files can be compiled as dylibs with the `--dylib` option. - #[arg(short, long, value_delimiter = ' ', action = clap::ArgAction::Append)] - include: Vec, -} + let host = load_host(&[file], transform_args, &run_opts.include); + run(host, run_opts, args); + } + CliMode::Reduce { run_opts, transform_args, files, exprs } => { + let host = load_host(&files, transform_args, &run_opts.include); + let exprs: Vec<_> = exprs.iter().map(|x| x.parse().unwrap()).collect(); + reduce_exprs(host, &exprs, &run_opts); + } + CliMode::Transform { transform_args, files } => { + let book = load_book(&files, transform_args); + println!("{}", book); + } + }; -#[derive(Args, Clone, Debug)] -struct RunArgs { - /// Name of the definition that will get reduced. - #[arg(short, default_value = "main")] - entry_point: String, - /// List of arguments to pass to the program. - /// - /// Arguments are passed using the lambda-calculus interpretation - /// of interaction combinators. So, for example, if the arguments are - /// "#1" "#2" "#3", then the expression that will get reduced is - /// `r & @main ~ (#1 (#2 (#3 r)))`. - args: Vec, + if cfg!(feature = "trace") { + hvmc_runtime::trace::_read_traces(usize::MAX); + } } fn run(host: Arc>, opts: RuntimeOpts, args: RunArgs) { let mut net = Net { root: Tree::Ref { nam: args.entry_point }, redexes: vec![] }; for arg in args.args { - let arg: Net = Net::from_str(&arg).unwrap(); + let arg: Net = arg.parse().unwrap(); net.redexes.extend(arg.redexes); net.apply_tree(arg.root); } @@ -247,7 +94,7 @@ fn run(host: Arc>, opts: RuntimeOpts, args: RunArgs) { fn load_host( files: &[PathBuf], - transform_args: &TransformArgs, + transform_args: TransformArgs, include: &[PathBuf], ) -> Arc> { let host: Arc> = Default::default(); @@ -257,7 +104,7 @@ fn load_host( host } -fn load_book(files: &[PathBuf], transform_args: &TransformArgs) -> Book { +fn load_book(files: &[PathBuf], transform_args: TransformArgs) -> Book { let mut book = files .iter() .map(|name| { @@ -265,18 +112,25 @@ fn load_book(files: &[PathBuf], transform_args: &TransformArgs) -> Book { eprintln!("Input file {:?} not found", name); process::exit(1); }); - contents.parse::().unwrap_or_else(|e| { + contents.parse().unwrap_or_else(|e| { eprintln!("Parsing error {e}"); process::exit(1); }) }) - .fold(Book::default(), |mut acc, i| { + .fold(Book::default(), |mut acc, i: Book| { acc.nets.extend(i.nets); acc }); - let transform_passes = TransformPasses::from(&transform_args.transform_passes[..]); - book.transform(transform_passes, &transform_args.transform_opts).unwrap(); + let transform_passes = TransformPass::to_passes(&transform_args.transform_passes[..]); + book + .transform(transform_passes, &hvmc_transform::TransformOpts { + pre_reduce_skip: transform_args.transform_opts.pre_reduce_skip, + pre_reduce_memory: transform_args.transform_opts.pre_reduce_memory, + pre_reduce_rewrites: transform_args.transform_opts.pre_reduce_rewrites, + prune_entrypoints: transform_args.transform_opts.prune_entrypoints, + }) + .unwrap(); book } @@ -313,21 +167,26 @@ fn load_dylibs(host: Arc>, include: &[PathBuf]) { ); } - let insert_into_host = - lib.get::(b"hvmc_dylib_v0__insert_host").expect("failed to load insert_host"); - insert_into_host(&mut host.lock()); + let insert_into = lib + .get:: + Send + Sync>))>(b"hvmc_dylib_v0__insert_into") + .expect("failed to load insert_into"); + let mut host = host.lock(); + insert_into(&mut |name, def| { + host.insert_def(name, DefRef::Owned(def)); + }); + // Leak the lib to avoid unloading it, as code from it is still referenced. std::mem::forget(lib); } } } fn reduce_exprs(host: Arc>, exprs: &[Net], opts: &RuntimeOpts) { - let heap = run::Heap::new(opts.memory).expect("memory allocation failed"); + let heap = Heap::new(opts.memory).expect("memory allocation failed"); for expr in exprs { let mut net = DynNet::new(&heap, opts.lazy_mode); dispatch_dyn_net!(&mut net => { - host.lock().encode_net(net, Trg::port(run::Port::new_var(net.root.addr())), expr); + host.lock().encode_net(net, Trg::port(Port::new_var(net.root.addr())), expr); let start_time = Instant::now(); if opts.single_core { net.normal(); @@ -343,7 +202,7 @@ fn reduce_exprs(host: Arc>, exprs: &[Net], opts: &RuntimeOpts) { } } -fn print_stats(net: &run::Net, elapsed: Duration) { +fn print_stats(net: &hvmc_runtime::Net, elapsed: Duration) { eprintln!("RWTS : {:>15}", pretty_num(net.rwts.total())); eprintln!("- ANNI : {:>15}", pretty_num(net.rwts.anni)); eprintln!("- COMM : {:>15}", pretty_num(net.rwts.comm)); @@ -365,151 +224,12 @@ fn pretty_num(n: u64) -> String { .collect() } -/// Copies the `hvm-core` source to a temporary `.hvm` directory. -/// Only a subset of `Cargo.toml` is included. -fn create_temp_hvm(host: Arc>) -> Result<(), io::Error> { - let gen = compile::compile_host(&host.lock()); - let outdir = ".hvm"; - if Path::new(&outdir).exists() { - fs::remove_dir_all(outdir)?; - } - let cargo_toml = include_str!("../Cargo.toml"); - let mut cargo_toml = cargo_toml.split_once("##--COMPILER-CUTOFF--##").unwrap().0.to_owned(); - cargo_toml.push_str("[features]\ndefault = ['cli']\ncli = ['std', 'dep:clap']\nstd = []"); - - macro_rules! include_files { - ($([$($prefix:ident)*])? $mod:ident {$($sub:tt)*} $($rest:tt)*) => { - fs::create_dir_all(concat!(".hvm/src/", $($(stringify!($prefix), "/",)*)? stringify!($mod)))?; - include_files!([$($($prefix)* $mod)?] $($sub)*); - include_files!([$($($prefix)*)?] $mod $($rest)*); - }; - ($([$($prefix:ident)*])? $file:ident $($rest:tt)*) => { - fs::write( - concat!(".hvm/src/", $($(stringify!($prefix), "/",)*)* stringify!($file), ".rs"), - include_str!(concat!($($(stringify!($prefix), "/",)*)* stringify!($file), ".rs")), - )?; - include_files!([$($($prefix)*)?] $($rest)*); - }; - ($([$($prefix:ident)*])?) => {}; - } - - fs::create_dir_all(".hvm/src")?; - fs::write(".hvm/Cargo.toml", cargo_toml)?; - fs::write(".hvm/src/gen.rs", gen)?; - - include_files! { - ast - compile - fuzz - host { - calc_labels - encode - readback - } - lib - main - ops { - num - word - } - prelude - run { - addr - allocator - def - dyn_net - instruction - interact - linker - net - node - parallel - port - wire - } - stdlib - trace - transform { - coalesce_ctrs - encode_adts - eta_reduce - inline - pre_reduce - prune - } - util { - apply_tree - array_vec - bi_enum - create_var - deref - maybe_grow - parse_abbrev_number - stats - } - } - - Ok(()) -} - -/// Appends a function to `lib.rs` that will be dynamically loaded -/// by hvm-core when the generated dylib is included. -fn prepare_temp_hvm_dylib() -> Result<(), io::Error> { - insert_crate_type_cargo_toml()?; - - let mut lib = fs::read_to_string(".hvm/src/lib.rs")?; - - writeln!(lib).unwrap(); - writeln!( - lib, - r#" -#[no_mangle] -pub fn hvmc_dylib_v0__insert_host(host: &mut host::Host) {{ - gen::insert_into_host(host) -}} - -#[no_mangle] -pub fn hvmc_dylib_v0__hvmc_version() -> &'static str {{ - {hvmc_version:?} -}} - -#[no_mangle] -pub fn hvmc_dylib_v0__rust_version() -> &'static str {{ - {rust_version:?} -}} - "#, - hvmc_version = env!("CARGO_PKG_VERSION"), - rust_version = env!("RUSTC_VERSION"), - ) - .unwrap(); - - fs::write(".hvm/src/lib.rs", lib) -} - -/// Adds `crate_type = ["dylib"]` under the `[lib]` section of `Cargo.toml`. -fn insert_crate_type_cargo_toml() -> Result<(), io::Error> { - let mut cargo_toml = String::new(); - - let file = File::open(".hvm/Cargo.toml")?; - for line in io::BufReader::new(file).lines() { - let line = line?; - writeln!(cargo_toml, "{line}").unwrap(); - - if line == "[lib]" { - writeln!(cargo_toml, r#"crate_type = ["dylib"]"#).unwrap(); - } - } - - fs::write(".hvm/Cargo.toml", cargo_toml) -} - /// Compiles the `.hvm` directory, appending the provided `args` to `cargo`. -fn compile_temp_hvm(args: &[&'static str]) -> Result<(), io::Error> { +fn compile_temp_hvm() -> Result<(), io::Error> { let output = process::Command::new("cargo") - .current_dir(".hvm") + .current_dir(".hvm/gen/") .arg("build") .arg("--release") - .args(args) .stderr(Stdio::inherit()) .output()?; diff --git a/src/prelude.rs b/src/prelude.rs deleted file mode 100644 index e6f23d19..00000000 --- a/src/prelude.rs +++ /dev/null @@ -1,20 +0,0 @@ -pub use alloc::{ - borrow::ToOwned, - boxed::Box, - format, - string::{String, ToString}, - vec, - vec::Vec, -}; -pub use core::{fmt, hint, iter, mem, ptr}; - -#[cfg(feature = "std")] -pub use std::collections::{hash_map::Entry, HashMap as Map, HashSet as Set}; - -#[cfg(not(feature = "std"))] -pub use alloc::collections::{btree_map::Entry, BTreeMap as Map, BTreeSet as Set}; - -#[cfg(feature = "std")] -pub use std::error::Error; -#[cfg(feature = "std")] -pub use thiserror::Error; diff --git a/src/transform.rs b/src/transform.rs deleted file mode 100644 index 3cfe021c..00000000 --- a/src/transform.rs +++ /dev/null @@ -1,181 +0,0 @@ -use crate::prelude::*; - -use crate::ast::Book; - -pub mod coalesce_ctrs; -pub mod encode_adts; -pub mod eta_reduce; -pub mod inline; -pub mod pre_reduce; -pub mod prune; - -#[derive(Debug, Clone, PartialEq, Eq)] -#[non_exhaustive] -#[cfg_attr(feature = "std", derive(Error))] -pub enum TransformError { - #[cfg_attr(feature = "std", error("infinite reference cycle in `@{0}`"))] - InfiniteRefCycle(String), -} - -impl Book { - pub fn transform(&mut self, passes: TransformPasses, opts: &TransformOpts) -> Result<(), TransformError> { - if passes.prune { - self.prune(&opts.prune_entrypoints); - } - if passes.pre_reduce { - if passes.eta_reduce { - for def in self.nets.values_mut() { - def.eta_reduce(); - } - } - self.pre_reduce( - &|x| opts.pre_reduce_skip.iter().any(|y| x == y), - opts.pre_reduce_memory, - opts.pre_reduce_rewrites, - ); - } - for def in &mut self.nets.values_mut() { - if passes.eta_reduce { - def.eta_reduce(); - } - for tree in def.trees_mut() { - if passes.coalesce_ctrs { - tree.coalesce_constructors(); - } - if passes.encode_adts { - tree.encode_scott_adts(); - } - } - } - if passes.inline { - loop { - let inline_changed = self.inline()?; - if inline_changed.is_empty() { - break; - } - if !(passes.eta_reduce || passes.encode_adts) { - break; - } - for name in inline_changed { - let def = self.get_mut(&name).unwrap(); - if passes.eta_reduce { - def.eta_reduce(); - } - if passes.encode_adts { - for tree in def.trees_mut() { - tree.encode_scott_adts(); - } - } - } - } - } - if passes.prune { - self.prune(&opts.prune_entrypoints); - } - Ok(()) - } -} - -#[derive(Clone, Debug)] -#[cfg_attr(feature = "cli", derive(clap::Args))] -#[non_exhaustive] -pub struct TransformOpts { - /// Names of the definitions that should not get pre-reduced. - /// - /// For programs that don't take arguments and don't have side effects this is - /// usually the entry point of the program (otherwise, the whole program will - /// get reduced to normal form). - #[cfg_attr(feature = "cli", arg(long = "pre-reduce-skip", value_delimiter = ' ', action = clap::ArgAction::Append))] - pub pre_reduce_skip: Vec, - - /// How much memory to allocate when pre-reducing. - /// - /// Supports abbreviations such as '4G' or '400M'. - #[cfg_attr(feature = "cli", arg(long = "pre-reduce-memory", value_parser = crate::util::parse_abbrev_number::))] - pub pre_reduce_memory: Option, - - /// Maximum amount of rewrites to do when pre-reducing. - /// - /// Supports abbreviations such as '4G' or '400M'. - #[cfg_attr(feature = "cli", arg(long = "pre-reduce-rewrites", default_value = "100M", value_parser = crate::util::parse_abbrev_number::))] - pub pre_reduce_rewrites: u64, - - /// Names of the definitions that should not get pruned. - #[cfg_attr(feature = "cli", arg(long = "prune-entrypoints", default_value = "main"))] - pub prune_entrypoints: Vec, -} - -impl TransformOpts { - pub fn add_entrypoint(&mut self, entrypoint: &str) { - self.pre_reduce_skip.push(entrypoint.to_owned()); - self.prune_entrypoints.push(entrypoint.to_owned()); - } -} - -macro_rules! transform_passes { - ($($pass:ident: $name:literal $(| $alias:literal)*),* $(,)?) => { - #[derive(Debug, Default, Clone, Copy)] - #[non_exhaustive] - pub struct TransformPasses { - $(pub $pass: bool),* - } - - impl TransformPasses { - pub const NONE: Self = Self { $($pass: false),* }; - pub const ALL: Self = Self { $($pass: true),* }; - } - - #[derive(Debug, Clone, Copy)] - #[allow(non_camel_case_types)] - pub enum TransformPass { - all(bool), - $($pass(bool),)* - } - - #[cfg(feature = "cli")] - impl clap::ValueEnum for TransformPass { - fn value_variants<'a>() -> &'a [Self] { - &[ - Self::all(true), Self::all(false), - $(Self::$pass(true), Self::$pass(false),)* - ] - } - - fn to_possible_value(&self) -> Option { - use TransformPass::*; - Some(match self { - all(true) => clap::builder::PossibleValue::new("all"), - all(false) => clap::builder::PossibleValue::new("no-all"), - $( - $pass(true) => clap::builder::PossibleValue::new($name)$(.alias($alias))*, - $pass(false) => clap::builder::PossibleValue::new(concat!("no-", $name))$(.alias(concat!("no-", $alias)))*, - )* - }) - } - } - - impl From<&[TransformPass]> for TransformPasses { - fn from(args: &[TransformPass]) -> TransformPasses { - use TransformPass::*; - let mut opts = TransformPasses::NONE; - for arg in args { - match arg { - all(true) => opts = TransformPasses::ALL, - all(false) => opts = TransformPasses::NONE, - $(&$pass(b) => opts.$pass = b,)* - } - } - opts - } - } - }; -} - -transform_passes! { - pre_reduce: "pre-reduce" | "pre", - coalesce_ctrs: "coalesce-ctrs" | "coalesce", - encode_adts: "encode-adts" | "adts", - eta_reduce: "eta-reduce" | "eta", - inline: "inline", - prune: "prune", -} diff --git a/src/util.rs b/src/util.rs deleted file mode 100644 index a4770622..00000000 --- a/src/util.rs +++ /dev/null @@ -1,15 +0,0 @@ -mod apply_tree; -pub(crate) mod array_vec; -mod bi_enum; -mod create_var; -mod deref; -mod maybe_grow; -mod parse_abbrev_number; -mod stats; - -pub(crate) use bi_enum::*; -pub(crate) use create_var::*; -pub(crate) use deref::*; -pub(crate) use maybe_grow::*; -pub use parse_abbrev_number::*; -pub use stats::*; diff --git a/src/util/apply_tree.rs b/src/util/apply_tree.rs deleted file mode 100644 index 772f4fe1..00000000 --- a/src/util/apply_tree.rs +++ /dev/null @@ -1,48 +0,0 @@ -use crate::prelude::*; - -use super::{create_var, var_to_num}; -use crate::ast::{Net, Tree}; - -impl Net { - /// Transforms the net `x & ...` into `y & x ~ (arg y) & ...` - /// - /// The result is equivalent a λ-calculus application. Thus, - /// if the net is a λ-calculus term, then this function will - /// apply an argument to it. - pub fn apply_tree(&mut self, arg: Tree) { - let mut fresh = 0usize; - self.ensure_no_conflicts(&mut fresh); - arg.ensure_no_conflicts(&mut fresh); - - let fresh_str = create_var(fresh + 1); - - let fun = mem::take(&mut self.root); - let app = Tree::Ctr { lab: 0, ports: vec![arg, Tree::Var { nam: fresh_str.clone() }] }; - self.root = Tree::Var { nam: fresh_str }; - self.redexes.push((fun, app)); - } - - pub(crate) fn ensure_no_conflicts(&self, fresh: &mut usize) { - self.root.ensure_no_conflicts(fresh); - for (a, b) in &self.redexes { - a.ensure_no_conflicts(fresh); - b.ensure_no_conflicts(fresh); - } - } -} - -impl Tree { - /// Increases `fresh` until `create_var(*fresh)` does not conflict - /// with a [`Tree::Var`] in `tree` - /// - /// This function can be called multiple times with many trees to - /// ensure that `fresh` does not conflict with any of them. - pub(crate) fn ensure_no_conflicts(&self, fresh: &mut usize) { - if let Tree::Var { nam } = self { - if let Some(var_num) = var_to_num(nam) { - *fresh = (*fresh).max(var_num); - } - } - self.children().for_each(|child| child.ensure_no_conflicts(fresh)); - } -} diff --git a/src/util/stats.rs b/src/util/stats.rs deleted file mode 100644 index 6b155556..00000000 --- a/src/util/stats.rs +++ /dev/null @@ -1,37 +0,0 @@ -use crate::prelude::*; - -use crate::run::Rewrites; -use core::{str, time::Duration}; - -pub fn show_rewrites(rwts: &Rewrites) -> String { - format!( - "{}{}{}{}{}{}", - format_args!("RWTS : {:>15}\n", pretty_num(rwts.total())), - format_args!("- ANNI : {:>15}\n", pretty_num(rwts.anni)), - format_args!("- COMM : {:>15}\n", pretty_num(rwts.comm)), - format_args!("- ERAS : {:>15}\n", pretty_num(rwts.eras)), - format_args!("- DREF : {:>15}\n", pretty_num(rwts.dref)), - format_args!("- OPER : {:>15}\n", pretty_num(rwts.oper)), - ) -} - -pub fn show_stats(rwts: &Rewrites, elapsed: Duration) -> String { - format!( - "{}{}{}", - show_rewrites(rwts), - format_args!("TIME : {:.3?}\n", elapsed), - format_args!("RPS : {:.3} M\n", (rwts.total() as f64) / (elapsed.as_millis() as f64) / 1000.0), - ) -} - -#[rustfmt::skip] // utterly unreadable on one line -fn pretty_num(n: u64) -> String { - n.to_string() - .as_bytes() - .rchunks(3) - .rev() - .map(|x| str::from_utf8(x).unwrap()) - .flat_map(|x| ["_", x]) - .skip(1) - .collect() -} diff --git a/tests/cli.rs b/tests/cli.rs index d89ba9dc..21c26bd3 100644 --- a/tests/cli.rs +++ b/tests/cli.rs @@ -3,15 +3,13 @@ use std::{ error::Error, io::Read, - path::PathBuf, process::{Command, ExitStatus, Stdio}, }; -use hvmc::{ - ast::{Net, Tree}, - host::Host, -}; -use insta::assert_display_snapshot; +use hvmc_ast::{Net, Tree}; +use hvmc_host::Host; +use hvmc_runtime as run; +use insta::assert_snapshot; fn get_arithmetic_program_path() -> String { env!("CARGO_MANIFEST_DIR").to_owned() + "/examples/arithmetic.hvmc" @@ -41,17 +39,17 @@ fn execute_hvmc(args: &[&str]) -> Result<(ExitStatus, String), Box> { #[test] fn test_cli_reduce() { // Test normal-form expressions - assert_display_snapshot!( + assert_snapshot!( execute_hvmc(&["reduce", "-m", "100M", "--", "#1"]).unwrap().1, @"#1" ); // Test non-normal form expressions - assert_display_snapshot!( + assert_snapshot!( execute_hvmc(&["reduce", "-m", "100M", "--", "a & #3 ~ <* #4 a>"]).unwrap().1, @"#12" ); // Test multiple expressions - assert_display_snapshot!( + assert_snapshot!( execute_hvmc(&["reduce", "-m", "100M", "--", "a & #3 ~ <* #4 a>", "a & #64 ~ "]).unwrap().1, @"#12\n#32" ); @@ -59,7 +57,7 @@ fn test_cli_reduce() { // Test loading file and reducing expression let arithmetic_program = get_arithmetic_program_path(); - assert_display_snapshot!( + assert_snapshot!( execute_hvmc(&[ "reduce", "-m", "100M", &arithmetic_program, @@ -68,7 +66,7 @@ fn test_cli_reduce() { @"#12" ); - assert_display_snapshot!( + assert_snapshot!( execute_hvmc(&[ "reduce", "-m", "100M", &arithmetic_program, @@ -83,7 +81,7 @@ fn test_cli_run_with_args() { let arithmetic_program = get_arithmetic_program_path(); // Test simple program running - assert_display_snapshot!( + assert_snapshot!( execute_hvmc(&[ "run", "-m", "100M", &arithmetic_program, @@ -92,7 +90,7 @@ fn test_cli_run_with_args() { ); // Test partial argument passing - assert_display_snapshot!( + assert_snapshot!( execute_hvmc(&[ "run", "-m", "100M", &arithmetic_program, @@ -102,7 +100,7 @@ fn test_cli_run_with_args() { ); // Test passing all arguments. - assert_display_snapshot!( + assert_snapshot!( execute_hvmc(&[ "run", "-m", "100M", &arithmetic_program, @@ -118,7 +116,7 @@ fn test_cli_transform() { let arithmetic_program = get_arithmetic_program_path(); // Test simple program running - assert_display_snapshot!( + assert_snapshot!( execute_hvmc(&[ "transform", "-Opre-reduce", @@ -139,7 +137,7 @@ fn test_cli_transform() { "### ); - assert_display_snapshot!( + assert_snapshot!( execute_hvmc(&[ "transform", "-Opre-reduce", @@ -165,7 +163,7 @@ fn test_cli_transform() { // Test log - assert_display_snapshot!( + assert_snapshot!( execute_hvmc(&[ "transform", "-Opre-reduce", @@ -181,7 +179,7 @@ fn test_cli_transform() { #[test] fn test_cli_errors() { // Test passing all arguments. - assert_display_snapshot!( + assert_snapshot!( execute_hvmc(&[ "run", "this-file-does-not-exist.hvmc" ]).unwrap().1, @@ -189,7 +187,7 @@ fn test_cli_errors() { Input file "this-file-does-not-exist.hvmc" not found "### ); - assert_display_snapshot!( + assert_snapshot!( execute_hvmc(&[ "reduce", "this-file-does-not-exist.hvmc" ]).unwrap().1, @@ -201,7 +199,6 @@ fn test_cli_errors() { #[test] fn test_apply_tree() { - use hvmc::run; fn eval_with_args(fun: &str, args: &[&str]) -> Net { let area = run::Heap::new_exact(16).unwrap(); @@ -218,31 +215,31 @@ fn test_apply_tree() { rnet.normal(); host.readback(&rnet) } - assert_display_snapshot!( + assert_snapshot!( eval_with_args("(a a)", &["(a a)"]), @"(a a)" ); - assert_display_snapshot!( + assert_snapshot!( eval_with_args("b & (a b) ~ a", &["(a a)"]), @"a" ); - assert_display_snapshot!( + assert_snapshot!( eval_with_args("(z0 z0)", &["(z1 z1)"]), @"(a a)" ); - assert_display_snapshot!( + assert_snapshot!( eval_with_args("(* #1)", &["(a a)"]), @"#1" ); - assert_display_snapshot!( + assert_snapshot!( eval_with_args("(<+ a b> (a b))", &["#1", "#2"]), @"#3" ); - assert_display_snapshot!( + assert_snapshot!( eval_with_args("(<* a b> (a b))", &["#2", "#3"]), @"#6" ); - assert_display_snapshot!( + assert_snapshot!( eval_with_args("(<* a b> (a b))", &["#2"]), @"(<* #2 a> a)" ); @@ -250,8 +247,6 @@ fn test_apply_tree() { #[test] fn test_cli_compile() { - // Test normal-form expressions - if !Command::new(env!("CARGO_BIN_EXE_hvmc")) .args(["compile", &get_arithmetic_program_path()]) .stdout(Stdio::inherit()) @@ -265,18 +260,12 @@ fn test_cli_compile() { panic!("{:?}", "compilation failed"); }; - let mut output_path = PathBuf::from(env!("CARGO_MANIFEST_DIR")); - output_path.push("examples/arithmetic"); - let mut child = Command::new(&output_path).args(["#40", "#3"]).stdout(Stdio::piped()).spawn().unwrap(); - - let mut stdout = child.stdout.take().ok_or("Couldn't capture stdout!").unwrap(); - child.wait().unwrap(); - let mut output = String::new(); - stdout.read_to_string(&mut output).unwrap(); + let (status, output) = execute_hvmc(&["run", "-i", "examples/arithmetic", "/dev/null", "#40", "#3"]).unwrap(); - assert_display_snapshot!(output, @r###" + assert_snapshot!(format_args!("{status}\n{output}"), @r###" + exit status: 0 [#13 #1] "###); - std::fs::remove_file(&output_path).unwrap(); + std::fs::remove_file("examples/arithmetic").unwrap(); } diff --git a/tests/fuzz.rs b/tests/fuzz.rs deleted file mode 100644 index 651db8a9..00000000 --- a/tests/fuzz.rs +++ /dev/null @@ -1,198 +0,0 @@ -#![cfg(feature = "_fuzz")] -#![feature(const_type_name)] - -use hvmc::{ - fuzz::*, - run::{Addr, Heap, Port, Strict, Tag}, - trace, -}; - -use serial_test::serial; - -type Net<'a> = hvmc::run::Net<'a, Strict>; - -#[test] -#[serial] -fn fuzz_var_link_link_var() { - assert!(cfg!(not(feature = "_fuzz_no_free"))); - trace::set_hook(); - Fuzzer::default().fuzz(|fuzz| { - unsafe { trace::_reset_traces() }; - let heap = Heap::new_exact(16).unwrap(); - let mut net = Net::new(&heap); - let x = net.alloc(); - let y = net.alloc(); - let z = net.alloc(); - let a = Port::new_var(x); - let b = Port::new_var(x.other_half()); - let c = Port::new_var(y); - let d = Port::new_var(y.other_half()); - let e = Port::new_var(z); - let f = Port::new_var(z.other_half()); - net.link_port_port(a.clone(), b.clone()); - net.link_port_port(c.clone(), d.clone()); - net.link_port_port(e.clone(), f.clone()); - let mut nets = net.fork(2); - let mut n0 = nets.next().unwrap(); - let mut n1 = nets.next().unwrap(); - fuzz.scope(|s| { - s.spawn(|| { - let (x, y) = fuzz.maybe_swap(b.clone(), c.clone()); - n0.link_wire_wire(x.wire(), y.wire()); - }); - s.spawn(|| { - let (x, y) = fuzz.maybe_swap(d.clone(), e.clone()); - n1.link_wire_wire(x.wire(), y.wire()); - }); - }); - let used = assert_linked(a, f); - for x in [b, c, d, e] { - if !used.contains(&x.addr()) && x.addr().val().read() != Port::FREE.0 { - panic!("failed to free"); - } - } - }); -} - -#[test] -#[serial] -fn fuzz_pri_link_link_pri() { - assert!(cfg!(not(feature = "_fuzz_no_free"))); - trace::set_hook(); - Fuzzer::default().fuzz(|fuzz| { - unsafe { trace::_reset_traces() }; - let p = Port::new(Tag::Ctr, 0, Addr::NULL); - let q = Port::new(Tag::Ctr, 1, Addr::NULL); - let heap = Heap::new_exact(16).unwrap(); - let mut net = Net::new(&heap); - let x = net.alloc(); - let a = Port::new_var(x); - let b = Port::new_var(x.other_half()); - net.link_port_port(a.clone(), b.clone()); - let mut nets = net.fork(2); - let mut n0 = nets.next().unwrap(); - let mut n1 = nets.next().unwrap(); - fuzz.scope(|s| { - s.spawn(|| { - n0.link_wire_port(a.wire(), p); - }); - s.spawn(|| { - n1.link_wire_port(b.wire(), q); - }); - }); - assert!(n0.redexes.len() == 1 || n1.redexes.len() == 1); - for x in [a, b] { - assert_eq!(x.addr().val().read(), Port::FREE.0); - } - }) -} - -#[test] -#[serial] -fn fuzz_var_link_link_pri() { - assert!(cfg!(not(feature = "_fuzz_no_free"))); - trace::set_hook(); - let heap = Heap::new_exact(16).unwrap(); - Fuzzer::default().fuzz(|fuzz| { - unsafe { trace::_reset_traces() }; - let mut net = Net::new(&heap); - let x = net.alloc(); - let y = net.alloc(); - let a = Port::new_var(x); - let b = Port::new_var(x.other_half()); - let c = Port::new_var(y); - let d = Port::new_var(y.other_half()); - net.link_port_port(a.clone(), b.clone()); - net.link_port_port(c.clone(), d.clone()); - let mut nets = net.fork(2); - let mut n0 = nets.next().unwrap(); - let mut n1 = nets.next().unwrap(); - fuzz.scope(|s| { - s.spawn(|| { - let (x, y) = fuzz.maybe_swap(b.clone(), c.clone()); - n0.link_wire_wire(x.wire(), y.wire()); - }); - s.spawn(|| { - n1.link_wire_port(d.wire(), Port::ERA); - }); - }); - let at = Port(a.addr().val().read()); - assert_eq!(at, Port::ERA); - // TODO: reenable leak detection - if false { - for x in [b, c, d] { - assert_eq!(Port(x.addr().val().read()), Port::FREE, "failed to free {:?}", x.wire()); - } - } - }) -} - -#[test] -#[serial] -#[ignore = "very slow"] // takes ~50m on my M3 Max (or ~13.5h with tracing enabled) -fn fuzz_var_link_link_link_var() { - assert!(cfg!(feature = "_fuzz_no_free")); - trace::set_hook(); - let heap = Heap::new_exact(16).unwrap(); - Fuzzer::default().fuzz(|fuzz| { - unsafe { trace::_reset_traces() }; - let mut net = Net::new(&heap); - let x = net.alloc(); - let y = net.alloc(); - let z = net.alloc(); - let w = net.alloc(); - let a = Port::new_var(x); - let b = Port::new_var(x.other_half()); - let c = Port::new_var(y); - let d = Port::new_var(y.other_half()); - let e = Port::new_var(z); - let f = Port::new_var(z.other_half()); - let g = Port::new_var(w); - let h = Port::new_var(w.other_half()); - net.link_port_port(a.clone(), b.clone()); - net.link_port_port(c.clone(), d.clone()); - net.link_port_port(e.clone(), f.clone()); - net.link_port_port(g.clone(), h.clone()); - let mut nets = net.fork(3); - let mut n0 = nets.next().unwrap(); - let mut n1 = nets.next().unwrap(); - let mut n2 = nets.next().unwrap(); - fuzz.scope(|s| { - s.spawn(|| { - let (x, y) = fuzz.maybe_swap(b.clone(), c.clone()); - n0.link_wire_wire(x.wire(), y.wire()); - }); - s.spawn(|| { - let (x, y) = fuzz.maybe_swap(d, e); - n1.link_wire_wire(x.wire(), y.wire()); - }); - s.spawn(|| { - let (x, y) = fuzz.maybe_swap(f, g); - n2.link_wire_wire(x.wire(), y.wire()); - }); - }); - assert_linked(a, h); - }) -} - -fn assert_linked(x: Port, y: Port) -> Vec { - let mut used = vec![]; - for (x, y) in [(x.clone(), y.clone()), (y, x)] { - let mut p = Port(x.wire().addr().val().read()); - if p != y { - loop { - used.push(p.addr()); - let q = Port(p.addr().val().read()); - if p.tag() == Tag::Red && p.wire() == y.wire() { - break; - } - if q.tag() == Tag::Red { - p = q; - continue; - } - panic!("bad link"); - } - } - } - used -} diff --git a/tests/lists.rs b/tests/lists.rs index 02d3ce0e..feeb0643 100644 --- a/tests/lists.rs +++ b/tests/lists.rs @@ -1,7 +1,7 @@ #![cfg(feature = "std")] use crate::loaders::*; -use hvmc::ast::Book; +use hvmc_ast::{Book, Tree}; use insta::assert_debug_snapshot; mod loaders; @@ -9,9 +9,9 @@ fn list_got(index: u32) -> Book { let code = load_file("list_put_got.hvmc"); let mut book = parse_core(&code); let def = book.get_mut("GenGotIndex").unwrap(); - def.apply_tree(hvmc::ast::Tree::Ref { nam: format!("S{index}") }); + def.apply_tree(Tree::Ref { nam: format!("S{index}") }); let def = book.get_mut("main").unwrap(); - def.apply_tree(hvmc::ast::Tree::Ref { nam: "GenGotIndex".to_string() }); + def.apply_tree(Tree::Ref { nam: "GenGotIndex".to_string() }); book } @@ -19,10 +19,10 @@ fn list_put(index: u32, value: u32) -> Book { let code = load_file("list_put_got.hvmc"); let mut book = parse_core(&code); let def = book.get_mut("GenPutIndexValue").unwrap(); - def.apply_tree(hvmc::ast::Tree::Ref { nam: format!("S{index}") }); - def.apply_tree(hvmc::ast::Tree::Ref { nam: format!("S{value}") }); + def.apply_tree(Tree::Ref { nam: format!("S{index}") }); + def.apply_tree(Tree::Ref { nam: format!("S{value}") }); let def = book.get_mut("main").unwrap(); - def.apply_tree(hvmc::ast::Tree::Ref { nam: "GenPutIndexValue".to_string() }); + def.apply_tree(Tree::Ref { nam: "GenPutIndexValue".to_string() }); book } diff --git a/tests/loaders/mod.rs b/tests/loaders.rs similarity index 79% rename from tests/loaders/mod.rs rename to tests/loaders.rs index e97434a7..a7586a73 100644 --- a/tests/loaders/mod.rs +++ b/tests/loaders.rs @@ -1,6 +1,9 @@ #![allow(dead_code)] -use hvmc::{ast::*, run, stdlib::create_host}; +// use hvmc::{ast::*, run, stdlib::create_host}; +use hvmc_ast::{Book, Net}; +use hvmc_host::stdlib::create_host; +use hvmc_runtime as run; use std::fs; pub fn load_file(file: &str) -> String { @@ -22,7 +25,7 @@ pub fn replace_template(mut code: String, map: &[(&str, &str)]) -> String { code } -pub fn normal_with(book: Book, mem: Option, entry_point: &str) -> (hvmc::run::Rewrites, Net) { +pub fn normal_with(book: Book, mem: Option, entry_point: &str) -> (run::Rewrites, Net) { let area = run::Heap::new(mem).unwrap(); let host = create_host(&book); @@ -34,6 +37,6 @@ pub fn normal_with(book: Book, mem: Option, entry_point: &str) -> (hvmc:: (rnet.rwts, net) } -pub fn normal(book: Book, mem: Option) -> (hvmc::run::Rewrites, Net) { +pub fn normal(book: Book, mem: Option) -> (run::Rewrites, Net) { normal_with(book, mem, "main") } diff --git a/tests/programs/bool_and.hvmc b/tests/programs/bool_and.hvmc new file mode 100644 index 00000000..74f0a8cc --- /dev/null +++ b/tests/programs/bool_and.hvmc @@ -0,0 +1,4 @@ +@true = (b (* b)) +@false = (* (b b)) +@and = ((b (@false c)) (b c)) +@main = root & @and ~ (@true (@false root)) diff --git a/tests/programs/commutation.hvmc b/tests/programs/commutation.hvmc new file mode 100644 index 00000000..d64f3e20 --- /dev/null +++ b/tests/programs/commutation.hvmc @@ -0,0 +1 @@ +@main = root & (x x) ~ [* root] diff --git a/tests/programs/era_era.hvmc b/tests/programs/era_era.hvmc new file mode 100644 index 00000000..3c3563fb --- /dev/null +++ b/tests/programs/era_era.hvmc @@ -0,0 +1 @@ +@main = * & * ~ * diff --git a/tests/snapshots/tests__pre_reduce_run@bool_and.hvmc.snap b/tests/snapshots/tests__pre_reduce_run@bool_and.hvmc.snap new file mode 100644 index 00000000..a663b4c0 --- /dev/null +++ b/tests/snapshots/tests__pre_reduce_run@bool_and.hvmc.snap @@ -0,0 +1,20 @@ +--- +source: tests/tests.rs +expression: output +input_file: tests/programs/bool_and.hvmc +--- +(* (a a)) +pre-reduce: +RWTS : 3 +- ANNI : 0 +- COMM : 0 +- ERAS : 0 +- DREF : 3 +- OPER : 0 +run: +RWTS : 14 +- ANNI : 4 +- COMM : 0 +- ERAS : 1 +- DREF : 9 +- OPER : 0 diff --git a/tests/snapshots/tests__pre_reduce_run@commutation.hvmc.snap b/tests/snapshots/tests__pre_reduce_run@commutation.hvmc.snap new file mode 100644 index 00000000..d923fdfd --- /dev/null +++ b/tests/snapshots/tests__pre_reduce_run@commutation.hvmc.snap @@ -0,0 +1,20 @@ +--- +source: tests/tests.rs +expression: output +input_file: tests/programs/commutation.hvmc +--- +(a a) +pre-reduce: +RWTS : 0 +- ANNI : 0 +- COMM : 0 +- ERAS : 0 +- DREF : 0 +- OPER : 0 +run: +RWTS : 7 +- ANNI : 1 +- COMM : 2 +- ERAS : 1 +- DREF : 3 +- OPER : 0 diff --git a/tests/snapshots/tests__pre_reduce_run@era_era.hvmc.snap b/tests/snapshots/tests__pre_reduce_run@era_era.hvmc.snap new file mode 100644 index 00000000..9635ee51 --- /dev/null +++ b/tests/snapshots/tests__pre_reduce_run@era_era.hvmc.snap @@ -0,0 +1,20 @@ +--- +source: tests/tests.rs +expression: output +input_file: tests/programs/era_era.hvmc +--- +* +pre-reduce: +RWTS : 0 +- ANNI : 0 +- COMM : 0 +- ERAS : 0 +- DREF : 0 +- OPER : 0 +run: +RWTS : 3 +- ANNI : 0 +- COMM : 0 +- ERAS : 1 +- DREF : 2 +- OPER : 0 diff --git a/tests/snapshots/tests__run@bool_and.hvmc.snap b/tests/snapshots/tests__run@bool_and.hvmc.snap new file mode 100644 index 00000000..099b81d8 --- /dev/null +++ b/tests/snapshots/tests__run@bool_and.hvmc.snap @@ -0,0 +1,12 @@ +--- +source: tests/tests.rs +expression: output +input_file: tests/programs/bool_and.hvmc +--- +(* (a a)) +RWTS : 14 +- ANNI : 4 +- COMM : 0 +- ERAS : 1 +- DREF : 9 +- OPER : 0 diff --git a/tests/snapshots/tests__run@commutation.hvmc.snap b/tests/snapshots/tests__run@commutation.hvmc.snap new file mode 100644 index 00000000..276b3763 --- /dev/null +++ b/tests/snapshots/tests__run@commutation.hvmc.snap @@ -0,0 +1,12 @@ +--- +source: tests/tests.rs +expression: output +input_file: tests/programs/commutation.hvmc +--- +(a a) +RWTS : 7 +- ANNI : 1 +- COMM : 2 +- ERAS : 1 +- DREF : 3 +- OPER : 0 diff --git a/tests/snapshots/tests__run@era_era.hvmc.snap b/tests/snapshots/tests__run@era_era.hvmc.snap new file mode 100644 index 00000000..f7e4ff2d --- /dev/null +++ b/tests/snapshots/tests__run@era_era.hvmc.snap @@ -0,0 +1,12 @@ +--- +source: tests/tests.rs +expression: output +input_file: tests/programs/era_era.hvmc +--- +* +RWTS : 3 +- ANNI : 0 +- COMM : 0 +- ERAS : 1 +- DREF : 2 +- OPER : 0 diff --git a/tests/tests.rs b/tests/tests.rs index 8e5124e5..2656175a 100644 --- a/tests/tests.rs +++ b/tests/tests.rs @@ -1,5 +1,6 @@ #![cfg(feature = "std")] +use hvmc_transform::pre_reduce::PreReduce; use parking_lot::Mutex; use std::{ fs, @@ -10,62 +11,17 @@ use std::{ time::Instant, }; -use hvmc::{ - ast::{self, Book, Net}, - host::Host, - run::{self, Strict}, - util::show_rewrites, -}; -use insta::{assert_debug_snapshot, assert_snapshot}; -use loaders::*; +use hvmc_ast::{self as ast, Book, Net}; +use hvmc_host::{stdlib::create_host, Host}; +use hvmc_runtime as run; -mod loaders; +use insta::assert_snapshot; use serial_test::serial; -#[test] -fn test_era_era() { - let net = parse_core("@main = * & * ~ *"); - let (rwts, net) = normal(net, Some(128)); - assert_snapshot!(Net::to_string(&net), @"*"); - assert_debug_snapshot!(rwts.total(), @"3"); -} - -#[test] -fn test_era_era2() { - let net = parse_core("@main = (* *) & * ~ *"); - let (rwts, net) = normal(net, Some(128)); - assert_snapshot!(Net::to_string(&net), @"(* *)"); - assert_debug_snapshot!(rwts.total(), @"5"); -} - -#[test] -fn test_commutation() { - let net = parse_core("@main = root & (x x) ~ [* root]"); - let (rwts, net) = normal(net, Some(128)); - assert_snapshot!(Net::to_string(&net), @"(a a)"); - assert_debug_snapshot!(rwts.total(), @"7"); -} - -#[test] -fn test_bool_and() { - let book = parse_core( - " - @true = (b (* b)) - @false = (* (b b)) - @and = ((b (@false c)) (b c)) - @main = root & @and ~ (@true (@false root)) - ", - ); - let (rwts, net) = normal(book, Some(128)); - - assert_snapshot!(Net::to_string(&net), @"(* (a a))"); - assert_debug_snapshot!(rwts.total(), @"14"); -} - fn execute_host(host: Arc>) -> Option<(run::Rewrites, Net)> { let heap = run::Heap::new(None).unwrap(); - let mut net = run::Net::::new(&heap); + let mut net = run::Net::::new(&heap); // The host is locked inside this block. { let lock = host.lock(); @@ -87,7 +43,7 @@ fn test_run(name: &str, host: Arc>) { let Some((rwts, net)) = execute_host(host) else { return }; - let output = format!("{}\n{}", net, show_rewrites(&rwts)); + let output = format!("{}\n{}", net, &rwts); assert_snapshot!(output); } @@ -101,20 +57,20 @@ fn test_pre_reduce_run(path: &str, mut book: Book) { print!(" {:.3?}...", start.elapsed()); io::stdout().flush().unwrap(); - let host = hvmc::stdlib::create_host(&book); + let host = create_host(&book); let Some((rwts, net)) = execute_host(host) else { - assert_snapshot!(show_rewrites(&pre_stats.rewrites)); + assert_snapshot!(&pre_stats.rewrites); return; }; - let output = format!("{}\npre-reduce:\n{}run:\n{}", net, show_rewrites(&pre_stats.rewrites), show_rewrites(&rwts)); + let output = format!("{}\npre-reduce:\n{}run:\n{}", net, &pre_stats.rewrites, &rwts); assert_snapshot!(output); } fn test_path(path: &Path) { let code = fs::read_to_string(path).unwrap(); let book = ast::Book::from_str(&code).unwrap(); - let host = hvmc::stdlib::create_host(&book); + let host = create_host(&book); let path = path.strip_prefix(env!("CARGO_MANIFEST_DIR")).unwrap(); let path = path.to_str().unwrap(); diff --git a/tests/transform.rs b/tests/transform.rs index 25247af5..0dba767e 100644 --- a/tests/transform.rs +++ b/tests/transform.rs @@ -2,10 +2,16 @@ //! Tests for transformation passes -pub mod loaders; +use hvmc_ast::{Book, Net, Tree}; +use hvmc_transform::{ + coalesce_ctrs::CoalesceCtrs, encode_adts::EncodeAdts, eta_reduce::EtaReduce, inline::Inline, pre_reduce::PreReduce, + prune::Prune, TransformError, +}; -use hvmc::{transform::TransformError, util::show_rewrites}; -use insta::{assert_display_snapshot, assert_snapshot}; +use insta::assert_snapshot; +use std::str::FromStr; + +mod loaders; use loaders::*; #[test] @@ -18,8 +24,8 @@ pub fn test_fast_pre_reduce() { let rwts_2 = book_2.pre_reduce(&|x| !["expensive_1", "expensive_2", "main_slow"].contains(&x), None, u64::MAX).rewrites; - let rwts_1 = show_rewrites(&(rwts_1 + normal_with(book_1, None, "main_fast").0)); - let rwts_2 = show_rewrites(&(rwts_2 + normal_with(book_2, None, "main_slow").0)); + let rwts_1 = rwts_1 + normal_with(book_1, None, "main_fast").0; + let rwts_2 = rwts_2 + normal_with(book_2, None, "main_slow").0; assert_snapshot!(format!("Fast:\n{rwts_1}Slow:\n{rwts_2}"), @r###" Fast: @@ -41,66 +47,60 @@ pub fn test_fast_pre_reduce() { #[test] pub fn test_adt_encoding() { - use hvmc::ast::{Net, Tree}; - use std::str::FromStr; pub fn parse_and_encode(net: &str) -> String { let mut net = Net::from_str(net).unwrap(); net.trees_mut().for_each(Tree::coalesce_constructors); net.trees_mut().for_each(Tree::encode_scott_adts); format!("{net}") } - assert_display_snapshot!(parse_and_encode("(a (b (c d)))"), @"(a b c d)"); - assert_display_snapshot!(parse_and_encode("(a (b c (d e)))"), @"(a b c d e)"); - assert_display_snapshot!(parse_and_encode("(a b c d e f g h)"), @"(a b c d e f g h)"); - assert_display_snapshot!(parse_and_encode("(a b c d (e f g h (i j k l)))"), @"(a b c d (e f g h (i j k l)))"); - - assert_display_snapshot!(parse_and_encode("(* ((a R) R))"), @"(:1:2 a)"); - assert_display_snapshot!(parse_and_encode("((a R) (* R))"), @"(:0:2 a)"); - assert_display_snapshot!(parse_and_encode("(* (* ((a R) R)))"), @"(:2:3 a)"); - assert_display_snapshot!(parse_and_encode("(* ((a R) (* R)))"), @"(:1:3 a)"); - assert_display_snapshot!(parse_and_encode("((a (b R)) R)"), @"(:0:1 a b)"); - assert_display_snapshot!(parse_and_encode("((a (b (c R))) R)"), @"(:0:1 a b c)"); - assert_display_snapshot!(parse_and_encode("(* ((a (b (c R))) R))"), @"(:1:2 a b c)"); - assert_display_snapshot!(parse_and_encode("{4 * {4 {4 a {4 b {4 c R}}} R}}"), @"{4:1:2 a b c}"); - assert_display_snapshot!(parse_and_encode("(* x x)"), @"(:1:2)"); - assert_display_snapshot!(parse_and_encode("(((((* x x) x) * x) x) * x)"), @"(:0:2 (:0:2 (:1:2)))"); - assert_display_snapshot!(parse_and_encode("(a b * (a b c) * c)"), @"(a b (:1:3 a b))"); - assert_display_snapshot!(parse_and_encode("(* (:0:1))"), @"(:1:2)"); - assert_display_snapshot!(parse_and_encode("(a * (:0:1))"), @"(a (:1:2))"); + assert_snapshot!(parse_and_encode("(a (b (c d)))"), @"(a b c d)"); + assert_snapshot!(parse_and_encode("(a (b c (d e)))"), @"(a b c d e)"); + assert_snapshot!(parse_and_encode("(a b c d e f g h)"), @"(a b c d e f g h)"); + assert_snapshot!(parse_and_encode("(a b c d (e f g h (i j k l)))"), @"(a b c d (e f g h (i j k l)))"); + + assert_snapshot!(parse_and_encode("(* ((a R) R))"), @"(:1:2 a)"); + assert_snapshot!(parse_and_encode("((a R) (* R))"), @"(:0:2 a)"); + assert_snapshot!(parse_and_encode("(* (* ((a R) R)))"), @"(:2:3 a)"); + assert_snapshot!(parse_and_encode("(* ((a R) (* R)))"), @"(:1:3 a)"); + assert_snapshot!(parse_and_encode("((a (b R)) R)"), @"(:0:1 a b)"); + assert_snapshot!(parse_and_encode("((a (b (c R))) R)"), @"(:0:1 a b c)"); + assert_snapshot!(parse_and_encode("(* ((a (b (c R))) R))"), @"(:1:2 a b c)"); + assert_snapshot!(parse_and_encode("{4 * {4 {4 a {4 b {4 c R}}} R}}"), @"{4:1:2 a b c}"); + assert_snapshot!(parse_and_encode("(* x x)"), @"(:1:2)"); + assert_snapshot!(parse_and_encode("(((((* x x) x) * x) x) * x)"), @"(:0:2 (:0:2 (:1:2)))"); + assert_snapshot!(parse_and_encode("(a b * (a b c) * c)"), @"(a b (:1:3 a b))"); + assert_snapshot!(parse_and_encode("(* (:0:1))"), @"(:1:2)"); + assert_snapshot!(parse_and_encode("(a * (:0:1))"), @"(a (:1:2))"); } #[test] pub fn test_eta() { - use hvmc::ast::Net; - use std::str::FromStr; pub fn parse_and_reduce(net: &str) -> String { let mut net = Net::from_str(net).unwrap(); net.eta_reduce(); format!("{net}") } - assert_display_snapshot!(parse_and_reduce("((x y) (x y))"), @"(x x)"); - assert_display_snapshot!(parse_and_reduce("((a b c d e f) (a b c d e f))"), @"(a a)"); - assert_display_snapshot!(parse_and_reduce("<+ (a b) (a b)>"), @"<+ a a>"); - assert_display_snapshot!(parse_and_reduce("(a b) & ((a b) (c d)) ~ (c d) "), @r###" + assert_snapshot!(parse_and_reduce("((x y) (x y))"), @"(x x)"); + assert_snapshot!(parse_and_reduce("((a b c d e f) (a b c d e f))"), @"(a a)"); + assert_snapshot!(parse_and_reduce("<+ (a b) (a b)>"), @"<+ a a>"); + assert_snapshot!(parse_and_reduce("(a b) & ((a b) (c d)) ~ (c d) "), @r###" a & (a c) ~ c "###); - assert_display_snapshot!(parse_and_reduce("((a b) [a b])"), @"((a b) [a b])"); - assert_display_snapshot!(parse_and_reduce("((a b c) b c)"), @"((a b) b)"); - assert_display_snapshot!(parse_and_reduce("([(a b) (c d)] [(a b) (c d)])"), @"(a a)"); - assert_display_snapshot!(parse_and_reduce("(* *)"), @"*"); - assert_display_snapshot!(parse_and_reduce("([(#0 #0) (#12345 #12345)] [(* *) (a a)])"), @"([#0 #12345] [* (a a)])"); + assert_snapshot!(parse_and_reduce("((a b) [a b])"), @"((a b) [a b])"); + assert_snapshot!(parse_and_reduce("((a b c) b c)"), @"((a b) b)"); + assert_snapshot!(parse_and_reduce("([(a b) (c d)] [(a b) (c d)])"), @"(a a)"); + assert_snapshot!(parse_and_reduce("(* *)"), @"*"); + assert_snapshot!(parse_and_reduce("([(#0 #0) (#12345 #12345)] [(* *) (a a)])"), @"([#0 #12345] [* (a a)])"); } #[test] pub fn test_inline() { - use hvmc::ast::Book; - use std::str::FromStr; pub fn parse_and_inline(net: &str) -> Result { let mut net = Book::from_str(net).unwrap(); net.inline().map(|_| format!("{net}")) } - assert_display_snapshot!(parse_and_inline(" + assert_snapshot!(parse_and_inline(" @era = * @num = #123 @abab = (a b a b) @@ -149,14 +149,12 @@ pub fn test_inline() { #[test] pub fn test_prune() { - use hvmc::ast::Book; - use std::str::FromStr; pub fn parse_and_prune(net: &str) -> String { let mut net = Book::from_str(net).unwrap(); net.prune(&["main".to_owned()]); format!("{net}") } - assert_display_snapshot!(parse_and_prune(" + assert_snapshot!(parse_and_prune(" @self = (* @self) @main = (@main @a @b) @a = (@b @c @d) diff --git a/transform/Cargo.toml b/transform/Cargo.toml new file mode 100644 index 00000000..163a665b --- /dev/null +++ b/transform/Cargo.toml @@ -0,0 +1,24 @@ +[package] +name = "hvmc-transform" +version = "0.1.0" +edition = "2021" + +[lib] +path = "src/transform.rs" + +[dependencies] +ordered-float = { version = "4.2.0", default-features = false } +parking_lot = "0.12.2" +thiserror = "1.0.58" + +hvmc-ast = { path = "../ast", default-features = false } +hvmc-runtime = { path = "../runtime", default-features = false } +hvmc-util = { path = "../util", default-features = false } +hvmc-host = { path = "../host", default-features = false } + +[features] +default = ["std"] +std = ["hvmc-ast/std", "hvmc-runtime/std", "hvmc-util/std", "hvmc-host/std"] + +[lints] +workspace = true diff --git a/src/transform/coalesce_ctrs.rs b/transform/src/coalesce_ctrs.rs similarity index 77% rename from src/transform/coalesce_ctrs.rs rename to transform/src/coalesce_ctrs.rs index 2bf7b770..45743b84 100644 --- a/src/transform/coalesce_ctrs.rs +++ b/transform/src/coalesce_ctrs.rs @@ -1,14 +1,15 @@ use crate::prelude::*; +use hvmc_ast::{Tree, MAX_ARITY}; +use hvmc_util::maybe_grow; -use crate::{ - ast::{Tree, MAX_ARITY}, - util::maybe_grow, -}; +pub trait CoalesceCtrs { + fn coalesce_constructors(&mut self); +} -impl Tree { +impl CoalesceCtrs for Tree { /// Join chains of CTR nodes, such as `(a (b (c d)))` into n-ary nodes `(a b c /// d)` - pub fn coalesce_constructors(&mut self) { + fn coalesce_constructors(&mut self) { maybe_grow(|| match self { Tree::Ctr { lab, ports } => { ports.iter_mut().for_each(Tree::coalesce_constructors); diff --git a/src/transform/encode_adts.rs b/transform/src/encode_adts.rs similarity index 93% rename from src/transform/encode_adts.rs rename to transform/src/encode_adts.rs index 509a4b5d..f05e7919 100644 --- a/src/transform/encode_adts.rs +++ b/transform/src/encode_adts.rs @@ -1,13 +1,14 @@ use crate::prelude::*; +use hvmc_ast::{Tree, MAX_ADT_VARIANTS}; +use hvmc_util::maybe_grow; -use crate::{ - ast::{Tree, MAX_ADT_VARIANTS}, - util::maybe_grow, -}; +pub trait EncodeAdts { + fn encode_scott_adts(&mut self); +} -impl Tree { +impl EncodeAdts for Tree { /// Encode scott-encoded ADTs into optimized compact ADT nodes - pub fn encode_scott_adts(&mut self) { + fn encode_scott_adts(&mut self) { maybe_grow(|| match self { &mut Tree::Ctr { lab, ref mut ports } => { fn get_adt_info(lab: u16, ports: &[Tree]) -> Option<(usize, usize)> { diff --git a/src/transform/eta_reduce.rs b/transform/src/eta_reduce.rs similarity index 97% rename from src/transform/eta_reduce.rs rename to transform/src/eta_reduce.rs index 11e4c23b..f13d3bbd 100644 --- a/src/transform/eta_reduce.rs +++ b/transform/src/eta_reduce.rs @@ -53,16 +53,20 @@ //! //! The pass also reduces subnets such as `(* *) -> *` -use crate::prelude::*; - -use crate::ast::{Net, Tree}; use core::ops::RangeFrom; +use crate::prelude::*; +use hvmc_ast::{Net, Tree}; + use ordered_float::OrderedFloat; -impl Net { +pub trait EtaReduce { + fn eta_reduce(&mut self); +} + +impl EtaReduce for Net { /// Carries out simple eta-reduction - pub fn eta_reduce(&mut self) { + fn eta_reduce(&mut self) { let mut phase1 = Phase1::default(); for tree in self.trees() { phase1.walk_tree(tree); diff --git a/src/transform/inline.rs b/transform/src/inline.rs similarity index 86% rename from src/transform/inline.rs rename to transform/src/inline.rs index 704ba91d..b825fb06 100644 --- a/src/transform/inline.rs +++ b/transform/src/inline.rs @@ -1,14 +1,17 @@ +use core::ops::BitOr; + use crate::prelude::*; +use hvmc_ast::{Book, Net, Tree}; +use hvmc_util::maybe_grow; use super::TransformError; -use crate::{ - ast::{Book, Net, Tree}, - util::maybe_grow, -}; -use core::ops::BitOr; -impl Book { - pub fn inline(&mut self) -> Result, TransformError> { +pub trait Inline { + fn inline(&mut self) -> Result, TransformError>; +} + +impl Inline for Book { + fn inline(&mut self) -> Result, TransformError> { let mut state = InlineState::default(); state.populate_inlinees(self)?; let mut all_changed = Set::new(); @@ -75,7 +78,11 @@ impl InlineState { } } -impl Net { +trait ShouldInline { + fn should_inline(&self) -> bool; +} + +impl ShouldInline for Net { fn should_inline(&self) -> bool { self.redexes.is_empty() && self.root.children().next().is_none() } diff --git a/src/transform/pre_reduce.rs b/transform/src/pre_reduce.rs similarity index 76% rename from src/transform/pre_reduce.rs rename to transform/src/pre_reduce.rs index 8eb86acc..17d134cf 100644 --- a/src/transform/pre_reduce.rs +++ b/transform/src/pre_reduce.rs @@ -4,8 +4,8 @@ //! - Each definition is visited in topological order (dependencies before //! dependents). In the case of cycles, one will be arbitrarily selected to be //! first. -//! - The definition is reduced in a [`run::Net`] -//! - The reduced [`run::Net`] is readback into an [`ast::Net`] +//! - The definition is reduced in a [`hvmc_runtime::Net`] +//! - The reduced [`hvmc_runtime::Net`] is readback into an [`ast::Net`] //! - The [`ast::Net`] is encoded into a [`Vec`] //! - The [`ast::Net`] is stored in the [`State`], as it will be used later. //! - The [`InterpretedDef`] corresponding to the definition is mutated in-place @@ -15,37 +15,36 @@ //! overriding the previous one. use crate::prelude::*; - -use crate::{ - ast::{Book, Net, Tree}, - host::{DefRef, Host}, - run::{self, Def, Heap, InterpretedDef, LabSet, Rewrites}, +use hvmc_ast::{Book, Tree}; +use hvmc_host::{ stdlib::{AsHostedDef, HostedDef}, - util::maybe_grow, + DefRef, Host, }; +use hvmc_runtime::{Def, Heap, InterpretedDef, LabSet, Mode, Port, Rewrites, Strict}; +use hvmc_util::maybe_grow; + use alloc::sync::Arc; use parking_lot::Mutex; -impl Book { +pub trait PreReduce { + fn pre_reduce(&mut self, skip: &dyn Fn(&str) -> bool, max_memory: Option, max_rwts: u64) -> PreReduceStats; +} + +impl PreReduce for Book { /// Reduces the definitions in the book individually, except for the skipped /// ones. /// /// Defs that are not in the book are treated as inert defs. /// /// `max_memory` is measured in bytes. - pub fn pre_reduce( - &mut self, - skip: &dyn Fn(&str) -> bool, - max_memory: Option, - max_rwts: u64, - ) -> PreReduceStats { + fn pre_reduce(&mut self, skip: &dyn Fn(&str) -> bool, max_memory: Option, max_rwts: u64) -> PreReduceStats { let mut host = Host::default(); let captured_redexes = Arc::new(Mutex::new(Vec::new())); // When a ref is not found in the `Host`, put an inert def in its place. host.insert_book_with_default(self, &mut |_| unsafe { HostedDef::new_hosted(LabSet::ALL, InertDef(captured_redexes.clone())) }); - let area = run::Heap::new(max_memory).expect("pre-reduce memory allocation failed"); + let area = Heap::new(max_memory).expect("pre-reduce memory allocation failed"); let mut state = State { book: self, @@ -86,16 +85,16 @@ pub struct PreReduceStats { enum SeenState { Cycled, - Reduced { net: Net, normal: bool }, + Reduced { net: hvmc_ast::Net, normal: bool }, } /// A Def that pushes all interactions to its inner Vec. #[derive(Default)] -struct InertDef(Arc>>); +struct InertDef(Arc>>); impl AsHostedDef for InertDef { - fn call(def: &run::Def, _: &mut run::Net, port: run::Port) { - def.data.0.lock().push((run::Port::new_ref(def), port)); + fn call(def: &Def, _: &mut hvmc_runtime::Net, port: Port) { + def.data.0.lock().push((Port::new_ref(def), port)); } } @@ -107,7 +106,7 @@ struct State<'a> { max_rwts: u64, area: &'a Heap, - captured_redexes: Arc>>, + captured_redexes: Arc>>, skip: &'a dyn Fn(&str) -> bool, seen: Map, @@ -124,7 +123,7 @@ impl<'a> State<'a> { tree.children().for_each(|child| self.visit_tree(child)) }) } - fn visit_net(&mut self, net: &Net) { + fn visit_net(&mut self, net: &hvmc_ast::Net) { self.visit_tree(&net.root); for (a, b) in &net.redexes { self.visit_tree(a); @@ -140,7 +139,7 @@ impl<'a> State<'a> { // First, pre-reduce all nets referenced by this net by walking the tree self.visit_net(self.book.get(nam).unwrap()); - let mut rt = run::Net::::new(self.area); + let mut rt = hvmc_runtime::Net::::new(self.area); rt.boot(self.host.defs.get(nam).expect("No function.")); let n_reduced = rt.reduce(self.max_rwts as usize); @@ -154,8 +153,8 @@ impl<'a> State<'a> { // Mutate the host in-place with the pre-reduced net. let instr = self.host.encode_def(&net); if let DefRef::Owned(def_box) = self.host.defs.get_mut(nam).unwrap() { - let interpreted_def: &mut Def> = def_box.downcast_mut().unwrap(); - interpreted_def.data.0 = instr; + let interpreted_def: &mut Def = def_box.downcast_mut().unwrap(); + interpreted_def.data = instr; }; // Replace the "Cycled" state with the "Reduced" state diff --git a/src/transform/prune.rs b/transform/src/prune.rs similarity index 80% rename from src/transform/prune.rs rename to transform/src/prune.rs index c7f8c926..6c4207e2 100644 --- a/src/transform/prune.rs +++ b/transform/src/prune.rs @@ -1,12 +1,13 @@ use crate::prelude::*; +use hvmc_ast::{Book, Tree}; +use hvmc_util::maybe_grow; -use crate::{ - ast::{Book, Tree}, - util::maybe_grow, -}; +pub trait Prune { + fn prune(&mut self, entrypoints: &[String]); +} -impl Book { - pub fn prune(&mut self, entrypoints: &[String]) { +impl Prune for Book { + fn prune(&mut self, entrypoints: &[String]) { let mut state = PruneState { book: self, unvisited: self.keys().map(|x| x.to_owned()).collect() }; for name in entrypoints { state.visit_def(name); diff --git a/transform/src/transform.rs b/transform/src/transform.rs new file mode 100644 index 00000000..d084d3ec --- /dev/null +++ b/transform/src/transform.rs @@ -0,0 +1,129 @@ +#![cfg_attr(not(feature = "std"), no_std)] + +include!("../../prelude.rs"); + +use hvmc_ast::Book; + +pub mod coalesce_ctrs; +pub mod encode_adts; +pub mod eta_reduce; +pub mod inline; +pub mod pre_reduce; +pub mod prune; + +use coalesce_ctrs::CoalesceCtrs; +use encode_adts::EncodeAdts; +use eta_reduce::EtaReduce; +use inline::Inline; +use pre_reduce::PreReduce; +use prune::Prune; + +#[derive(Debug, Clone, PartialEq, Eq)] +#[non_exhaustive] +#[cfg_attr(feature = "std", derive(thiserror::Error))] +pub enum TransformError { + #[cfg_attr(feature = "std", error("infinite reference cycle in `@{0}`"))] + InfiniteRefCycle(String), +} + +pub trait Transform { + fn transform(&mut self, passes: TransformPasses, opts: &TransformOpts) -> Result<(), TransformError>; +} + +impl Transform for Book { + fn transform(&mut self, passes: TransformPasses, opts: &TransformOpts) -> Result<(), TransformError> { + if passes.prune { + self.prune(&opts.prune_entrypoints); + } + if passes.pre_reduce { + if passes.eta_reduce { + for def in self.nets.values_mut() { + def.eta_reduce(); + } + } + self.pre_reduce( + &|x| opts.pre_reduce_skip.iter().any(|y| x == y), + opts.pre_reduce_memory, + opts.pre_reduce_rewrites, + ); + } + for def in &mut self.nets.values_mut() { + if passes.eta_reduce { + def.eta_reduce(); + } + for tree in def.trees_mut() { + if passes.coalesce_ctrs { + tree.coalesce_constructors(); + } + if passes.encode_adts { + tree.encode_scott_adts(); + } + } + } + if passes.inline { + loop { + let inline_changed = self.inline()?; + if inline_changed.is_empty() { + break; + } + if !(passes.eta_reduce || passes.encode_adts) { + break; + } + for name in inline_changed { + let def = self.get_mut(&name).unwrap(); + if passes.eta_reduce { + def.eta_reduce(); + } + if passes.encode_adts { + for tree in def.trees_mut() { + tree.encode_scott_adts(); + } + } + } + } + } + if passes.prune { + self.prune(&opts.prune_entrypoints); + } + Ok(()) + } +} + +#[derive(Clone, Debug)] +pub struct TransformOpts { + pub pre_reduce_skip: Vec, + pub pre_reduce_memory: Option, + pub pre_reduce_rewrites: u64, + pub prune_entrypoints: Vec, +} + +impl TransformOpts { + pub fn add_entrypoint(&mut self, entrypoint: &str) { + self.pre_reduce_skip.push(entrypoint.to_owned()); + self.prune_entrypoints.push(entrypoint.to_owned()); + } +} + +macro_rules! transform_passes { + ($($pass:ident),* $(,)?) => { + #[derive(Debug, Default, Clone, Copy)] + #[non_exhaustive] + pub struct TransformPasses { + $(pub $pass: bool),* + } + + impl TransformPasses { + pub const NONE: Self = Self { $($pass: false),* }; + pub const ALL: Self = Self { $($pass: true),* }; + } + } +} + +transform_passes! { + pre_reduce, + coalesce_ctrs, + encode_adts, + eta_reduce, + inline, + prune, +} diff --git a/util/Cargo.toml b/util/Cargo.toml new file mode 100644 index 00000000..4c145270 --- /dev/null +++ b/util/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "hvmc-util" +version = "0.1.0" +edition = "2021" + +[dependencies] +arrayvec = { version = "0.7.4", default-features = false } +stacker = { version = "0.1.15", default-features = false } + +[features] +std = ["arrayvec/std"] + +[lints] +workspace = true diff --git a/src/util/array_vec.rs b/util/src/array_vec.rs similarity index 64% rename from src/util/array_vec.rs rename to util/src/array_vec.rs index 824f5212..3e2c6b32 100644 --- a/src/util/array_vec.rs +++ b/util/src/array_vec.rs @@ -7,7 +7,7 @@ trait IsTrue {} impl IsTrue for Assert {} #[allow(private_bounds)] -pub(crate) fn from_array(array: [T; LEN]) -> ArrayVec +pub fn from_array(array: [T; LEN]) -> ArrayVec where Assert<{ LEN <= CAP }>: IsTrue, { @@ -20,7 +20,7 @@ where vec } -pub(crate) fn from_iter(iter: impl IntoIterator) -> ArrayVec { +pub fn from_iter(iter: impl IntoIterator) -> ArrayVec { let mut vec = ArrayVec::new(); for item in iter { vec.push(item); diff --git a/src/util/bi_enum.rs b/util/src/bi_enum.rs similarity index 98% rename from src/util/bi_enum.rs rename to util/src/bi_enum.rs index 541338b5..88abcb9d 100644 --- a/src/util/bi_enum.rs +++ b/util/src/bi_enum.rs @@ -1,4 +1,5 @@ /// Defines bi-directional mappings for a numeric enum. +#[macro_export] macro_rules! bi_enum { ( #[repr($uN:ident)] @@ -51,8 +52,6 @@ macro_rules! bi_enum { }; } -pub(crate) use bi_enum; - #[test] fn test_bi_enum() { use alloc::string::ToString; diff --git a/src/util/create_var.rs b/util/src/create_var.rs similarity index 92% rename from src/util/create_var.rs rename to util/src/create_var.rs index 5a4725a9..02d9be2b 100644 --- a/src/util/create_var.rs +++ b/util/src/create_var.rs @@ -1,7 +1,8 @@ +#[allow(unused_imports)] use crate::prelude::*; /// Creates a variable uniquely identified by `id`. -pub(crate) fn create_var(mut id: usize) -> String { +pub fn create_var(mut id: usize) -> String { let mut txt = Vec::new(); id += 1; while id > 0 { @@ -17,7 +18,7 @@ pub(crate) fn create_var(mut id: usize) -> String { /// /// Returns None when the provided string is not an output of /// `create_var`. -pub(crate) fn var_to_num(s: &str) -> Option { +pub fn var_to_num(s: &str) -> Option { let mut n = 0usize; for i in s.chars() { let i = (i as u32).checked_sub('a' as u32)? as usize; diff --git a/src/util/deref.rs b/util/src/deref.rs similarity index 94% rename from src/util/deref.rs rename to util/src/deref.rs index a0bac15e..70731a1f 100644 --- a/src/util/deref.rs +++ b/util/src/deref.rs @@ -1,3 +1,4 @@ +#[macro_export] macro_rules! deref { ($({$($gen:tt)*})? $ty:ty => self.$field:ident: $trg:ty) => { impl $($($gen)*)? core::ops::Deref for $ty { @@ -13,5 +14,3 @@ macro_rules! deref { } }; } - -pub(crate) use deref; diff --git a/util/src/lib.rs b/util/src/lib.rs new file mode 100644 index 00000000..5f066894 --- /dev/null +++ b/util/src/lib.rs @@ -0,0 +1,20 @@ +#![cfg_attr(not(feature = "std"), no_std)] +#![allow(incomplete_features)] +#![feature(generic_const_exprs)] + +include!("../../prelude.rs"); + +pub mod array_vec; +pub mod bi_enum; +pub mod create_var; +pub mod deref; +pub mod maybe_grow; +pub mod ops; +pub mod parse_abbrev_number; +pub mod pretty_num; + +pub use array_vec::*; +pub use create_var::*; +pub use maybe_grow::*; +pub use parse_abbrev_number::*; +pub use pretty_num::*; diff --git a/src/util/maybe_grow.rs b/util/src/maybe_grow.rs similarity index 65% rename from src/util/maybe_grow.rs rename to util/src/maybe_grow.rs index e0d3fb31..11c04cf8 100644 --- a/src/util/maybe_grow.rs +++ b/util/src/maybe_grow.rs @@ -1,4 +1,4 @@ /// Guard against stack overflows in recursive functions. -pub(crate) fn maybe_grow(f: impl FnOnce() -> R) -> R { +pub fn maybe_grow(f: impl FnOnce() -> R) -> R { stacker::maybe_grow(1024 * 32, 1024 * 1024, f) } diff --git a/src/ops.rs b/util/src/ops.rs similarity index 95% rename from src/ops.rs rename to util/src/ops.rs index 4335eb00..fc77c475 100644 --- a/src/ops.rs +++ b/util/src/ops.rs @@ -1,7 +1,9 @@ mod num; mod word; -use crate::{prelude::*, util::bi_enum}; +use crate::prelude::*; + +use crate::bi_enum; use self::{ num::Numeric, @@ -211,15 +213,21 @@ impl From for u16 { } } -#[cfg_attr(feature = "std", derive(Error))] #[derive(Debug)] pub enum OpParseError { - #[cfg_attr(feature = "std", error("invalid type: {0}"))] Type(String), - #[cfg_attr(feature = "std", error("invalid operator: {0}"))] Op(String), } +impl fmt::Display for OpParseError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + OpParseError::Type(ty) => write!(f, "invalid type: {ty}"), + OpParseError::Op(op) => write!(f, "invalid operator: {op}"), + } + } +} + impl FromStr for TypedOp { type Err = OpParseError; diff --git a/src/ops/num.rs b/util/src/ops/num.rs similarity index 100% rename from src/ops/num.rs rename to util/src/ops/num.rs diff --git a/src/ops/word.rs b/util/src/ops/word.rs similarity index 100% rename from src/ops/word.rs rename to util/src/ops/word.rs diff --git a/src/util/parse_abbrev_number.rs b/util/src/parse_abbrev_number.rs similarity index 100% rename from src/util/parse_abbrev_number.rs rename to util/src/parse_abbrev_number.rs diff --git a/util/src/pretty_num.rs b/util/src/pretty_num.rs new file mode 100644 index 00000000..5844e35c --- /dev/null +++ b/util/src/pretty_num.rs @@ -0,0 +1,6 @@ +use crate::prelude::*; +use core::str::from_utf8; + +pub fn pretty_num(n: u64) -> String { + n.to_string().as_bytes().rchunks(3).rev().map(|x| from_utf8(x).unwrap()).flat_map(|x| ["_", x]).skip(1).collect() +}