commit 3f507064ce20d38ceed13805cf00faa82393c3ce Author: Dominic Date: Sun Oct 16 20:14:55 2022 +0200 upload code diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml new file mode 100644 index 0000000..6147367 --- /dev/null +++ b/.github/workflows/rust.yml @@ -0,0 +1,37 @@ +name: Rust + +on: + push: + branches: [main] + pull_request: + +jobs: + test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - uses: dtolnay/rust-toolchain@stable + + - name: Get Rust Version + id: rust-version + run: echo "::set-output name=version::$(cargo -V | head -n1 | awk '{print $2}')" + - uses: actions/cache@v3 + with: + path: | + ~/.cargo/git + ~/.cargo/registry + target + key: ${{runner.os}}-rust-${{steps.rust-version.outputs.version}} + + - run: cargo test + env: + RUST_BACKTRACE: 1 + + rustfmt: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - uses: dtolnay/rust-toolchain@nightly + with: + components: rustfmt + - run: cargo fmt --all -- --check diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..d14a744 --- /dev/null +++ b/.gitignore @@ -0,0 +1,17 @@ +a.out + +# rust/cargo +/target/ + +# git +*.orig + +# emacs +*~ +\#*# +.#*# + +# intellij +/.idea/ +*.ipr +*.iml diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..ea0dbe2 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,1402 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "adler" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" + +[[package]] +name = "aho-corasick" +version = "0.7.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4f55bd91a0978cbfd91c457a164bab8b4001c833b7f323132c0a4e1922dd44e" +dependencies = [ + "memchr", +] + +[[package]] +name = "ansi_colours" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32678233b67f9056b0c144b39d46dc3218637e8d84ad6038ded339e08b19620d" +dependencies = [ + "rgb", +] + +[[package]] +name = "ansi_term" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d52a9bb7ec0cf484c551830a7ce27bd20d67eac647e1befb56b0be4ee39a55d2" +dependencies = [ + "winapi", +] + +[[package]] +name = "anyhow" +version = "1.0.65" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "98161a4e3e2184da77bb14f02184cdd111e83bbbcc9979dfee3c44b9a85f5602" + +[[package]] +name = "ariadne" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1cb2a2046bea8ce5e875551f5772024882de0b540c7f93dfc5d6cf1ca8b030c" +dependencies = [ + "yansi", +] + +[[package]] +name = "askama" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb98f10f371286b177db5eeb9a6e5396609555686a35e1d4f7b9a9c6d8af0139" +dependencies = [ + "askama_derive", + "askama_escape", + "askama_shared", +] + +[[package]] +name = "askama_derive" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87bf87e6e8b47264efa9bde63d6225c6276a52e05e91bf37eaa8afd0032d6b71" +dependencies = [ + "askama_shared", + "proc-macro2", + "syn", +] + +[[package]] +name = "askama_escape" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "619743e34b5ba4e9703bba34deac3427c72507c7159f5fd030aea8cac0cfe341" + +[[package]] +name = "askama_shared" +version = "0.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf722b94118a07fcbc6640190f247334027685d4e218b794dbfe17c32bf38ed0" +dependencies = [ + "askama_escape", + "mime", + "mime_guess", + "nom", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "atty" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" +dependencies = [ + "hermit-abi", + "libc", + "winapi", +] + +[[package]] +name = "autocfg" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" + +[[package]] +name = "bat" +version = "0.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd1212d80800b3d7614b3725e0b2ee3b45b2b7484805d54b5660c8fa6f706305" +dependencies = [ + "ansi_colours", + "ansi_term", + "bincode", + "bytesize", + "clircle", + "console", + "content_inspector", + "encoding", + "flate2", + "globset", + "once_cell", + "path_abs", + "semver", + "serde", + "serde_yaml", + "syntect", + "thiserror", + "unicode-width", +] + +[[package]] +name = "bincode" +version = "1.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1f45e9417d87227c7a56d22e471c6206462cba514c7590c09aff4cf6d1ddcad" +dependencies = [ + "serde", +] + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bstr" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba3569f383e8f1598449f1a423e72e99569137b47740b1da11ef19af3d5c3223" +dependencies = [ + "memchr", +] + +[[package]] +name = "bytemuck" +version = "1.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f5715e491b5a1598fc2bef5a606847b5dc1d48ea625bd3c02c00de8285591da" + +[[package]] +name = "bytesize" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c58ec36aac5066d5ca17df51b3e70279f5670a72102f5752cb7e7c856adfc70" + +[[package]] +name = "cc" +version = "1.0.73" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2fff2a6927b3bb87f9595d67196a70493f627687a71d87a0d692242c33f58c11" +dependencies = [ + "jobserver", +] + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "clap" +version = "3.2.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86447ad904c7fb335a790c9d7fe3d0d971dc523b8ccd1561a520de9a85302750" +dependencies = [ + "atty", + "bitflags", + "clap_derive", + "clap_lex", + "indexmap", + "once_cell", + "strsim", + "termcolor", + "textwrap", +] + +[[package]] +name = "clap_derive" +version = "3.2.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea0c8bce528c4be4da13ea6fead8965e95b6073585a2f05204bd8f4119f82a65" +dependencies = [ + "heck", + "proc-macro-error", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "clap_lex" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2850f2f5a82cbf437dd5af4d49848fbdfc27c157c3d010345776f952765261c5" +dependencies = [ + "os_str_bytes", +] + +[[package]] +name = "clircle" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e68bbd985a63de680ab4d1ad77b6306611a8f961b282c8b5ab513e6de934e396" +dependencies = [ + "cfg-if", + "libc", + "serde", + "winapi", +] + +[[package]] +name = "compiler" +version = "0.0.0" +dependencies = [ + "anyhow", + "ariadne", + "askama", + "bat", + "clap", + "indexmap", + "lazy-regex", + "libtest-mimic", + "prettyplease", + "proc-macro2", + "quote", + "serde", + "syn", + "tar", + "tempfile", + "vergen", +] + +[[package]] +name = "console" +version = "0.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89eab4d20ce20cea182308bca13088fecea9c05f6776cf287205d41a0ed3c847" +dependencies = [ + "encode_unicode", + "libc", + "once_cell", + "terminal_size", + "unicode-width", + "winapi", +] + +[[package]] +name = "content_inspector" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7bda66e858c683005a53a9a60c69a4aca7eeaa45d124526e389f7aec8e62f38" +dependencies = [ + "memchr", +] + +[[package]] +name = "crc32fast" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b540bd8bc810d3885c6ea91e2018302f68baba2129ab3e88f32389ee9370880d" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "crossbeam-channel" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2dd04ddaf88237dc3b8d8f9a3c1004b506b54b3313403944054d23c0870c521" +dependencies = [ + "cfg-if", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-deque" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "715e8152b692bba2d374b53d4875445368fdf21a94751410af607a5ac677d1fc" +dependencies = [ + "cfg-if", + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "045ebe27666471bb549370b4b0b3e51b07f56325befa4284db65fc89c02511b1" +dependencies = [ + "autocfg", + "cfg-if", + "crossbeam-utils", + "memoffset", + "once_cell", + "scopeguard", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51887d4adc7b564537b15adcfb307936f8075dfcd5f00dde9a9f1d29383682bc" +dependencies = [ + "cfg-if", + "once_cell", +] + +[[package]] +name = "either" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90e5c1c8368803113bf0c9584fc495a58b86dc8a29edbf8fe877d21d9507e797" + +[[package]] +name = "encode_unicode" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a357d28ed41a50f9c765dbfe56cbc04a64e53e5fc58ba79fbc34c10ef3df831f" + +[[package]] +name = "encoding" +version = "0.2.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b0d943856b990d12d3b55b359144ff341533e516d94098b1d3fc1ac666d36ec" +dependencies = [ + "encoding-index-japanese", + "encoding-index-korean", + "encoding-index-simpchinese", + "encoding-index-singlebyte", + "encoding-index-tradchinese", +] + +[[package]] +name = "encoding-index-japanese" +version = "1.20141219.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04e8b2ff42e9a05335dbf8b5c6f7567e5591d0d916ccef4e0b1710d32a0d0c91" +dependencies = [ + "encoding_index_tests", +] + +[[package]] +name = "encoding-index-korean" +version = "1.20141219.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4dc33fb8e6bcba213fe2f14275f0963fd16f0a02c878e3095ecfdf5bee529d81" +dependencies = [ + "encoding_index_tests", +] + +[[package]] +name = "encoding-index-simpchinese" +version = "1.20141219.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d87a7194909b9118fc707194baa434a4e3b0fb6a5a757c73c3adb07aa25031f7" +dependencies = [ + "encoding_index_tests", +] + +[[package]] +name = "encoding-index-singlebyte" +version = "1.20141219.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3351d5acffb224af9ca265f435b859c7c01537c0849754d3db3fdf2bfe2ae84a" +dependencies = [ + "encoding_index_tests", +] + +[[package]] +name = "encoding-index-tradchinese" +version = "1.20141219.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd0e20d5688ce3cab59eb3ef3a2083a5c77bf496cb798dc6fcdb75f323890c18" +dependencies = [ + "encoding_index_tests", +] + +[[package]] +name = "encoding_index_tests" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a246d82be1c9d791c5dfde9a2bd045fc3cbba3fa2b11ad558f27d01712f00569" + +[[package]] +name = "enum-iterator" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "45a0ac4aeb3a18f92eaf09c6bb9b3ac30ff61ca95514fc58cbead1c9a6bf5401" +dependencies = [ + "enum-iterator-derive", +] + +[[package]] +name = "enum-iterator-derive" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "828de45d0ca18782232dfb8f3ea9cc428e8ced380eb26a520baaacfc70de39ce" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "fastrand" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7a407cfaa3385c4ae6b23e84623d48c2798d06e3e6a1878f7f59f17b3f86499" +dependencies = [ + "instant", +] + +[[package]] +name = "filetime" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e94a7bbaa59354bc20dd75b67f23e2797b4490e9d6928203fb105c79e448c86c" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "windows-sys", +] + +[[package]] +name = "flate2" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f82b0f4c27ad9f8bfd1f3208d882da2b09c301bc1c828fd3a00d0216d2fbbff6" +dependencies = [ + "crc32fast", + "miniz_oxide", +] + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "form_urlencoded" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9c384f161156f5260c24a097c56119f9be8c798586aecc13afbcbe7b7e26bf8" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "getrandom" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4eb1a864a501629691edf6c15a593b7a51eebaa1e8468e9ddc623de7c9b58ec6" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "getset" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e45727250e75cc04ff2846a66397da8ef2b3db8e40e0cef4df67950a07621eb9" +dependencies = [ + "proc-macro-error", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "git2" +version = "0.14.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0155506aab710a86160ddb504a480d2964d7ab5b9e62419be69e0032bc5931c" +dependencies = [ + "bitflags", + "libc", + "libgit2-sys", + "log", + "url", +] + +[[package]] +name = "globset" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a1e17342619edbc21a964c2afbeb6c820c6a2560032872f397bb97ea127bd0a" +dependencies = [ + "aho-corasick", + "bstr", + "fnv", + "log", + "regex", +] + +[[package]] +name = "hashbrown" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" + +[[package]] +name = "heck" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2540771e65fc8cb83cd6e8a237f70c319bd5c29f78ed1084ba5d50eeac86f7f9" + +[[package]] +name = "hermit-abi" +version = "0.1.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33" +dependencies = [ + "libc", +] + +[[package]] +name = "idna" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e14ddfc70884202db2244c223200c204c2bda1bc6e0998d11b5e024d657209e6" +dependencies = [ + "unicode-bidi", + "unicode-normalization", +] + +[[package]] +name = "indexmap" +version = "1.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10a35a97730320ffe8e2d410b5d3b69279b98d2c14bdb8b70ea89ecf7888d41e" +dependencies = [ + "autocfg", + "hashbrown", +] + +[[package]] +name = "instant" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "itoa" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c8af84674fe1f223a982c933a0ee1086ac4d4052aa0fb8060c12c6ad838e754" + +[[package]] +name = "jobserver" +version = "0.1.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af25a77299a7f711a01975c35a6a424eb6862092cc2d6c72c4ed6cbc56dfc1fa" +dependencies = [ + "libc", +] + +[[package]] +name = "lazy-regex" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6b12f2eb6ed7d39405c5eb25a034b4c106a9ad87a6d9be3298de6c5f32fd57d" +dependencies = [ + "lazy-regex-proc_macros", + "once_cell", + "regex", +] + +[[package]] +name = "lazy-regex-proc_macros" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2496e5264069bc726ccf37eb76b9cd89406ae110d836c3f76729f99c8a23293" +dependencies = [ + "proc-macro2", + "quote", + "regex", + "syn", +] + +[[package]] +name = "lazy_static" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" + +[[package]] +name = "libc" +version = "0.2.133" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0f80d65747a3e43d1596c7c5492d95d5edddaabd45a7fcdb02b95f644164966" + +[[package]] +name = "libgit2-sys" +version = "0.13.4+1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0fa6563431ede25f5cc7f6d803c6afbc1c5d3ad3d4925d12c882bf2b526f5d1" +dependencies = [ + "cc", + "libc", + "libz-sys", + "pkg-config", +] + +[[package]] +name = "libtest-mimic" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc195aab5b803465bf614a5a4765741abce6c8d64e7d8ca57acd2923661fba9f" +dependencies = [ + "clap", + "crossbeam-channel", + "rayon", + "termcolor", +] + +[[package]] +name = "libz-sys" +version = "1.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9702761c3935f8cc2f101793272e202c72b99da8f4224a19ddcf1279a6450bbf" +dependencies = [ + "cc", + "libc", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "linked-hash-map" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f" + +[[package]] +name = "log" +version = "0.4.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "abb12e687cfb44aa40f41fc3978ef76448f9b6038cad6aef4259d3c095a2382e" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "memchr" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" + +[[package]] +name = "memoffset" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5aa361d4faea93603064a027415f07bd8e1d5c88c9fbf68bf56a285428fd79ce" +dependencies = [ + "autocfg", +] + +[[package]] +name = "mime" +version = "0.3.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a60c7ce501c71e03a9c9c0d35b861413ae925bd979cc7a4e30d060069aaac8d" + +[[package]] +name = "mime_guess" +version = "2.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4192263c238a5f0d0c6bfd21f336a313a4ce1c450542449ca191bb657b4642ef" +dependencies = [ + "mime", + "unicase", +] + +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + +[[package]] +name = "miniz_oxide" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96590ba8f175222643a85693f33d26e9c8a015f599c216509b1a6894af675d34" +dependencies = [ + "adler", +] + +[[package]] +name = "nom" +version = "7.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8903e5a29a317527874d0402f867152a3d21c908bb0b933e416c65e301d4c36" +dependencies = [ + "memchr", + "minimal-lexical", +] + +[[package]] +name = "num-integer" +version = "0.1.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "225d3389fb3509a24c93f5c29eb6bde2586b98d9f016636dff58d7c6f7569cd9" +dependencies = [ + "autocfg", + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "578ede34cf02f8924ab9447f50c28075b4d3e5b269972345e7e0372b38c6cdcd" +dependencies = [ + "autocfg", +] + +[[package]] +name = "num_cpus" +version = "1.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19e64526ebdee182341572e50e9ad03965aa510cd94427a4549448f285e957a1" +dependencies = [ + "hermit-abi", + "libc", +] + +[[package]] +name = "num_threads" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2819ce041d2ee131036f4fc9d6ae7ae125a3a40e97ba64d04fe799ad9dabbb44" +dependencies = [ + "libc", +] + +[[package]] +name = "once_cell" +version = "1.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f7254b99e31cad77da24b08ebf628882739a608578bb1bcdfc1f9c21260d7c0" + +[[package]] +name = "onig" +version = "6.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c4b31c8722ad9171c6d77d3557db078cab2bd50afcc9d09c8b315c59df8ca4f" +dependencies = [ + "bitflags", + "libc", + "once_cell", + "onig_sys", +] + +[[package]] +name = "onig_sys" +version = "69.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b829e3d7e9cc74c7e315ee8edb185bf4190da5acde74afd7fc59c35b1f086e7" +dependencies = [ + "cc", + "pkg-config", +] + +[[package]] +name = "os_str_bytes" +version = "6.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ff7415e9ae3fff1225851df9e0d9e4e5479f947619774677a63572e55e80eff" + +[[package]] +name = "paste" +version = "1.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1de2e551fb905ac83f73f7aedf2f0cb4a0da7e35efa24a202a936269f1f18e1" + +[[package]] +name = "path_abs" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05ef02f6342ac01d8a93b65f96db53fe68a92a15f41144f97fb00a9e669633c3" +dependencies = [ + "std_prelude", +] + +[[package]] +name = "percent-encoding" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "478c572c3d73181ff3c2539045f6eb99e5491218eae919370993b890cdbdd98e" + +[[package]] +name = "pkg-config" +version = "0.3.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1df8c4ec4b0627e53bdf214615ad287367e482558cf84b109250b37464dc03ae" + +[[package]] +name = "prettyplease" +version = "0.1.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a49e86d2c26a24059894a3afa13fd17d063419b05dfb83f06d9c3566060c3f5a" +dependencies = [ + "proc-macro2", + "syn", +] + +[[package]] +name = "proc-macro-error" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" +dependencies = [ + "proc-macro-error-attr", + "proc-macro2", + "quote", + "syn", + "version_check", +] + +[[package]] +name = "proc-macro-error-attr" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" +dependencies = [ + "proc-macro2", + "quote", + "version_check", +] + +[[package]] +name = "proc-macro2" +version = "1.0.43" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a2ca2c61bc9f3d74d2886294ab7b9853abd9c1ad903a3ac7815c58989bb7bab" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbe448f377a7d6961e30f5955f9b8d106c3f5e449d493ee1b125c1d43c2b5179" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom", +] + +[[package]] +name = "rayon" +version = "1.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd99e5772ead8baa5215278c9b15bf92087709e9c1b2d1f97cdb5a183c933a7d" +dependencies = [ + "autocfg", + "crossbeam-deque", + "either", + "rayon-core", +] + +[[package]] +name = "rayon-core" +version = "1.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "258bcdb5ac6dad48491bb2992db6b7cf74878b0384908af124823d118c99683f" +dependencies = [ + "crossbeam-channel", + "crossbeam-deque", + "crossbeam-utils", + "num_cpus", +] + +[[package]] +name = "redox_syscall" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a" +dependencies = [ + "bitflags", +] + +[[package]] +name = "regex" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c4eb3267174b8c6c2f654116623910a0fef09c4753f8dd83db29c48a0df988b" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.6.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3f87b73ce11b1619a3c6332f45341e0047173771e8b8b73f87bfeefb7b56244" + +[[package]] +name = "remove_dir_all" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3acd125665422973a33ac9d3dd2df85edad0f4ae9b00dafb1a05e43a9f5ef8e7" +dependencies = [ + "winapi", +] + +[[package]] +name = "rgb" +version = "0.8.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3603b7d71ca82644f79b5a06d1220e9a58ede60bd32255f698cb1af8838b8db3" +dependencies = [ + "bytemuck", +] + +[[package]] +name = "rustversion" +version = "1.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97477e48b4cf8603ad5f7aaf897467cf42ab4218a38ef76fb14c2d6773a6d6a8" + +[[package]] +name = "ryu" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4501abdff3ae82a1c1b477a17252eb69cee9e66eb915c1abaa4f44d873df9f09" + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "scopeguard" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" + +[[package]] +name = "semver" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e25dfac463d778e353db5be2449d1cce89bd6fd23c9f1ea21310ce6e5a1b29c4" + +[[package]] +name = "serde" +version = "1.0.144" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f747710de3dcd43b88c9168773254e809d8ddbdf9653b84e2554ab219f17860" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.144" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94ed3a816fb1d101812f83e789f888322c34e291f894f19590dc310963e87a00" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.85" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e55a28e3aaef9d5ce0506d0a14dbba8054ddc7e499ef522dd8b26859ec9d4a44" +dependencies = [ + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "serde_yaml" +version = "0.8.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "578a7433b776b56a35785ed5ce9a7e777ac0598aac5a6dd1b4b18a307c7fc71b" +dependencies = [ + "indexmap", + "ryu", + "serde", + "yaml-rust", +] + +[[package]] +name = "std_prelude" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8207e78455ffdf55661170876f88daf85356e4edd54e0a3dbc79586ca1e50cbe" + +[[package]] +name = "stdlib" +version = "0.0.0" +dependencies = [ + "indexmap", + "itoa", + "num-integer", + "paste", + "rand", + "svgwriter", +] + +[[package]] +name = "strsim" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" + +[[package]] +name = "svgwriter" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "66772d99dbc6588c57cfa03566bd6ddc3f9f4f820f0ea09dc45a441f2e7b2825" +dependencies = [ + "indexmap", + "itoa", + "paste", +] + +[[package]] +name = "syn" +version = "1.0.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52205623b1b0f064a4e71182c3b18ae902267282930c6d5462c91b859668426e" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syntect" +version = "5.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6c454c27d9d7d9a84c7803aaa3c50cd088d2906fe3c6e42da3209aa623576a8" +dependencies = [ + "bincode", + "bitflags", + "flate2", + "fnv", + "lazy_static", + "once_cell", + "onig", + "regex-syntax", + "serde", + "serde_derive", + "serde_json", + "thiserror", + "walkdir", +] + +[[package]] +name = "tar" +version = "0.4.38" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b55807c0344e1e6c04d7c965f5289c39a8d94ae23ed5c0b57aabac549f871c6" +dependencies = [ + "filetime", + "libc", + "xattr", +] + +[[package]] +name = "tempfile" +version = "3.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5cdb1ef4eaeeaddc8fbd371e5017057064af0911902ef36b39801f67cc6d79e4" +dependencies = [ + "cfg-if", + "fastrand", + "libc", + "redox_syscall", + "remove_dir_all", + "winapi", +] + +[[package]] +name = "termcolor" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bab24d30b911b2376f3a13cc2cd443142f0c81dda04c118693e35b3835757755" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "terminal_size" +version = "0.1.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "633c1a546cee861a1a6d0dc69ebeca693bf4296661ba7852b9d21d159e0506df" +dependencies = [ + "libc", + "winapi", +] + +[[package]] +name = "textwrap" +version = "0.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "949517c0cf1bf4ee812e2e07e08ab448e3ae0d23472aee8a06c985f0c8815b16" + +[[package]] +name = "thiserror" +version = "1.0.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c53f98874615aea268107765aa1ed8f6116782501d18e53d08b471733bea6c85" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8b463991b4eab2d801e724172285ec4195c650e8ec79b149e6c2a8e6dd3f783" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "time" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c3f9a28b618c3a6b9251b6908e9c99e04b9e5c02e6581ccbb67d59c34ef7f9b" +dependencies = [ + "itoa", + "libc", + "num_threads", +] + +[[package]] +name = "tinyvec" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87cc5ceb3875bb20c2890005a4e226a4651264a5c75edb2421b52861a0a0cb50" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c" + +[[package]] +name = "unicase" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50f37be617794602aabbeee0be4f259dc1778fabe05e2d67ee8f79326d5cb4f6" +dependencies = [ + "version_check", +] + +[[package]] +name = "unicode-bidi" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "099b7128301d285f79ddd55b9a83d5e6b9e97c92e0ea0daebee7263e932de992" + +[[package]] +name = "unicode-ident" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dcc811dc4066ac62f84f11307873c4850cb653bfa9b1719cee2bd2204a4bc5dd" + +[[package]] +name = "unicode-normalization" +version = "0.1.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c5713f0fc4b5db668a2ac63cdb7bb4469d8c9fed047b1d0292cc7b0ce2ba921" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "unicode-width" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0edd1e5b14653f783770bce4a4dabb4a5108a5370a5f5d8cfe8710c361f6c8b" + +[[package]] +name = "url" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d68c799ae75762b8c3fe375feb6600ef5602c883c5d21eb51c09f22b83c4643" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", +] + +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + +[[package]] +name = "vergen" +version = "7.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73ba753d713ec3844652ad2cb7eb56bc71e34213a14faddac7852a10ba88f61e" +dependencies = [ + "anyhow", + "cfg-if", + "enum-iterator", + "getset", + "git2", + "rustversion", + "thiserror", + "time", +] + +[[package]] +name = "version_check" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" + +[[package]] +name = "walkdir" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "808cf2735cd4b6866113f648b791c6adc5714537bc222d9347bb203386ffda56" +dependencies = [ + "same-file", + "winapi", + "winapi-util", +] + +[[package]] +name = "wasi" +version = "0.11.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-util" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178" +dependencies = [ + "winapi", +] + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows-sys" +version = "0.36.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea04155a16a59f9eab786fe12a4a450e75cdb175f9e0d80da1e17db09f55b8d2" +dependencies = [ + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_msvc" +version = "0.36.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9bb8c3fd39ade2d67e9874ac4f3db21f0d710bee00fe7cab16949ec184eeaa47" + +[[package]] +name = "windows_i686_gnu" +version = "0.36.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "180e6ccf01daf4c426b846dfc66db1fc518f074baa793aa7d9b9aaeffad6a3b6" + +[[package]] +name = "windows_i686_msvc" +version = "0.36.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2e7917148b2812d1eeafaeb22a97e4813dfa60a3f8f78ebe204bcc88f12f024" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.36.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4dcd171b8776c41b97521e5da127a2d86ad280114807d0b2ab1e462bc764d9e1" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.36.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c811ca4a8c853ef420abd8592ba53ddbbac90410fab6903b3e79972a631f7680" + +[[package]] +name = "xattr" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d1526bbe5aaeb5eb06885f4d987bcdfa5e23187055de9b83fe00156a821fabc" +dependencies = [ + "libc", +] + +[[package]] +name = "yaml-rust" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56c1936c4cc7a1c9ab21a1ebb602eb942ba868cbd44a99cb7cdc5892335e1c85" +dependencies = [ + "linked-hash-map", +] + +[[package]] +name = "yansi" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09041cd90cf85f7f8b2df60c646f853b7f535ce68f85244eb6731cf89fa498ec" diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..91e33d6 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,44 @@ +# -*- eval: (cargo-minor-mode 1) -*- + +[workspace] +members = [".", "stdlib"] + +[package] +name = "compiler" +version = "0.0.0" +publish = false +edition = "2021" +authors = ["Dominic Meiser "] +description = "Compiler for our symbolic heap-manipulating probabilistic programming language" + +[[test]] +name = "integration" +path = "integration/main.rs" +harness = false + +[build-dependencies] +anyhow = "1" +tar = "0.4" +vergen = { version = "7", features = ["git"], default-features = false } + +[dependencies] +anyhow = "1" +ariadne = "0.1" +askama = { version = "0.11", default-features = false } +bat = { version = "0.21", features = ["regex-onig"], default-features = false } +clap = { version = "3", features = ["derive"] } +indexmap = "1" +prettyplease = "0.1" +proc-macro2 = { version = "1", features = ["span-locations"], default-features = false } +quote = { version = "1", default-features = false } +serde = { version = "1", features = ["derive"] } +syn = { version = "1", features = ["clone-impls", "extra-traits", "full", "parsing", "printing"], default-features = false } +tar = "0.4" +tempfile = "3" + +[dev-dependencies] +lazy-regex = "2" +libtest = { version = "0.4", package = "libtest-mimic" } + +[profile.release] +strip = true diff --git a/README.md b/README.md new file mode 100644 index 0000000..2ae5835 --- /dev/null +++ b/README.md @@ -0,0 +1,116 @@ +# Compiler + +This is a compiler for a heap-manipulating symbolic probabilistic programming language +that I'm designing as part of my bachelor thesis. + +## Types + +- `bool` boolean value +- `int` integer value (either 32 or 64 bit, depending on the target architecture) +- `()` empty tuple (also the default return value of functions, like `void` in C) +- `(..)` tuple +- `Rc<_>` reference-counting pointer +- `Option<_>` optional value, can be `None` or `Some(_)` +- `$ident`, `$ident<..>` custom type, must have previously been typedef'ed + +## Syntax + +``` +$program => $typedefs $functions $inputs $main $outputs + +$typedefs => $typedef $typedefs + | ε + +$functions => $function $functions + | ε + +$inputs => $input $inputs + | ε + +$main => $stmt $block + +$outputs => $output $outputs + | ε + +$typedef => type $ident = $type; + | type $ident<$identlist> = $type; + +$type => bool + | int + | () + | ($typelist) + | ($typelist,) + | Rc<$type> + | Option<$type> + | $ident + | $ident<$typelist> + +$typelist => $type | $type, $typelist + +$function => fn $ident($params) { $block } + | fn $ident($params) -> $type { $block } + | fn $ident<$identlist>($params) { $block } + | fn $ident<$identlist>($params) -> $type { $block } + +$params => $paramlist | ε + +$paramlist => $ident: $type, $paramlist + | $ident: $type + +$input => input!($ident); + +$output => output!($identlist); + +$stmt => let $ident: $type = $expr; + | $ident = $expr; + | $ifstmt + | { $block } [$float] { $block } + | while $ifcond { $block } + | { $block } + | return; + | return $expr; + | continue; + | break; + +$block => $stmt $block + | ε + +$ifstmt => if $ifcond { $block } + | if $ifcond { $block } else $ifstmt + | if $ifcond { $block } else { $block } + +$ifcond => $expr + | let Some($ident) = $expr + +$expr => $expr $op $expr + | !$expr + | -$expr + | *$expr + | ($exprs) + | ($exprlist,) + | $expr.$index + | Rc($expr) + | Some($expr) + | None + | $int + | $bool + | $ident($exprs) + | $ident<$typelist>($exprs) + +$exprs => $exprlist | ε + +$exprlist => $expr, $exprlist + | $expr + +$op => + | - | * | / + | == | != | < | <= | > | >= + | && | || + +$ident matches all valid Rust identifiers +$identlist => $ident | $ident, $identlist + +$bool => true | false +$index matches all non-negative integer values +$int matches all integer values +$float matches all floating-point values +``` diff --git a/build.rs b/build.rs new file mode 100644 index 0000000..a997c8b --- /dev/null +++ b/build.rs @@ -0,0 +1,24 @@ +use std::{env, fs::File, path::PathBuf}; +use vergen::{vergen, Config, ShaKind}; + +fn main() -> anyhow::Result<()> { + // provide version information + let mut cfg = Config::default(); + let git = cfg.git_mut(); + *git.branch_mut() = false; + *git.commit_count_mut() = true; + *git.sha_kind_mut() = ShaKind::Short; + vergen(cfg)?; + + // include the stdlib crate with the compiler + let out_dir: PathBuf = env::var("OUT_DIR")?.parse()?; + let archive_path = out_dir.join("stdlib.tar"); + println!("cargo:rustc-env=ARCHIVE_PATH={}", archive_path.display()); + let mut archive = tar::Builder::new(File::create(&archive_path)?); + let stdlib_path = "stdlib"; + println!("cargo:rerun-if-changed={stdlib_path}"); + archive.append_dir_all(stdlib_path, stdlib_path)?; + archive.finish()?; + + Ok(()) +} diff --git a/examples/dice.p3l b/examples/dice.p3l new file mode 100644 index 0000000..91ea7bb --- /dev/null +++ b/examples/dice.p3l @@ -0,0 +1,3 @@ +input!(dice); +{} +output!(dice); diff --git a/examples/quicksort.p3l b/examples/quicksort.p3l new file mode 100644 index 0000000..f032dfe --- /dev/null +++ b/examples/quicksort.p3l @@ -0,0 +1,262 @@ +type List = Option)>>; +type Partition = (List, T, List); + +// Return a random number between 0 (inclusive) and +// max (exclusive). +fn random(max: int) -> int { + while true { + let value: int = 0; + let max_value: int = 0; + while max_value < max { + max_value = max_value * 2 + 1; + value = value * 2; + { value = value + 1; } [0.5] {} + } + if value < max { + return value; + } + } +} + +// Return true if the option is `None`. +fn is_none(opt: O) -> bool { + if let Some(_value) = opt { + return false; + } else { + return true; + } +} + +// Unwrap an option. +fn unwrap(opt: Option) -> T { + if let Some(value) = opt { + return value; + } +} + +// Create a new, empty list. +// Complexity: O(1) +fn list_new() -> List { + return None; +} + +// Push a new value to the front of the list. +// Complexity: O(1) +fn list_push_front(list: List, elem: T) -> List { + return Some(Rc((elem, list))); +} + +// Pop the value from the front of the list. +// Complexity: O(1) +fn list_pop_front(list: List) -> (T, List) { + if let Some(front) = list { + return *front; + } +} + +// Append a list to the end of this one. +// Complexity: O(n) +fn list_append(list: List, list2: List) -> List { + if is_none::>(list) { + return list2; + } + + let back: List = list; + while let Some(front) = back { + let tail: List = (*front).1; + if is_none::>(tail) { + break; + } + back = tail; + } + + if let Some(back) = back { + back.1 = list2; + return list; + } + // else unreachable - back is always a non-empty list +} + +// Return the length of a list. +// Complexity: O(n) +fn list_len(list: List) -> int { + let len: int = 0; + while let Some(inner) = list { + len = len + 1; + list = (*inner).1; + } + return len; +} + +// Swap the value of the two list elements. +fn list_swap(list: List, list2: List) { + let list: Rc<(T, List)> = unwrap::)>>(list); + let list2: Rc<(T, List)> = unwrap::)>>(list2); + let tmp: T = (*list).0; + list.0 = (*list2).0; + list2.0 = tmp; +} + +// Swap the n-th and last elements, and return its value. +fn list_swap_nth_last(list: List, n: int) -> T { + let i: int = 0; + let nth: List = None; + while let Some(front) = list { + if i == n { + nth = list; + } + let tail: List = (*front).1; + if is_none::>(tail) { + let _unit: () = list_swap::(nth, list); + return (*front).0; + } + list = tail; + i = i + 1; + } +} + +// Return the next list element. +fn list_next(list: List) -> List { + return (*unwrap::)>>(list)).1; +} + +// Split the list at index n. +fn list_split(list: List, n: int) -> (List, List) { + if n == 0 { + return (None, list); + } + + let list2: List = list; + let i: int = 0; + while i < n - 1 { + list2 = list_next::(list2); + i = i + 1; + } + + if let Some(front) = list2 { + list2 = (*front).1; + front.1 = None; + } + return (list, list2); +} + +// Increase swap count +fn inc_swaps(swaps_and_comps: Rc<(int, int)>) { + swaps_and_comps.0 = (*swaps_and_comps).0 + 1; +} + +// Increase comparison count +fn inc_comps(swaps_and_comps: Rc<(int, int)>) { + swaps_and_comps.1 = (*swaps_and_comps).1 + 1; +} + +// Randomised quicksort. +// Expected complexity: O(n log n) +fn quicksort(list: List, swaps_and_comps: Rc<(int, int)>) -> List { + if list_len::(list) < 2 { + return list; + } + + let partition: Partition = partition(list, swaps_and_comps); + let list: List = quicksort(partition.0, swaps_and_comps); + let pivot: int = partition.1; + let list2: List = quicksort(partition.2, swaps_and_comps); + + list2 = list_push_front::(list2, pivot); + return list_append::(list, list2); +} + +// Partition a list into smaller, pivot, and larger elements +fn partition(list: List, swaps_and_comps: Rc<(int, int)>) -> Partition { + // choose some random element as pivot and move it to the last position + let list_len: int = list_len::(list); + let pivot: int = list_swap_nth_last::(list, random(list_len)); + let _unit: () = inc_swaps(swaps_and_comps); + + // this is our pivot position + let iter: List = list; + let iter_idx: int = -1; + + // move all elements smaller than our pivot to its left + let i: int = 0; + let iter2: List = list; + while let Some(front) = iter2 { + // don't swap out the pivot already + if i != list_len - 1 { + + let _unit: () = inc_comps(swaps_and_comps); + if (*front).0 <= pivot { + // move pivot index forward + if iter_idx >= 0 { + iter = list_next::(iter); + } + iter_idx = iter_idx + 1; + + // swap current element with pivot position + if iter_idx != i { + let _unit: () = list_swap::(iter, iter2); + let _unit: () = inc_swaps(swaps_and_comps); + } + } + + } + + iter2 = (*front).1; + i = i + 1; + } + + // move pivot to the correct pivot position + iter_idx = iter_idx + 1; + let _pivot: int = list_swap_nth_last::(list, iter_idx); + let _unit: () = inc_swaps(swaps_and_comps); + + // split the list into two + let lists: (List, List) = list_split::(list, iter_idx); + let pop: (int, List) = list_pop_front::(lists.1); + return (lists.0, pop.0, pop.1); +} + +// Assert that the list is sorted. +fn assert_sorted(list: List) -> () { + if let Some(first) = list { + if assert_sorted_impl((*first).1, (*first).0) { + return (); + } + // else there's no path to a return statement - assert failed + } else { + return (); + } +} +fn assert_sorted_impl(list: List, prev: int) -> bool { + while let Some(front) = list { + let curr: int = (*front).0; + if curr < prev { + return false; + } + prev = curr; + list = (*front).1; + } + return true; +} + +input!(len); + +// Create a random list. +let list: List = list_new::(); +let i: int = 0; +while i < len { + list = list_push_front::(list, random(len*2)); + i = i + 1; +} + +// Run quicksort. +let swaps_and_comps: Rc<(int, int)> = Rc((0, 0)); +list = quicksort(list, swaps_and_comps); +let swaps: int = (*swaps_and_comps).0; +let comps: int = (*swaps_and_comps).1; + +// Ensure the list is now sorted. +let _unit: () = assert_sorted(list); + +output!(swaps); +output!(comps); diff --git a/examples/skiplist-1-4.p3l b/examples/skiplist-1-4.p3l new file mode 100644 index 0000000..bf346e8 --- /dev/null +++ b/examples/skiplist-1-4.p3l @@ -0,0 +1,382 @@ +// 0: own level (lowest level is 0) +// 1: entry below this one (exists iff level is greater than 0) +// 2: entry to the right of this one +type SkipList = (int, SkipListLink, Option>); + +type SkipListLink = Option>>; + +// 0: own level (lowest level is 0) +// 1: entry below this one (exists iff level is greater than 0) +// 2: entry to the right of this one +// 3: key of the column this entry belongs to +// 4: value of the column (only on the lowest level) +type SkipListNode = Rc<(int, Option>, Option>, int, Option)>; + +type Stack = Rc<(Option<(T, Stack)>,)>; + +// Unwrap an option. +fn unwrap(opt: Option) -> T { + if let Some(value) = opt { + return value; + } +} + +// Create a new, empty stack +fn stack_new() -> Stack { + return Rc((None,)); +} + +// Push a new value to the stack. +fn stack_push(stack: Stack, value: T) { + stack.0 = Some((value, Rc(*stack))); +} + +// Pop a value from the stack. +fn stack_pop(stack: Stack) -> Option { + if let Some(top) = (*stack).0 { + stack.0 = (*top.1).0; + return Some(top.0); + } + return None; +} + +// Return a new, empty skiplist. +fn skiplist_new() -> SkipList { + return (0, None, None); +} + +// Return the current maximum level of the skiplist. +fn skiplist_level(list: SkipList) -> int { + return list.0; +} + +// NOT PUBLIC API +// Insert a new value into the skiplist at the first position. +fn skiplist_insert_first( + list: SkipList, + new_lvl: int, + key: int, + value: V +) -> SkipList { + // decompose list into levels + let levels: Stack>> = + stack_new::>>(); + let _unit: () = stack_push::>>(levels, list.2); + while let Some(next) = list.1 { + list = *next; + let _unit: () = stack_push::>>(levels, list.2); + } + + // create new list with just the lowest level + let lvl: int = 0; + let top: Option>> = + stack_pop::>>(levels); + let new_node: SkipListNode = Rc(( + 0, + None, + unwrap::>>(top), + key, + Some(value) + )); + list = (0, None, Some(new_node)); + + // rebuild list up to its original level + while let Some(top) = stack_pop::>>(levels) { + lvl = lvl + 1; + if new_lvl >= lvl { + new_node = Rc((lvl, Some(new_node), top, key, None)); + } else { + let top: SkipListNode = unwrap::>(top); + new_node = Rc((lvl, Some(new_node), (*top).2, (*top).3, None)); + } + list = (lvl, Some(Rc(list)), Some(new_node)); + } + + // add new levels if necessary + while new_lvl > lvl { + lvl = lvl + 1; + new_node = Rc((lvl, Some(new_node), None, key, None)); + list = (lvl, Some(Rc(list)), Some(new_node)); + } + + return list; +} + +// NOT PUBLIC API +// Insert a new value into the skiplist that needs to be inserted after +// the current node. Will be called when entering a new level while +// searching for the insert position. Returns the level of the inserted +// node. +fn skiplist_insert_impl( + node: SkipListNode, + new_lvl: int, + key: int, + value: V +) -> Option> { + // go to the last node in the level whose key is less than the new key + while true { + if let Some(next) = (*node).2 { + if (*next).3 < key { + node = next; + continue; + } + } + break; + } + + // when not at the lowest level, descend + if let Some(below) = (*node).1 { + let new_node: Option> = + skiplist_insert_impl::(below, new_lvl, key, value); + if let Some(new_node) = new_node { + let node_lvl: int = (*node).0; + if new_lvl >= node_lvl { + let new_node: SkipListNode = + Rc((node_lvl, Some(new_node), (*node).2, key, None)); + node.2 = Some(new_node); + return Some(new_node); + } + } + return new_node; + } + + // if we found a node with the exact key, just update the value + if (*node).3 == key { + node.4 = Some(value); + return None; + } + + // otherwise, we need to create a new node immediately following the + // current one + let new_node: SkipListNode = + Rc((0, None, (*node).2, key, Some(value))); + node.2 = Some(new_node); + return Some(new_node); +} + +// NOT PUBLIC API +// Insert a new value with a set level. +fn skiplist_insert_with_level( + orig_list: SkipList, + new_lvl: int, + key: int, + value: V +) -> SkipList { + // find the level at which to start search for the insert position + let list: SkipList = orig_list; + while true { + if let Some(level) = list.2 { + if (*level).3 <= key { + break; + } + } + + if let Some(below) = list.1 { + list = *below; + } else { + return skiplist_insert_first::(list, new_lvl, key, value); + } + } + + if let Some(node) = list.2 { + let new_node: Option> = + skiplist_insert_impl::(node, new_lvl, key, value); + if let Some(new_node) = new_node { + let node_lvl: int = (*node).0; + + // update levels above node_lvl if necessary + list = orig_list; + if new_lvl >= list.0 { + let new_node: SkipListNode = + Rc((list.0, None, list.2, key, None)); + list.2 = Some(new_node); + } + while let Some(below) = list.1 { + list = *below; + if list.0 <= node_lvl || list.0 < new_lvl { + break; + } + let new_node: SkipListNode = + Rc((list.0, None, list.2, key, None)); + list.2 = Some(new_node); + } + + // add levels above the list level if necessary + list = orig_list; + while new_lvl > list.0 { + new_node = Rc((list.0, Some(new_node), None, key, None)); + list = (list.0 + 1, Some(Rc(list)), Some(new_node)); + } + + return list; + } else { + return orig_list; + } + } else { + // we are inserting into an empty list + return skiplist_insert_first::(list, new_lvl, key, value); + } +} + +// NOT PUBLIC API +// Return a random level. +fn skiplist_random_level() -> int { + let lvl: int = 0; + while true { + { lvl = lvl + 1; } [0.25] { return lvl; } + } +} + +// Take the skiplist, insert a new value, and return the skiplist. +fn skiplist_insert( + list: SkipList, + key: int, + value: V +) -> SkipList { + let lvl: int = skiplist_random_level(); + return skiplist_insert_with_level::(list, lvl, key, value); +} + +// NOT PUBLIC API +// Find a value in the skiplist, starting the search at a specific node. +fn skiplist_get_impl( + node: SkipListNode, + key: int +) -> (Option, int) { + let steps: int = 1; + + if (*node).3 < key { + while true { + if let Some(next) = (*node).2 { + let next_key: int = (*next).3; + steps = steps + 1; + if next_key <= key { + node = next; + if next_key == key { + break; + } else { + continue; + } + } + } + + if let Some(below) = (*node).1 { + node = below; + continue; + } + + break; + } + } + + if (*node).3 != key { + return (None, steps); + } + + // the value is only stored at the lowest level + while let Some(below) = (*node).1 { + node = below; + } + return (Some(unwrap::((*node).4)), steps); +} + +// Find a value in the skiplist. +fn skiplist_get(list: SkipList, key: int) -> (Option, int) { + let steps: int = 0; + + while true { + if let Some(node) = list.2 { + steps = steps + 1; + if (*node).3 <= key { + let tmp: (Option, int) = skiplist_get_impl::(node, key); + return (tmp.0, steps + tmp.1); + } + } + + if let Some(below) = list.1 { + list = *below; + continue; + } + + return (None, steps); + } +} + +// Assert that the skiplist contains a certain key. +fn assert_contains(list: SkipList, key: int) -> () { + if let Some(_value) = skiplist_get::(list, key).0 { + return (); + } +} + +// Return the length of the skiplist. +fn skiplist_len(list: SkipList) -> int { + // descend to the lowest level + while let Some(next) = list.1 { + list = *next; + } + + // count the lowest level + let node: Option> = list.2; + let len: int = 0; + while let Some(next) = node { + len = len + 1; + node = (*next).2; + } + return len; +} + +// Assert that the skiplist has the correct length. +fn assert_len(list: SkipList, len: int) -> () { + if skiplist_len::(list) == len { + return (); + } +} + +// Assert that a value is in a certain range. +fn assert_in_range(value: int, min_incl: int, max_excl: int) -> () { + if value >= min_incl && value < max_excl { + return (); + } +} + +// Return a random number between 0 (inclusive) and +// max (exclusive). +fn random(max: int) -> int { + while true { + let value: int = 0; + let max_value: int = 0; + while max_value < max { + max_value = max_value * 2 + 1; + value = value * 2; + { value = value + 1; } [0.25] {} + } + if value < max { + return value; + } + } +} + +input!(len); + +// create a new list with entries 0..len +let list: SkipList<()> = skiplist_new::<()>(); +let i: int = 0; +while i < len { + list = skiplist_insert::<()>(list, i, ()); + let _unit: () = assert_contains::<()>(list, i); + i = i + 1; +} +let _unit: () = assert_len::<()>(list, len); + +// lookup a random value in the list +let key: int = random(len); +let _unit: () = assert_in_range(key, 0, len); +let lookup: (Option<()>, int) = skiplist_get::<()>(list, key); +let _unit: () = unwrap::<()>(lookup.0); + +let steps: int = lookup.1; +let lvl: int = skiplist_level::<()>(list); + +output!(len); +output!(lvl, steps); diff --git a/examples/skiplist-3-8.p3l b/examples/skiplist-3-8.p3l new file mode 100644 index 0000000..111aaf4 --- /dev/null +++ b/examples/skiplist-3-8.p3l @@ -0,0 +1,382 @@ +// 0: own level (lowest level is 0) +// 1: entry below this one (exists iff level is greater than 0) +// 2: entry to the right of this one +type SkipList = (int, SkipListLink, Option>); + +type SkipListLink = Option>>; + +// 0: own level (lowest level is 0) +// 1: entry below this one (exists iff level is greater than 0) +// 2: entry to the right of this one +// 3: key of the column this entry belongs to +// 4: value of the column (only on the lowest level) +type SkipListNode = Rc<(int, Option>, Option>, int, Option)>; + +type Stack = Rc<(Option<(T, Stack)>,)>; + +// Unwrap an option. +fn unwrap(opt: Option) -> T { + if let Some(value) = opt { + return value; + } +} + +// Create a new, empty stack +fn stack_new() -> Stack { + return Rc((None,)); +} + +// Push a new value to the stack. +fn stack_push(stack: Stack, value: T) { + stack.0 = Some((value, Rc(*stack))); +} + +// Pop a value from the stack. +fn stack_pop(stack: Stack) -> Option { + if let Some(top) = (*stack).0 { + stack.0 = (*top.1).0; + return Some(top.0); + } + return None; +} + +// Return a new, empty skiplist. +fn skiplist_new() -> SkipList { + return (0, None, None); +} + +// Return the current maximum level of the skiplist. +fn skiplist_level(list: SkipList) -> int { + return list.0; +} + +// NOT PUBLIC API +// Insert a new value into the skiplist at the first position. +fn skiplist_insert_first( + list: SkipList, + new_lvl: int, + key: int, + value: V +) -> SkipList { + // decompose list into levels + let levels: Stack>> = + stack_new::>>(); + let _unit: () = stack_push::>>(levels, list.2); + while let Some(next) = list.1 { + list = *next; + let _unit: () = stack_push::>>(levels, list.2); + } + + // create new list with just the lowest level + let lvl: int = 0; + let top: Option>> = + stack_pop::>>(levels); + let new_node: SkipListNode = Rc(( + 0, + None, + unwrap::>>(top), + key, + Some(value) + )); + list = (0, None, Some(new_node)); + + // rebuild list up to its original level + while let Some(top) = stack_pop::>>(levels) { + lvl = lvl + 1; + if new_lvl >= lvl { + new_node = Rc((lvl, Some(new_node), top, key, None)); + } else { + let top: SkipListNode = unwrap::>(top); + new_node = Rc((lvl, Some(new_node), (*top).2, (*top).3, None)); + } + list = (lvl, Some(Rc(list)), Some(new_node)); + } + + // add new levels if necessary + while new_lvl > lvl { + lvl = lvl + 1; + new_node = Rc((lvl, Some(new_node), None, key, None)); + list = (lvl, Some(Rc(list)), Some(new_node)); + } + + return list; +} + +// NOT PUBLIC API +// Insert a new value into the skiplist that needs to be inserted after +// the current node. Will be called when entering a new level while +// searching for the insert position. Returns the level of the inserted +// node. +fn skiplist_insert_impl( + node: SkipListNode, + new_lvl: int, + key: int, + value: V +) -> Option> { + // go to the last node in the level whose key is less than the new key + while true { + if let Some(next) = (*node).2 { + if (*next).3 < key { + node = next; + continue; + } + } + break; + } + + // when not at the lowest level, descend + if let Some(below) = (*node).1 { + let new_node: Option> = + skiplist_insert_impl::(below, new_lvl, key, value); + if let Some(new_node) = new_node { + let node_lvl: int = (*node).0; + if new_lvl >= node_lvl { + let new_node: SkipListNode = + Rc((node_lvl, Some(new_node), (*node).2, key, None)); + node.2 = Some(new_node); + return Some(new_node); + } + } + return new_node; + } + + // if we found a node with the exact key, just update the value + if (*node).3 == key { + node.4 = Some(value); + return None; + } + + // otherwise, we need to create a new node immediately following the + // current one + let new_node: SkipListNode = + Rc((0, None, (*node).2, key, Some(value))); + node.2 = Some(new_node); + return Some(new_node); +} + +// NOT PUBLIC API +// Insert a new value with a set level. +fn skiplist_insert_with_level( + orig_list: SkipList, + new_lvl: int, + key: int, + value: V +) -> SkipList { + // find the level at which to start search for the insert position + let list: SkipList = orig_list; + while true { + if let Some(level) = list.2 { + if (*level).3 <= key { + break; + } + } + + if let Some(below) = list.1 { + list = *below; + } else { + return skiplist_insert_first::(list, new_lvl, key, value); + } + } + + if let Some(node) = list.2 { + let new_node: Option> = + skiplist_insert_impl::(node, new_lvl, key, value); + if let Some(new_node) = new_node { + let node_lvl: int = (*node).0; + + // update levels above node_lvl if necessary + list = orig_list; + if new_lvl >= list.0 { + let new_node: SkipListNode = + Rc((list.0, None, list.2, key, None)); + list.2 = Some(new_node); + } + while let Some(below) = list.1 { + list = *below; + if list.0 <= node_lvl || list.0 < new_lvl { + break; + } + let new_node: SkipListNode = + Rc((list.0, None, list.2, key, None)); + list.2 = Some(new_node); + } + + // add levels above the list level if necessary + list = orig_list; + while new_lvl > list.0 { + new_node = Rc((list.0, Some(new_node), None, key, None)); + list = (list.0 + 1, Some(Rc(list)), Some(new_node)); + } + + return list; + } else { + return orig_list; + } + } else { + // we are inserting into an empty list + return skiplist_insert_first::(list, new_lvl, key, value); + } +} + +// NOT PUBLIC API +// Return a random level. +fn skiplist_random_level() -> int { + let lvl: int = 0; + while true { + { lvl = lvl + 1; } [0.375] { return lvl; } + } +} + +// Take the skiplist, insert a new value, and return the skiplist. +fn skiplist_insert( + list: SkipList, + key: int, + value: V +) -> SkipList { + let lvl: int = skiplist_random_level(); + return skiplist_insert_with_level::(list, lvl, key, value); +} + +// NOT PUBLIC API +// Find a value in the skiplist, starting the search at a specific node. +fn skiplist_get_impl( + node: SkipListNode, + key: int +) -> (Option, int) { + let steps: int = 1; + + if (*node).3 < key { + while true { + if let Some(next) = (*node).2 { + let next_key: int = (*next).3; + steps = steps + 1; + if next_key <= key { + node = next; + if next_key == key { + break; + } else { + continue; + } + } + } + + if let Some(below) = (*node).1 { + node = below; + continue; + } + + break; + } + } + + if (*node).3 != key { + return (None, steps); + } + + // the value is only stored at the lowest level + while let Some(below) = (*node).1 { + node = below; + } + return (Some(unwrap::((*node).4)), steps); +} + +// Find a value in the skiplist. +fn skiplist_get(list: SkipList, key: int) -> (Option, int) { + let steps: int = 0; + + while true { + if let Some(node) = list.2 { + steps = steps + 1; + if (*node).3 <= key { + let tmp: (Option, int) = skiplist_get_impl::(node, key); + return (tmp.0, steps + tmp.1); + } + } + + if let Some(below) = list.1 { + list = *below; + continue; + } + + return (None, steps); + } +} + +// Assert that the skiplist contains a certain key. +fn assert_contains(list: SkipList, key: int) -> () { + if let Some(_value) = skiplist_get::(list, key).0 { + return (); + } +} + +// Return the length of the skiplist. +fn skiplist_len(list: SkipList) -> int { + // descend to the lowest level + while let Some(next) = list.1 { + list = *next; + } + + // count the lowest level + let node: Option> = list.2; + let len: int = 0; + while let Some(next) = node { + len = len + 1; + node = (*next).2; + } + return len; +} + +// Assert that the skiplist has the correct length. +fn assert_len(list: SkipList, len: int) -> () { + if skiplist_len::(list) == len { + return (); + } +} + +// Assert that a value is in a certain range. +fn assert_in_range(value: int, min_incl: int, max_excl: int) -> () { + if value >= min_incl && value < max_excl { + return (); + } +} + +// Return a random number between 0 (inclusive) and +// max (exclusive). +fn random(max: int) -> int { + while true { + let value: int = 0; + let max_value: int = 0; + while max_value < max { + max_value = max_value * 2 + 1; + value = value * 2; + { value = value + 1; } [0.375] {} + } + if value < max { + return value; + } + } +} + +input!(len); + +// create a new list with entries 0..len +let list: SkipList<()> = skiplist_new::<()>(); +let i: int = 0; +while i < len { + list = skiplist_insert::<()>(list, i, ()); + let _unit: () = assert_contains::<()>(list, i); + i = i + 1; +} +let _unit: () = assert_len::<()>(list, len); + +// lookup a random value in the list +let key: int = random(len); +let _unit: () = assert_in_range(key, 0, len); +let lookup: (Option<()>, int) = skiplist_get::<()>(list, key); +let _unit: () = unwrap::<()>(lookup.0); + +let steps: int = lookup.1; +let lvl: int = skiplist_level::<()>(list); + +output!(len); +output!(lvl, steps); diff --git a/examples/skiplist.p3l b/examples/skiplist.p3l new file mode 100644 index 0000000..586bb14 --- /dev/null +++ b/examples/skiplist.p3l @@ -0,0 +1,382 @@ +// 0: own level (lowest level is 0) +// 1: entry below this one (exists iff level is greater than 0) +// 2: entry to the right of this one +type SkipList = (int, SkipListLink, Option>); + +type SkipListLink = Option>>; + +// 0: own level (lowest level is 0) +// 1: entry below this one (exists iff level is greater than 0) +// 2: entry to the right of this one +// 3: key of the column this entry belongs to +// 4: value of the column (only on the lowest level) +type SkipListNode = Rc<(int, Option>, Option>, int, Option)>; + +type Stack = Rc<(Option<(T, Stack)>,)>; + +// Unwrap an option. +fn unwrap(opt: Option) -> T { + if let Some(value) = opt { + return value; + } +} + +// Create a new, empty stack +fn stack_new() -> Stack { + return Rc((None,)); +} + +// Push a new value to the stack. +fn stack_push(stack: Stack, value: T) { + stack.0 = Some((value, Rc(*stack))); +} + +// Pop a value from the stack. +fn stack_pop(stack: Stack) -> Option { + if let Some(top) = (*stack).0 { + stack.0 = (*top.1).0; + return Some(top.0); + } + return None; +} + +// Return a new, empty skiplist. +fn skiplist_new() -> SkipList { + return (0, None, None); +} + +// Return the current maximum level of the skiplist. +fn skiplist_level(list: SkipList) -> int { + return list.0; +} + +// NOT PUBLIC API +// Insert a new value into the skiplist at the first position. +fn skiplist_insert_first( + list: SkipList, + new_lvl: int, + key: int, + value: V +) -> SkipList { + // decompose list into levels + let levels: Stack>> = + stack_new::>>(); + let _unit: () = stack_push::>>(levels, list.2); + while let Some(next) = list.1 { + list = *next; + let _unit: () = stack_push::>>(levels, list.2); + } + + // create new list with just the lowest level + let lvl: int = 0; + let top: Option>> = + stack_pop::>>(levels); + let new_node: SkipListNode = Rc(( + 0, + None, + unwrap::>>(top), + key, + Some(value) + )); + list = (0, None, Some(new_node)); + + // rebuild list up to its original level + while let Some(top) = stack_pop::>>(levels) { + lvl = lvl + 1; + if new_lvl >= lvl { + new_node = Rc((lvl, Some(new_node), top, key, None)); + } else { + let top: SkipListNode = unwrap::>(top); + new_node = Rc((lvl, Some(new_node), (*top).2, (*top).3, None)); + } + list = (lvl, Some(Rc(list)), Some(new_node)); + } + + // add new levels if necessary + while new_lvl > lvl { + lvl = lvl + 1; + new_node = Rc((lvl, Some(new_node), None, key, None)); + list = (lvl, Some(Rc(list)), Some(new_node)); + } + + return list; +} + +// NOT PUBLIC API +// Insert a new value into the skiplist that needs to be inserted after +// the current node. Will be called when entering a new level while +// searching for the insert position. Returns the level of the inserted +// node. +fn skiplist_insert_impl( + node: SkipListNode, + new_lvl: int, + key: int, + value: V +) -> Option> { + // go to the last node in the level whose key is less than the new key + while true { + if let Some(next) = (*node).2 { + if (*next).3 < key { + node = next; + continue; + } + } + break; + } + + // when not at the lowest level, descend + if let Some(below) = (*node).1 { + let new_node: Option> = + skiplist_insert_impl::(below, new_lvl, key, value); + if let Some(new_node) = new_node { + let node_lvl: int = (*node).0; + if new_lvl >= node_lvl { + let new_node: SkipListNode = + Rc((node_lvl, Some(new_node), (*node).2, key, None)); + node.2 = Some(new_node); + return Some(new_node); + } + } + return new_node; + } + + // if we found a node with the exact key, just update the value + if (*node).3 == key { + node.4 = Some(value); + return None; + } + + // otherwise, we need to create a new node immediately following the + // current one + let new_node: SkipListNode = + Rc((0, None, (*node).2, key, Some(value))); + node.2 = Some(new_node); + return Some(new_node); +} + +// NOT PUBLIC API +// Insert a new value with a set level. +fn skiplist_insert_with_level( + orig_list: SkipList, + new_lvl: int, + key: int, + value: V +) -> SkipList { + // find the level at which to start search for the insert position + let list: SkipList = orig_list; + while true { + if let Some(level) = list.2 { + if (*level).3 <= key { + break; + } + } + + if let Some(below) = list.1 { + list = *below; + } else { + return skiplist_insert_first::(list, new_lvl, key, value); + } + } + + if let Some(node) = list.2 { + let new_node: Option> = + skiplist_insert_impl::(node, new_lvl, key, value); + if let Some(new_node) = new_node { + let node_lvl: int = (*node).0; + + // update levels above node_lvl if necessary + list = orig_list; + if new_lvl >= list.0 { + let new_node: SkipListNode = + Rc((list.0, None, list.2, key, None)); + list.2 = Some(new_node); + } + while let Some(below) = list.1 { + list = *below; + if list.0 <= node_lvl || list.0 < new_lvl { + break; + } + let new_node: SkipListNode = + Rc((list.0, None, list.2, key, None)); + list.2 = Some(new_node); + } + + // add levels above the list level if necessary + list = orig_list; + while new_lvl > list.0 { + new_node = Rc((list.0, Some(new_node), None, key, None)); + list = (list.0 + 1, Some(Rc(list)), Some(new_node)); + } + + return list; + } else { + return orig_list; + } + } else { + // we are inserting into an empty list + return skiplist_insert_first::(list, new_lvl, key, value); + } +} + +// NOT PUBLIC API +// Return a random level. +fn skiplist_random_level() -> int { + let lvl: int = 0; + while true { + { lvl = lvl + 1; } [0.5] { return lvl; } + } +} + +// Take the skiplist, insert a new value, and return the skiplist. +fn skiplist_insert( + list: SkipList, + key: int, + value: V +) -> SkipList { + let lvl: int = skiplist_random_level(); + return skiplist_insert_with_level::(list, lvl, key, value); +} + +// NOT PUBLIC API +// Find a value in the skiplist, starting the search at a specific node. +fn skiplist_get_impl( + node: SkipListNode, + key: int +) -> (Option, int) { + let steps: int = 1; + + if (*node).3 < key { + while true { + if let Some(next) = (*node).2 { + let next_key: int = (*next).3; + steps = steps + 1; + if next_key <= key { + node = next; + if next_key == key { + break; + } else { + continue; + } + } + } + + if let Some(below) = (*node).1 { + node = below; + continue; + } + + break; + } + } + + if (*node).3 != key { + return (None, steps); + } + + // the value is only stored at the lowest level + while let Some(below) = (*node).1 { + node = below; + } + return (Some(unwrap::((*node).4)), steps); +} + +// Find a value in the skiplist. +fn skiplist_get(list: SkipList, key: int) -> (Option, int) { + let steps: int = 0; + + while true { + if let Some(node) = list.2 { + steps = steps + 1; + if (*node).3 <= key { + let tmp: (Option, int) = skiplist_get_impl::(node, key); + return (tmp.0, steps + tmp.1); + } + } + + if let Some(below) = list.1 { + list = *below; + continue; + } + + return (None, steps); + } +} + +// Assert that the skiplist contains a certain key. +fn assert_contains(list: SkipList, key: int) -> () { + if let Some(_value) = skiplist_get::(list, key).0 { + return (); + } +} + +// Return the length of the skiplist. +fn skiplist_len(list: SkipList) -> int { + // descend to the lowest level + while let Some(next) = list.1 { + list = *next; + } + + // count the lowest level + let node: Option> = list.2; + let len: int = 0; + while let Some(next) = node { + len = len + 1; + node = (*next).2; + } + return len; +} + +// Assert that the skiplist has the correct length. +fn assert_len(list: SkipList, len: int) -> () { + if skiplist_len::(list) == len { + return (); + } +} + +// Assert that a value is in a certain range. +fn assert_in_range(value: int, min_incl: int, max_excl: int) -> () { + if value >= min_incl && value < max_excl { + return (); + } +} + +// Return a random number between 0 (inclusive) and +// max (exclusive). +fn random(max: int) -> int { + while true { + let value: int = 0; + let max_value: int = 0; + while max_value < max { + max_value = max_value * 2 + 1; + value = value * 2; + { value = value + 1; } [0.5] {} + } + if value < max { + return value; + } + } +} + +input!(len); + +// create a new list with entries 0..len +let list: SkipList<()> = skiplist_new::<()>(); +let i: int = 0; +while i < len { + list = skiplist_insert::<()>(list, i, ()); + let _unit: () = assert_contains::<()>(list, i); + i = i + 1; +} +let _unit: () = assert_len::<()>(list, len); + +// lookup a random value in the list +let key: int = random(len); +let _unit: () = assert_in_range(key, 0, len); +let lookup: (Option<()>, int) = skiplist_get::<()>(list, key); +let _unit: () = unwrap::<()>(lookup.0); + +let steps: int = lookup.1; +let lvl: int = skiplist_level::<()>(list); + +output!(len); +output!(lvl, steps); diff --git a/integration/fail/no-such-field/assignment-bool-field.out b/integration/fail/no-such-field/assignment-bool-field.out new file mode 100644 index 0000000..5665312 --- /dev/null +++ b/integration/fail/no-such-field/assignment-bool-field.out @@ -0,0 +1,7 @@ +Error: No such field + ╭─[assignment-bool-field.txt:2:5] + │ + 2 │ foo.bar = true; + · ─┬─ + · ╰─── bool is a primitive type and does not have any fields +───╯ diff --git a/integration/fail/no-such-field/assignment-bool-field.txt b/integration/fail/no-such-field/assignment-bool-field.txt new file mode 100644 index 0000000..165d40a --- /dev/null +++ b/integration/fail/no-such-field/assignment-bool-field.txt @@ -0,0 +1,2 @@ +let foo: bool = true; +foo.bar = true; \ No newline at end of file diff --git a/integration/fail/no-such-field/assignment-bool-index.out b/integration/fail/no-such-field/assignment-bool-index.out new file mode 100644 index 0000000..9130c82 --- /dev/null +++ b/integration/fail/no-such-field/assignment-bool-index.out @@ -0,0 +1,7 @@ +Error: No such field + ╭─[assignment-bool-index.txt:2:5] + │ + 2 │ foo.0 = true; + · ┬ + · ╰── bool is a primitive type and does not have any fields +───╯ diff --git a/integration/fail/no-such-field/assignment-bool-index.txt b/integration/fail/no-such-field/assignment-bool-index.txt new file mode 100644 index 0000000..f650b2b --- /dev/null +++ b/integration/fail/no-such-field/assignment-bool-index.txt @@ -0,0 +1,2 @@ +let foo: bool = true; +foo.0 = true; \ No newline at end of file diff --git a/integration/fail/no-such-field/assignment-int-field.out b/integration/fail/no-such-field/assignment-int-field.out new file mode 100644 index 0000000..e620ddf --- /dev/null +++ b/integration/fail/no-such-field/assignment-int-field.out @@ -0,0 +1,7 @@ +Error: No such field + ╭─[assignment-int-field.txt:2:5] + │ + 2 │ foo.bar = 1; + · ─┬─ + · ╰─── int is a primitive type and does not have any fields +───╯ diff --git a/integration/fail/no-such-field/assignment-int-field.txt b/integration/fail/no-such-field/assignment-int-field.txt new file mode 100644 index 0000000..10ad09e --- /dev/null +++ b/integration/fail/no-such-field/assignment-int-field.txt @@ -0,0 +1,2 @@ +let foo: int = 1; +foo.bar = 1; \ No newline at end of file diff --git a/integration/fail/no-such-field/assignment-int-index.out b/integration/fail/no-such-field/assignment-int-index.out new file mode 100644 index 0000000..578da72 --- /dev/null +++ b/integration/fail/no-such-field/assignment-int-index.out @@ -0,0 +1,7 @@ +Error: No such field + ╭─[assignment-int-index.txt:2:5] + │ + 2 │ foo.0 = 1; + · ┬ + · ╰── int is a primitive type and does not have any fields +───╯ diff --git a/integration/fail/no-such-field/assignment-int-index.txt b/integration/fail/no-such-field/assignment-int-index.txt new file mode 100644 index 0000000..b4352ca --- /dev/null +++ b/integration/fail/no-such-field/assignment-int-index.txt @@ -0,0 +1,2 @@ +let foo: int = 1; +foo.0 = 1; \ No newline at end of file diff --git a/integration/fail/no-such-field/assignment-tuple-field.out b/integration/fail/no-such-field/assignment-tuple-field.out new file mode 100644 index 0000000..864b9d6 --- /dev/null +++ b/integration/fail/no-such-field/assignment-tuple-field.out @@ -0,0 +1,9 @@ +Error: No such field + ╭─[assignment-tuple-field.txt:2:5] + │ + 2 │ foo.first = 0; + · ──┬── + · ╰──── tuples don't have named fields + · + · Help: To access the first element in a tuple, use `.0` +───╯ diff --git a/integration/fail/no-such-field/assignment-tuple-field.txt b/integration/fail/no-such-field/assignment-tuple-field.txt new file mode 100644 index 0000000..9a9ab50 --- /dev/null +++ b/integration/fail/no-such-field/assignment-tuple-field.txt @@ -0,0 +1,2 @@ +let foo: (int,) = (1,); +foo.first = 0; \ No newline at end of file diff --git a/integration/fail/no-such-field/assignment-tuple-out-of-bounds.out b/integration/fail/no-such-field/assignment-tuple-out-of-bounds.out new file mode 100644 index 0000000..207be0f --- /dev/null +++ b/integration/fail/no-such-field/assignment-tuple-out-of-bounds.out @@ -0,0 +1,9 @@ +Error: No such field + ╭─[assignment-tuple-out-of-bounds.txt:2:5] + │ + 2 │ foo.2 = 0; + · ┬ + · ╰── this tuple does not have enough elements + · + · Note: The first element in a tuple has index 0 +───╯ diff --git a/integration/fail/no-such-field/assignment-tuple-out-of-bounds.txt b/integration/fail/no-such-field/assignment-tuple-out-of-bounds.txt new file mode 100644 index 0000000..077d6bc --- /dev/null +++ b/integration/fail/no-such-field/assignment-tuple-out-of-bounds.txt @@ -0,0 +1,2 @@ +let foo: (int,) = (1,); +foo.2 = 0; \ No newline at end of file diff --git a/integration/fail/too-xxx-arguments/expected-2-found-1-generic.out b/integration/fail/too-xxx-arguments/expected-2-found-1-generic.out new file mode 100644 index 0000000..7e3ebae --- /dev/null +++ b/integration/fail/too-xxx-arguments/expected-2-found-1-generic.out @@ -0,0 +1,11 @@ +Error: Too few arguments + ╭─[expected-2-found-1-generic.txt:4:17] + │ + 2 │ fn foo(foo: V, bar: V) {} + · ─┬─ + · ╰─── This function takes 2 arguments, + · + 4 │ let _unit: () = foo::(1); + · ─┬─ + · ╰─── but only 1 arguments were supplied +───╯ diff --git a/integration/fail/too-xxx-arguments/expected-2-found-1-generic.txt b/integration/fail/too-xxx-arguments/expected-2-found-1-generic.txt new file mode 100644 index 0000000..ac48e5c --- /dev/null +++ b/integration/fail/too-xxx-arguments/expected-2-found-1-generic.txt @@ -0,0 +1,4 @@ +// This function does nothing. +fn foo(foo: V, bar: V) {} + +let _unit: () = foo::(1); diff --git a/integration/fail/too-xxx-arguments/expected-2-found-1.out b/integration/fail/too-xxx-arguments/expected-2-found-1.out new file mode 100644 index 0000000..047c29e --- /dev/null +++ b/integration/fail/too-xxx-arguments/expected-2-found-1.out @@ -0,0 +1,11 @@ +Error: Too few arguments + ╭─[expected-2-found-1.txt:4:17] + │ + 2 │ fn foo(foo: int, bar: int) {} + · ─┬─ + · ╰─── This function takes 2 arguments, + · + 4 │ let _unit: () = foo(1); + · ─┬─ + · ╰─── but only 1 arguments were supplied +───╯ diff --git a/integration/fail/too-xxx-arguments/expected-2-found-1.txt b/integration/fail/too-xxx-arguments/expected-2-found-1.txt new file mode 100644 index 0000000..709d38f --- /dev/null +++ b/integration/fail/too-xxx-arguments/expected-2-found-1.txt @@ -0,0 +1,4 @@ +// This function does nothing. +fn foo(foo: int, bar: int) {} + +let _unit: () = foo(1); diff --git a/integration/fail/type-mismatch/assignment-bool-int.out b/integration/fail/type-mismatch/assignment-bool-int.out new file mode 100644 index 0000000..c50a716 --- /dev/null +++ b/integration/fail/type-mismatch/assignment-bool-int.out @@ -0,0 +1,10 @@ +Error: Type Mismatch + ╭─[assignment-bool-int.txt:2:7] + │ + 1 │ let foo: bool = true; + · ──┬─ + · ╰─── Expected type `bool`, + 2 │ foo = 0; + · ┬ + · ╰── but expression is of type `int` +───╯ diff --git a/integration/fail/type-mismatch/assignment-bool-int.txt b/integration/fail/type-mismatch/assignment-bool-int.txt new file mode 100644 index 0000000..7418e86 --- /dev/null +++ b/integration/fail/type-mismatch/assignment-bool-int.txt @@ -0,0 +1,2 @@ +let foo: bool = true; +foo = 0; \ No newline at end of file diff --git a/integration/fail/type-mismatch/assignment-rc-tuple-bool-int.out b/integration/fail/type-mismatch/assignment-rc-tuple-bool-int.out new file mode 100644 index 0000000..b82f30c --- /dev/null +++ b/integration/fail/type-mismatch/assignment-rc-tuple-bool-int.out @@ -0,0 +1,10 @@ +Error: Type Mismatch + ╭─[assignment-rc-tuple-bool-int.txt:2:9] + │ + 1 │ let foo: Rc<(bool,)> = Rc((true,)); + · ──┬─ + · ╰─── Expected type `bool`, + 2 │ foo.0 = 0; + · ┬ + · ╰── but expression is of type `int` +───╯ diff --git a/integration/fail/type-mismatch/assignment-rc-tuple-bool-int.txt b/integration/fail/type-mismatch/assignment-rc-tuple-bool-int.txt new file mode 100644 index 0000000..e97c1c1 --- /dev/null +++ b/integration/fail/type-mismatch/assignment-rc-tuple-bool-int.txt @@ -0,0 +1,2 @@ +let foo: Rc<(bool,)> = Rc((true,)); +foo.0 = 0; diff --git a/integration/fail/type-mismatch/assignment-rc-tuple-option-rc.out b/integration/fail/type-mismatch/assignment-rc-tuple-option-rc.out new file mode 100644 index 0000000..50ae56d --- /dev/null +++ b/integration/fail/type-mismatch/assignment-rc-tuple-option-rc.out @@ -0,0 +1,10 @@ +Error: Type Mismatch + ╭─[assignment-rc-tuple-option-rc.txt:2:9] + │ + 1 │ let foo: Rc<(Option,)> = Rc((None,)); + · ───┬── + · ╰──── Expected type `Option`, + 2 │ foo.0 = Rc(true); + · ─┬ + · ╰── but expression is of type `Rc<_>` +───╯ diff --git a/integration/fail/type-mismatch/assignment-rc-tuple-option-rc.txt b/integration/fail/type-mismatch/assignment-rc-tuple-option-rc.txt new file mode 100644 index 0000000..77c8dcd --- /dev/null +++ b/integration/fail/type-mismatch/assignment-rc-tuple-option-rc.txt @@ -0,0 +1,2 @@ +let foo: Rc<(Option,)> = Rc((None,)); +foo.0 = Rc(true); diff --git a/integration/fail/type-mismatch/declaration-bool-int-arithmetic.out b/integration/fail/type-mismatch/declaration-bool-int-arithmetic.out new file mode 100644 index 0000000..4b7fe24 --- /dev/null +++ b/integration/fail/type-mismatch/declaration-bool-int-arithmetic.out @@ -0,0 +1,9 @@ +Error: Type Mismatch + ╭─[declaration-bool-int-arithmetic.txt:1:17] + │ + 1 │ let foo: bool = 1 + 2; + · ──┬─ ──┬── + · ╰─────────── Expected type `bool`, + · │ + · ╰──── but expression is of type `int` +───╯ diff --git a/integration/fail/type-mismatch/declaration-bool-int-arithmetic.txt b/integration/fail/type-mismatch/declaration-bool-int-arithmetic.txt new file mode 100644 index 0000000..57372ee --- /dev/null +++ b/integration/fail/type-mismatch/declaration-bool-int-arithmetic.txt @@ -0,0 +1 @@ +let foo: bool = 1 + 2; \ No newline at end of file diff --git a/integration/fail/type-mismatch/declaration-bool-int-negation.out b/integration/fail/type-mismatch/declaration-bool-int-negation.out new file mode 100644 index 0000000..b264a3e --- /dev/null +++ b/integration/fail/type-mismatch/declaration-bool-int-negation.out @@ -0,0 +1,9 @@ +Error: Type Mismatch + ╭─[declaration-bool-int-negation.txt:2:17] + │ + 2 │ let foo: bool = -one; + · ──┬─ ┬ + · ╰─────── Expected type `bool`, + · │ + · ╰── but expression is of type `int` +───╯ diff --git a/integration/fail/type-mismatch/declaration-bool-int-negation.txt b/integration/fail/type-mismatch/declaration-bool-int-negation.txt new file mode 100644 index 0000000..92f9509 --- /dev/null +++ b/integration/fail/type-mismatch/declaration-bool-int-negation.txt @@ -0,0 +1,2 @@ +let one: int = 1; +let foo: bool = -one; \ No newline at end of file diff --git a/integration/fail/type-mismatch/declaration-bool-int-tuple-bool-bool-tuple.out b/integration/fail/type-mismatch/declaration-bool-int-tuple-bool-bool-tuple.out new file mode 100644 index 0000000..561998e --- /dev/null +++ b/integration/fail/type-mismatch/declaration-bool-int-tuple-bool-bool-tuple.out @@ -0,0 +1,9 @@ +Error: Type Mismatch + ╭─[declaration-bool-int-tuple-bool-bool-tuple.txt:1:31] + │ + 1 │ let foo: (bool, int) = (true, true); + · ─┬─ ──┬─ + · ╰────────────────── Expected type `int`, + · │ + · ╰─── but expression is of type `bool` +───╯ diff --git a/integration/fail/type-mismatch/declaration-bool-int-tuple-bool-bool-tuple.txt b/integration/fail/type-mismatch/declaration-bool-int-tuple-bool-bool-tuple.txt new file mode 100644 index 0000000..eb08285 --- /dev/null +++ b/integration/fail/type-mismatch/declaration-bool-int-tuple-bool-bool-tuple.txt @@ -0,0 +1 @@ +let foo: (bool, int) = (true, true); \ No newline at end of file diff --git a/integration/fail/type-mismatch/declaration-bool-int.out b/integration/fail/type-mismatch/declaration-bool-int.out new file mode 100644 index 0000000..b9f1214 --- /dev/null +++ b/integration/fail/type-mismatch/declaration-bool-int.out @@ -0,0 +1,9 @@ +Error: Type Mismatch + ╭─[declaration-bool-int.txt:1:17] + │ + 1 │ let foo: bool = 0; + · ──┬─ ┬ + · ╰─────── Expected type `bool`, + · │ + · ╰── but expression is of type `int` +───╯ diff --git a/integration/fail/type-mismatch/declaration-bool-int.txt b/integration/fail/type-mismatch/declaration-bool-int.txt new file mode 100644 index 0000000..ad030d9 --- /dev/null +++ b/integration/fail/type-mismatch/declaration-bool-int.txt @@ -0,0 +1 @@ +let foo: bool = 0; \ No newline at end of file diff --git a/integration/fail/type-mismatch/declaration-int-bool-conjunction.out b/integration/fail/type-mismatch/declaration-int-bool-conjunction.out new file mode 100644 index 0000000..5f36ff9 --- /dev/null +++ b/integration/fail/type-mismatch/declaration-int-bool-conjunction.out @@ -0,0 +1,9 @@ +Error: Type Mismatch + ╭─[declaration-int-bool-conjunction.txt:1:16] + │ + 1 │ let foo: int = true && true; + · ─┬─ ──────┬───── + · ╰────────────────── Expected type `int`, + · │ + · ╰─────── but expression is of type `bool` +───╯ diff --git a/integration/fail/type-mismatch/declaration-int-bool-conjunction.txt b/integration/fail/type-mismatch/declaration-int-bool-conjunction.txt new file mode 100644 index 0000000..448a403 --- /dev/null +++ b/integration/fail/type-mismatch/declaration-int-bool-conjunction.txt @@ -0,0 +1 @@ +let foo: int = true && true; \ No newline at end of file diff --git a/integration/fail/type-mismatch/declaration-int-bool-negation.out b/integration/fail/type-mismatch/declaration-int-bool-negation.out new file mode 100644 index 0000000..7b67981 --- /dev/null +++ b/integration/fail/type-mismatch/declaration-int-bool-negation.out @@ -0,0 +1,9 @@ +Error: Type Mismatch + ╭─[declaration-int-bool-negation.txt:2:16] + │ + 2 │ let foo: int = !yes; + · ─┬─ ┬ + · ╰─────── Expected type `int`, + · │ + · ╰── but expression is of type `bool` +───╯ diff --git a/integration/fail/type-mismatch/declaration-int-bool-negation.txt b/integration/fail/type-mismatch/declaration-int-bool-negation.txt new file mode 100644 index 0000000..b22bdb2 --- /dev/null +++ b/integration/fail/type-mismatch/declaration-int-bool-negation.txt @@ -0,0 +1,2 @@ +let yes: bool = true; +let foo: int = !yes; \ No newline at end of file diff --git a/integration/fail/type-mismatch/declaration-int-bool.out b/integration/fail/type-mismatch/declaration-int-bool.out new file mode 100644 index 0000000..6124794 --- /dev/null +++ b/integration/fail/type-mismatch/declaration-int-bool.out @@ -0,0 +1,9 @@ +Error: Type Mismatch + ╭─[declaration-int-bool.txt:1:16] + │ + 1 │ let foo: int = true; + · ─┬─ ──┬─ + · ╰────────── Expected type `int`, + · │ + · ╰─── but expression is of type `bool` +───╯ diff --git a/integration/fail/type-mismatch/declaration-int-bool.txt b/integration/fail/type-mismatch/declaration-int-bool.txt new file mode 100644 index 0000000..90b007b --- /dev/null +++ b/integration/fail/type-mismatch/declaration-int-bool.txt @@ -0,0 +1 @@ +let foo: int = true; \ No newline at end of file diff --git a/integration/fail/type-mismatch/declaration-int-int-comparison.out b/integration/fail/type-mismatch/declaration-int-int-comparison.out new file mode 100644 index 0000000..24e6ba1 --- /dev/null +++ b/integration/fail/type-mismatch/declaration-int-int-comparison.out @@ -0,0 +1,9 @@ +Error: Type Mismatch + ╭─[declaration-int-int-comparison.txt:1:16] + │ + 1 │ let foo: int = 1 == 2; + · ─┬─ ───┬── + · ╰──────────── Expected type `int`, + · │ + · ╰──── but expression is of type `bool` +───╯ diff --git a/integration/fail/type-mismatch/declaration-int-int-comparison.txt b/integration/fail/type-mismatch/declaration-int-int-comparison.txt new file mode 100644 index 0000000..b025900 --- /dev/null +++ b/integration/fail/type-mismatch/declaration-int-int-comparison.txt @@ -0,0 +1 @@ +let foo: int = 1 == 2; \ No newline at end of file diff --git a/integration/fail/type-mismatch/declaration-int-tuple-bool-tuple.out b/integration/fail/type-mismatch/declaration-int-tuple-bool-tuple.out new file mode 100644 index 0000000..d06a2f7 --- /dev/null +++ b/integration/fail/type-mismatch/declaration-int-tuple-bool-tuple.out @@ -0,0 +1,9 @@ +Error: Type Mismatch + ╭─[declaration-int-tuple-bool-tuple.txt:1:20] + │ + 1 │ let foo: (int,) = (true,); + · ─┬─ ──┬─ + · ╰───────────── Expected type `int`, + · │ + · ╰─── but expression is of type `bool` +───╯ diff --git a/integration/fail/type-mismatch/declaration-int-tuple-bool-tuple.txt b/integration/fail/type-mismatch/declaration-int-tuple-bool-tuple.txt new file mode 100644 index 0000000..3f83e34 --- /dev/null +++ b/integration/fail/type-mismatch/declaration-int-tuple-bool-tuple.txt @@ -0,0 +1 @@ +let foo: (int,) = (true,); \ No newline at end of file diff --git a/integration/fail/type-mismatch/declaration-unit-one-tuple.out b/integration/fail/type-mismatch/declaration-unit-one-tuple.out new file mode 100644 index 0000000..3391591 --- /dev/null +++ b/integration/fail/type-mismatch/declaration-unit-one-tuple.out @@ -0,0 +1,9 @@ +Error: Type Mismatch + ╭─[declaration-unit-one-tuple.txt:1:15] + │ + 1 │ let foo: () = (1,); + · ─┬ ──┬─ + · ╰───────── Expected tuple with 0 elements, + · │ + · ╰─── but expression has 1 elements +───╯ diff --git a/integration/fail/type-mismatch/declaration-unit-one-tuple.txt b/integration/fail/type-mismatch/declaration-unit-one-tuple.txt new file mode 100644 index 0000000..7cb795d --- /dev/null +++ b/integration/fail/type-mismatch/declaration-unit-one-tuple.txt @@ -0,0 +1 @@ +let foo: () = (1,); \ No newline at end of file diff --git a/integration/fail/type-mismatch/expression-arithmetic-int-bool.out b/integration/fail/type-mismatch/expression-arithmetic-int-bool.out new file mode 100644 index 0000000..872d89f --- /dev/null +++ b/integration/fail/type-mismatch/expression-arithmetic-int-bool.out @@ -0,0 +1,9 @@ +Error: Type Mismatch + ╭─[expression-arithmetic-int-bool.txt:1:20] + │ + 1 │ let foo: int = 1 + true; + · ┬ ──┬─ + · ╰─────── Expected type `int`, + · │ + · ╰─── but expression is of type `bool` +───╯ diff --git a/integration/fail/type-mismatch/expression-arithmetic-int-bool.txt b/integration/fail/type-mismatch/expression-arithmetic-int-bool.txt new file mode 100644 index 0000000..41a75ee --- /dev/null +++ b/integration/fail/type-mismatch/expression-arithmetic-int-bool.txt @@ -0,0 +1 @@ +let foo: int = 1 + true; \ No newline at end of file diff --git a/integration/fail/type-mismatch/expression-comparison-int-bool.out b/integration/fail/type-mismatch/expression-comparison-int-bool.out new file mode 100644 index 0000000..2b5514f --- /dev/null +++ b/integration/fail/type-mismatch/expression-comparison-int-bool.out @@ -0,0 +1,9 @@ +Error: Type Mismatch + ╭─[expression-comparison-int-bool.txt:1:22] + │ + 1 │ let foo: bool = 1 == true; + · ─┬ ──┬─ + · ╰─────── Expected type `int`, + · │ + · ╰─── but expression is of type `bool` +───╯ diff --git a/integration/fail/type-mismatch/expression-comparison-int-bool.txt b/integration/fail/type-mismatch/expression-comparison-int-bool.txt new file mode 100644 index 0000000..f3b74c2 --- /dev/null +++ b/integration/fail/type-mismatch/expression-comparison-int-bool.txt @@ -0,0 +1 @@ +let foo: bool = 1 == true; \ No newline at end of file diff --git a/integration/fail/type-mismatch/expression-conjunction-bool-int.out b/integration/fail/type-mismatch/expression-conjunction-bool-int.out new file mode 100644 index 0000000..ca973a7 --- /dev/null +++ b/integration/fail/type-mismatch/expression-conjunction-bool-int.out @@ -0,0 +1,9 @@ +Error: Type Mismatch + ╭─[expression-conjunction-bool-int.txt:1:25] + │ + 1 │ let foo: bool = true || 1; + · ─┬ ┬ + · ╰──── Expected type `bool`, + · │ + · ╰── but expression is of type `int` +───╯ diff --git a/integration/fail/type-mismatch/expression-conjunction-bool-int.txt b/integration/fail/type-mismatch/expression-conjunction-bool-int.txt new file mode 100644 index 0000000..d95a755 --- /dev/null +++ b/integration/fail/type-mismatch/expression-conjunction-bool-int.txt @@ -0,0 +1 @@ +let foo: bool = true || 1; \ No newline at end of file diff --git a/integration/fail/type-mismatch/expression-int-0.out b/integration/fail/type-mismatch/expression-int-0.out new file mode 100644 index 0000000..f7c4b64 --- /dev/null +++ b/integration/fail/type-mismatch/expression-int-0.out @@ -0,0 +1,9 @@ +Error: Type Mismatch + ╭─[expression-int-0.txt:2:16] + │ + 2 │ let bar: int = foo.0; + · ──┬─┬ + · ╰──── Expression is of type `int`, + · │ + · ╰── but expected type `(_, ..)` +───╯ diff --git a/integration/fail/type-mismatch/expression-int-0.txt b/integration/fail/type-mismatch/expression-int-0.txt new file mode 100644 index 0000000..351400d --- /dev/null +++ b/integration/fail/type-mismatch/expression-int-0.txt @@ -0,0 +1,2 @@ +let foo: int = 1; +let bar: int = foo.0; \ No newline at end of file diff --git a/integration/fail/type-mismatch/expression-minus-bool.out b/integration/fail/type-mismatch/expression-minus-bool.out new file mode 100644 index 0000000..48ac6a8 --- /dev/null +++ b/integration/fail/type-mismatch/expression-minus-bool.out @@ -0,0 +1,9 @@ +Error: Type Mismatch + ╭─[expression-minus-bool.txt:1:17] + │ + 1 │ let foo: int = -true; + · ┬──┬─ + · ╰────── Expected type `int`, + · │ + · ╰─── but expression is of type `bool` +───╯ diff --git a/integration/fail/type-mismatch/expression-minus-bool.txt b/integration/fail/type-mismatch/expression-minus-bool.txt new file mode 100644 index 0000000..cb8b662 --- /dev/null +++ b/integration/fail/type-mismatch/expression-minus-bool.txt @@ -0,0 +1 @@ +let foo: int = -true; \ No newline at end of file diff --git a/integration/fail/type-mismatch/expression-not-int.out b/integration/fail/type-mismatch/expression-not-int.out new file mode 100644 index 0000000..bb70afd --- /dev/null +++ b/integration/fail/type-mismatch/expression-not-int.out @@ -0,0 +1,9 @@ +Error: Type Mismatch + ╭─[expression-not-int.txt:1:18] + │ + 1 │ let foo: bool = !0; + · ┬┬ + · ╰─── Expected type `bool`, + · │ + · ╰── but expression is of type `int` +───╯ diff --git a/integration/fail/type-mismatch/expression-not-int.txt b/integration/fail/type-mismatch/expression-not-int.txt new file mode 100644 index 0000000..7e9ca1e --- /dev/null +++ b/integration/fail/type-mismatch/expression-not-int.txt @@ -0,0 +1 @@ +let foo: bool = !0; \ No newline at end of file diff --git a/integration/fail/type-mismatch/expression-unit-0.out b/integration/fail/type-mismatch/expression-unit-0.out new file mode 100644 index 0000000..6004890 --- /dev/null +++ b/integration/fail/type-mismatch/expression-unit-0.out @@ -0,0 +1,9 @@ +Error: Type Mismatch + ╭─[expression-unit-0.txt:1:16] + │ + 1 │ let foo: int = ().0; + · ─┬┬─ + · ╰──── Found tuple with 0 elements, + · │ + · ╰─── but expression requires at least 1 elements +───╯ diff --git a/integration/fail/type-mismatch/expression-unit-0.txt b/integration/fail/type-mismatch/expression-unit-0.txt new file mode 100644 index 0000000..525e70f --- /dev/null +++ b/integration/fail/type-mismatch/expression-unit-0.txt @@ -0,0 +1 @@ +let foo: int = ().0; \ No newline at end of file diff --git a/integration/fail/type-mismatch/if-condition-int.out b/integration/fail/type-mismatch/if-condition-int.out new file mode 100644 index 0000000..475ce66 --- /dev/null +++ b/integration/fail/type-mismatch/if-condition-int.out @@ -0,0 +1,9 @@ +Error: Type Mismatch + ╭─[if-condition-int.txt:1:4] + │ + 1 │ if 0 {} + · ─┬ ┬ + · ╰──── Expected type `bool`, + · │ + · ╰── but expression is of type `int` +───╯ diff --git a/integration/fail/type-mismatch/if-condition-int.txt b/integration/fail/type-mismatch/if-condition-int.txt new file mode 100644 index 0000000..d8b969a --- /dev/null +++ b/integration/fail/type-mismatch/if-condition-int.txt @@ -0,0 +1 @@ +if 0 {} \ No newline at end of file diff --git a/integration/fail/type-mismatch/while-condition-int.out b/integration/fail/type-mismatch/while-condition-int.out new file mode 100644 index 0000000..00ba068 --- /dev/null +++ b/integration/fail/type-mismatch/while-condition-int.out @@ -0,0 +1,9 @@ +Error: Type Mismatch + ╭─[while-condition-int.txt:1:7] + │ + 1 │ while 0 {} + · ──┬── ┬ + · ╰────── Expected type `bool`, + · │ + · ╰── but expression is of type `int` +───╯ diff --git a/integration/fail/type-mismatch/while-condition-int.txt b/integration/fail/type-mismatch/while-condition-int.txt new file mode 100644 index 0000000..427170f --- /dev/null +++ b/integration/fail/type-mismatch/while-condition-int.txt @@ -0,0 +1 @@ +while 0 {} \ No newline at end of file diff --git a/integration/fail/unknown-type/declaration.out b/integration/fail/unknown-type/declaration.out new file mode 100644 index 0000000..ff45b73 --- /dev/null +++ b/integration/fail/unknown-type/declaration.out @@ -0,0 +1,7 @@ +Error: Unknown type + ╭─[declaration.txt:1:10] + │ + 1 │ let foo: Foo = 0; + · ─┬─ + · ╰─── This type has not been defined +───╯ diff --git a/integration/fail/unknown-type/declaration.txt b/integration/fail/unknown-type/declaration.txt new file mode 100644 index 0000000..5c84cde --- /dev/null +++ b/integration/fail/unknown-type/declaration.txt @@ -0,0 +1 @@ +let foo: Foo = 0; \ No newline at end of file diff --git a/integration/main.rs b/integration/main.rs new file mode 100644 index 0000000..c65e9d9 --- /dev/null +++ b/integration/main.rs @@ -0,0 +1,116 @@ +#![warn(rust_2018_idioms)] +#![deny(elided_lifetimes_in_paths)] +#![forbid(unsafe_code)] + +use compiler::{ + compile::{compile, CompileResult}, + diagnostic::{Diagnostic, Source} +}; +use lazy_regex::regex_replace_all; +use libtest::{run_tests, Arguments, Outcome, Test}; +use std::{ + fs::{self, File}, + io::{Read as _, Write as _}, + panic::catch_unwind, + path::{Path, PathBuf} +}; + +struct TestData { + path: PathBuf +} + +fn run_test(data: &TestData) -> anyhow::Result<()> { + let mut file = File::open(&data.path)?; + let mut input = String::new(); + file.read_to_string(&mut input)?; + drop(file); + + let compile_result = match catch_unwind(|| compile(&input)) { + Ok(result) => result, + Err(e) => match e.downcast_ref::() { + Some(str) => anyhow::bail!("The compiler paniced: {str}"), + None => match e.downcast_ref::<&str>() { + Some(str) => anyhow::bail!("The compiler paniced: {str}"), + None => anyhow::bail!("The compiler paniced") + } + } + }; + let msg = match compile_result { + Ok(CompileResult::Ok { .. }) => anyhow::bail!("Code compiled successfully"), + Ok(CompileResult::InternalErr(code, err)) => { + let mut buf = Vec::new(); + Diagnostic::ice(err).write(Source::new(&code), &mut buf)?; + String::from_utf8(buf).unwrap() + }, + Err(err) => { + let filename = data.path.file_name().unwrap().to_str().unwrap(); + let mut buf = Vec::new(); + err.write(Source::new(&input).with_filename(filename), &mut buf)?; + String::from_utf8(buf).unwrap() + } + }; + + // remove colors from the msg + let msg = regex_replace_all!("\x1B\\[[^m]+m", &msg, |_| ""); + + let mut path = data.path.clone(); + path.set_extension("out"); + if !path.exists() { + let mut file = File::create(&path)?; + write!(file, "{msg}")?; + Ok(()) + } else { + let mut file = File::open(&path)?; + let mut buf = String::new(); + file.read_to_string(&mut buf)?; + drop(file); + + if msg == buf { + Ok(()) + } else { + Err(anyhow::anyhow!( + "Error: Expected output\n\n{buf}\n\nActual output\n\n{msg}" + )) + } + } +} + +fn add_tests_from_dir

(tests: &mut Vec>, path: P) -> anyhow::Result<()> +where + P: AsRef +{ + for file in fs::read_dir(path)? { + let file = file?; + let path = file.path(); + let ty = file.file_type()?; + if ty.is_dir() { + add_tests_from_dir(tests, &path)?; + } else if ty.is_file() + && path.extension().map(|ext| ext == "txt").unwrap_or(false) + { + tests.push(Test { + name: path.display().to_string(), + kind: "".into(), + is_ignored: false, + is_bench: false, + data: TestData { path } + }); + } + } + Ok(()) +} + +fn main() -> anyhow::Result<()> { + let args = Arguments::from_args(); + + let mut tests = Vec::new(); + add_tests_from_dir(&mut tests, "integration/fail")?; + + run_tests(&args, tests, |test| match run_test(&test.data) { + Ok(()) => Outcome::Passed, + Err(err) => Outcome::Failed { + msg: Some(format!("{err:?}")) + } + }) + .exit(); +} diff --git a/rustfmt.toml b/rustfmt.toml new file mode 100644 index 0000000..8ea0007 --- /dev/null +++ b/rustfmt.toml @@ -0,0 +1,35 @@ +edition = "2021" +max_width = 90 +newline_style = "Unix" +unstable_features = true + +# always use tabs. +hard_tabs = true +tab_spaces = 4 + +# commas inbetween but not after +match_block_trailing_comma = true +trailing_comma = "Never" + +# fix my imports for me +imports_granularity = "Crate" +group_imports = "One" + +# format everything +format_code_in_doc_comments = true +format_macro_matchers = true + +# don't keep outdated syntax +use_field_init_shorthand = true +use_try_shorthand = true + +# condense Struct { _, _ } to Struct { .. } +condense_wildcard_suffixes = true + +# prefer foo(Bar { \n }) over foo(\nBar { \n }\n) +overflow_delimited_expr = true + +# I wish there was a way to allow 0..n but not a + 1..b + 2 +# However, the later looks so terible that I use spaces everywhere +# https://github.com/rust-lang/rustfmt/issues/3367 +spaces_around_ranges = true diff --git a/src/ast/assignment.rs b/src/ast/assignment.rs new file mode 100644 index 0000000..64d5c6b --- /dev/null +++ b/src/ast/assignment.rs @@ -0,0 +1,30 @@ +use super::Expression; +use syn::{ + parse::{Parse, ParseStream}, + punctuated::Punctuated, + spanned::Spanned, + Member, Token +}; + +#[derive(Clone, Debug)] +pub struct Assignment { + pub lhs: Punctuated, + pub eq_token: Token![=], + pub rhs: Expression, + pub semi_token: Token![;] +} + +impl Parse for Assignment { + fn parse(input: ParseStream<'_>) -> syn::Result { + let lhs = Punctuated::parse_separated_nonempty(input)?; + if lhs.trailing_punct() { + bail!(lhs.span() => "Expected ident after fullstop"); + } + Ok(Self { + lhs, + eq_token: input.parse()?, + rhs: input.parse()?, + semi_token: input.parse()? + }) + } +} diff --git a/src/ast/conditional.rs b/src/ast/conditional.rs new file mode 100644 index 0000000..122e656 --- /dev/null +++ b/src/ast/conditional.rs @@ -0,0 +1,84 @@ +use super::{Block, Expression}; +use proc_macro2::{Ident, Span}; +use syn::{ + parse::{Parse, ParseStream}, + token::{Brace, Paren}, + Token +}; + +#[derive(Clone, Debug)] +pub enum Condition { + Bool(Expression), + LetSome { + let_token: Token![let], + some_span: Span, + paren_token: Paren, + ident: Ident, + eq_token: Token![=], + expr: Expression + } +} + +impl Parse for Condition { + fn parse(input: ParseStream<'_>) -> syn::Result { + Ok(if input.peek(Token![let]) { + let content; + Self::LetSome { + let_token: input.parse()?, + some_span: { + let some: Ident = input.parse()?; + if some != "Some" { + bail!(some.span() => "Expected `Some`"); + } + some.span() + }, + paren_token: syn::parenthesized!(content in input), + ident: content.parse()?, + eq_token: input.parse()?, + expr: input.parse()? + } + } else { + Self::Bool(input.parse()?) + }) + } +} + +#[derive(Clone, Debug)] +pub enum ElseBranch { + If(Box), + Block(Block) +} + +impl Parse for ElseBranch { + fn parse(input: ParseStream<'_>) -> syn::Result { + Ok(if input.peek(Token![if]) { + Self::If(input.parse()?) + } else if input.peek(Brace) { + Self::Block(input.parse()?) + } else { + bail!(input.span() => "Expected `if` or block"); + }) + } +} + +#[derive(Clone, Debug)] +pub struct If { + pub if_token: Token![if], + pub condition: Condition, + pub then_branch: Block, + pub else_branch: Option<(Token![else], ElseBranch)> +} + +impl Parse for If { + fn parse(input: ParseStream<'_>) -> syn::Result { + Ok(Self { + if_token: input.parse()?, + condition: input.parse()?, + then_branch: input.parse()?, + else_branch: input + .peek(Token![else]) + .then(|| syn::Result::Ok((input.parse()?, input.parse()?))) + .transpose()? + }) + } +} diff --git a/src/ast/declaration.rs b/src/ast/declaration.rs new file mode 100644 index 0000000..23ce574 --- /dev/null +++ b/src/ast/declaration.rs @@ -0,0 +1,31 @@ +use super::{Expression, Type}; +use proc_macro2::Ident; +use syn::{ + parse::{Parse, ParseStream}, + Token +}; + +#[derive(Clone, Debug)] +pub struct Declaration { + pub let_token: Token![let], + pub ident: Ident, + pub colon_token: Token![:], + pub ty: Type, + pub eq_token: Token![=], + pub expr: Expression, + pub semi_token: Token![;] +} + +impl Parse for Declaration { + fn parse(input: ParseStream<'_>) -> syn::Result { + Ok(Self { + let_token: input.parse()?, + ident: input.parse()?, + colon_token: input.parse()?, + ty: input.parse()?, + eq_token: input.parse()?, + expr: input.parse()?, + semi_token: input.parse()? + }) + } +} diff --git a/src/ast/expression/mod.rs b/src/ast/expression/mod.rs new file mode 100644 index 0000000..5dfeb4e --- /dev/null +++ b/src/ast/expression/mod.rs @@ -0,0 +1,433 @@ +use super::{Generics, Type}; +use proc_macro2::{Ident, Span}; +use std::fmt::{self, Debug, Display, Formatter}; +use syn::{ + punctuated::Punctuated, spanned::Spanned, token::Paren, Index, LitBool, LitInt, Token +}; + +mod parse; + +#[derive(Clone, Copy, Debug)] +pub enum ArithmeticOp { + Add(Token![+]), + Sub(Token![-]), + Mul(Token![*]), + Div(Token![/]) +} + +impl ArithmeticOp { + pub fn is_add_or_sub_op(self) -> bool { + matches!(self, Self::Add(_) | Self::Sub(_)) + } + + pub fn is_mul_or_div_op(self) -> bool { + matches!(self, Self::Mul(_) | Self::Div(_)) + } + + pub fn char(self) -> char { + match self { + ArithmeticOp::Add(_) => '+', + ArithmeticOp::Sub(_) => '-', + ArithmeticOp::Mul(_) => '*', + ArithmeticOp::Div(_) => '/' + } + } +} + +impl Spanned for ArithmeticOp { + fn span(&self) -> Span { + match self { + Self::Add(token) => token.span, + Self::Sub(token) => token.span, + Self::Mul(token) => token.span, + Self::Div(token) => token.span + } + } +} + +impl Display for ArithmeticOp { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + write!(f, "{}", self.char()) + } +} + +#[derive(Clone, Copy, Debug)] +pub enum ComparisonOp { + Eq(Token![==]), + Neq(Token![!=]), + Lt(Token![<]), + Le(Token![<=]), + Gt(Token![>]), + Ge(Token![>=]) +} + +impl ComparisonOp { + fn str(self) -> &'static str { + match self { + ComparisonOp::Eq(_) => "==", + ComparisonOp::Neq(_) => "!=", + ComparisonOp::Lt(_) => "<", + ComparisonOp::Le(_) => "<=", + ComparisonOp::Gt(_) => ">", + ComparisonOp::Ge(_) => ">=" + } + } +} + +impl Spanned for ComparisonOp { + fn span(&self) -> Span { + match self { + Self::Eq(token) => token.span(), + Self::Neq(token) => token.span(), + Self::Lt(token) => token.span, + Self::Le(token) => token.span(), + Self::Gt(token) => token.span, + Self::Ge(token) => token.span() + } + } +} + +impl Display for ComparisonOp { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + f.write_str(self.str()) + } +} + +#[derive(Clone, Copy, Debug)] +pub enum ConjunctionOp { + And(Token![&&]), + Or(Token![||]) +} + +impl ConjunctionOp { + fn str(self) -> &'static str { + match self { + ConjunctionOp::And(_) => "&&", + ConjunctionOp::Or(_) => "||" + } + } +} + +impl Spanned for ConjunctionOp { + fn span(&self) -> Span { + match self { + Self::And(token) => token.span(), + Self::Or(token) => token.span() + } + } +} + +impl Display for ConjunctionOp { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + f.write_str(self.str()) + } +} + +#[derive(Clone)] +pub enum Expression { + /// An expression enclosed in parentheses. + Term { + paren_token: Paren, + expr: Box + }, + + /// Variable access. + Ident(Ident), + + /// A boolean literal. + Bool(LitBool), + + /// A number literal. + Literal(LitInt), + + /// A boolean negation (`!expr`). + BoolNegation { + not_token: Token![!], + expr: Box + }, + + /// An integer negation (`-expr`). + IntNegation { + minus_token: Token![-], + expr: Box + }, + + /// An arithmetic expression. + Arithmetic { + lhs: Box, + op: ArithmeticOp, + rhs: Box + }, + + /// A comparison. + Comparison { + lhs: Box, + op: ComparisonOp, + rhs: Box + }, + + /// A conjunction or disjunction. + Conjunction { + lhs: Box, + op: ConjunctionOp, + rhs: Box + }, + + /// A tuple constructor. + TupleCtor { + paren_token: Paren, + elems: Punctuated + }, + + /// A tuple index expression. + TupleIndex { + expr: Box, + dot_token: Token![.], + index: Index + }, + + /// An `Rc` constructor. + RcCtor { + span: Span, + paren_token: Paren, + expr: Box + }, + + /// An `Option` constructor. + OptionCtor { + span: Span, + expr: Option<(Paren, Box)> + }, + + /// A deref expression. + Deref { + star_token: Token![*], + expr: Box + }, + + /// A function call. + FnCall { + ident: Ident, + generics: Option>, + paren_token: Paren, + inputs: Punctuated + } +} + +impl Expression { + /// Replace the rightmost part of this expression with the return value + /// of the callback. If self is not a binary expression, or if the filter + /// callback returns false, calls the callback with self. + fn replace_rightmost(self, filter: F, callback: C) -> Self + where + F: Fn(&Self) -> bool, + C: FnOnce(Self) -> Self + { + if !filter(&self) { + return callback(self); + } + + match self { + Self::Arithmetic { lhs, op, rhs } => Self::Arithmetic { + lhs, + op, + rhs: Box::new(rhs.replace_rightmost(filter, callback)) + }, + Self::Comparison { lhs, op, rhs } => Self::Comparison { + lhs, + op, + rhs: Box::new(rhs.replace_rightmost(filter, callback)) + }, + Self::Conjunction { lhs, op, rhs } => Self::Conjunction { + lhs, + op, + rhs: Box::new(rhs.replace_rightmost(filter, callback)) + }, + + this => callback(this) + } + } +} + +impl Spanned for Expression { + fn span(&self) -> Span { + match self { + Self::Term { paren_token, .. } => paren_token.span, + Self::Ident(ident) => ident.span(), + Self::Bool(bool) => bool.span(), + Self::Literal(lit) => lit.span(), + Self::BoolNegation { not_token, .. } => not_token.span, + Self::IntNegation { minus_token, .. } => minus_token.span, + Self::Arithmetic { lhs, op, rhs } => lhs + .span() + .join(op.span()) + .unwrap() + .join(rhs.span()) + .unwrap(), + Self::Comparison { lhs, op, rhs } => lhs + .span() + .join(op.span()) + .unwrap() + .join(rhs.span()) + .unwrap(), + Self::Conjunction { lhs, op, rhs } => lhs + .span() + .join(op.span()) + .unwrap() + .join(rhs.span()) + .unwrap(), + Self::TupleCtor { paren_token, .. } => paren_token.span, + Self::TupleIndex { + expr, + dot_token, + index + } => expr + .span() + .join(dot_token.span()) + .unwrap() + .join(index.span()) + .unwrap(), + Self::RcCtor { span, .. } => *span, + Self::OptionCtor { span, .. } => *span, + Self::Deref { star_token, expr } => { + star_token.span.join(expr.span()).unwrap() + }, + Self::FnCall { ident, .. } => ident.span() + } + } +} + +impl Debug for Expression { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + match self { + Self::Term { expr, .. } => f + .debug_struct("Expression::Term") + .field("expr", &expr) + .finish_non_exhaustive(), + Self::Ident(ident) => { + f.debug_tuple("Expression::Ident").field(ident).finish() + }, + Self::Bool(lit) => { + f.debug_tuple("Expression::Bool").field(&lit.value).finish() + }, + Self::Literal(lit) => f + .debug_tuple("Expression::Literal") + .field(&lit.base10_parse::()) + .finish(), + Self::BoolNegation { expr, .. } => f + .debug_struct("Expression::BoolNegation") + .field("expr", &expr) + .finish_non_exhaustive(), + Self::IntNegation { expr, .. } => f + .debug_struct("Expression::IntNegation") + .field("expr", &expr) + .finish_non_exhaustive(), + Self::Arithmetic { lhs, op, rhs } => f + .debug_struct("Expression::Arithmetic") + .field("lhs", &lhs) + .field("op", &op.char()) + .field("rhs", &rhs) + .finish(), + Self::Comparison { lhs, op, rhs } => f + .debug_struct("Expression::Comparison") + .field("lhs", &lhs) + .field("op", &op.str()) + .field("rhs", &rhs) + .finish(), + Self::Conjunction { lhs, op, rhs } => f + .debug_struct("Expression::Conjunction") + .field("lhs", &lhs) + .field("op", &op.str()) + .field("rhs", &rhs) + .finish(), + Self::TupleCtor { elems, .. } => f + .debug_struct("Expression::TupleCtor") + .field("elems", &elems) + .finish_non_exhaustive(), + Self::TupleIndex { expr, index, .. } => f + .debug_struct("Expression::TupleCtor") + .field("expr", &expr) + .field("index", &index.index) + .finish_non_exhaustive(), + Self::RcCtor { expr, .. } => f + .debug_struct("Expression::RcCtor") + .field("expr", &expr) + .finish_non_exhaustive(), + Self::OptionCtor { expr, .. } => f + .debug_struct("Expression::OptionCtor") + .field("expr", &expr.as_ref().map(|(_, expr)| expr)) + .finish_non_exhaustive(), + Self::Deref { expr, .. } => f + .debug_struct("Expression::Deref") + .field("expr", &expr) + .finish_non_exhaustive(), + Self::FnCall { ident, .. } => f + .debug_struct("Expression::FnCall") + .field("ident", &ident) + .finish_non_exhaustive() + } + } +} + +impl Display for Expression { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + match self { + Self::Term { expr, .. } => write!(f, "({{ {expr} }})"), + Self::Ident(ident) => { + write!(f, "{ident}") + }, + Self::Bool(lit) => match lit.value() { + true => f.write_str("true"), + false => f.write_str("false") + }, + Self::Literal(lit) => f.write_str(lit.base10_digits()), + Self::BoolNegation { expr, .. } => write!(f, "!{{ {expr} }}"), + Self::IntNegation { expr, .. } => write!(f, "-{{ {expr} }}"), + Self::Arithmetic { lhs, op, rhs } => { + write!(f, "{{ {lhs} }} {op} {{ {rhs} }}") + }, + Self::Comparison { lhs, op, rhs } => { + write!(f, "{{ {lhs} }} {op} {{ {rhs} }}") + }, + Self::Conjunction { lhs, op, rhs } => { + write!(f, "{{ {lhs} }} {op} {{ {rhs} }}") + }, + Self::TupleCtor { elems, .. } => { + f.write_str("(")?; + for elem in elems.iter() { + write!(f, "{{ {elem} }}, ")?; + } + f.write_str(")") + }, + Self::TupleIndex { expr, index, .. } => { + write!(f, "{{ {expr} }}.{}", index.index) + }, + Self::RcCtor { expr, .. } => write!(f, "Rc({{ {expr} }})"), + Self::OptionCtor { expr: None, .. } => write!(f, "None"), + Self::OptionCtor { + expr: Some((_, expr)), + .. + } => write!(f, "Some({{ {expr} }})"), + Self::Deref { expr, .. } => write!(f, "*{{ {expr} }}"), + Self::FnCall { + ident, + generics, + inputs, + .. + } => { + write!(f, "{ident}")?; + if let Some(g) = generics { + write!(f, "::{g}")?; + } + f.write_str("(")?; + for arg in inputs.iter() { + write!(f, "{{ {arg} }}, ")?; + } + f.write_str(")") + } + } + } +} + +#[cfg(test)] +mod tests; diff --git a/src/ast/expression/parse.rs b/src/ast/expression/parse.rs new file mode 100644 index 0000000..7dcb5e0 --- /dev/null +++ b/src/ast/expression/parse.rs @@ -0,0 +1,300 @@ +use super::{ArithmeticOp, ComparisonOp, ConjunctionOp, Expression}; +use proc_macro2::Ident; +use syn::{ + parse::{Parse, ParseStream}, + punctuated::Punctuated, + token::Paren, + Index, LitBool, LitInt, Token +}; + +macro_rules! parse_next_op { + ($left:ident, $input:ident,Self:: $fn:ident, $op:ident:: $variant:ident) => { + ( + Self::$fn( + $left, + $op::$variant($input.parse()?), + Self::parse_first($input)? + ), + false + ) + }; +} + +macro_rules! reorder_expr { + ($lhs:ident, $op:ident, $rhs:ident => Self::$new_variant:ident { + $(Self::$variant:ident { + $lhs_lhs:ident, + $lhs_op:ident, + $lhs_rhs:ident + } $(if $condition:expr)?),* + }) => {{ + // This method will not be called when there are not variants in the macro + // Also, the variables in the pattern can only be used by the if condition + #[allow(dead_code, unused_variables)] + fn filter(expr: &Expression) -> bool { + match expr { + $(Expression::$variant { + lhs: $lhs_lhs, + op: $lhs_op, + rhs: $lhs_rhs + } $(if $condition)? => true,)* + _ => false + } + } + + match $lhs { + $(Self::$variant { + lhs: $lhs_lhs, + op: $lhs_op, + rhs: $lhs_rhs + } $(if $condition)? => { + Self::$variant { + lhs: $lhs_lhs, + op: $lhs_op, + rhs: $lhs_rhs + }.replace_rightmost(filter, |lhs_rhs| { + Self::$new_variant { + lhs: Box::new(lhs_rhs), + op: $op, + rhs: Box::new($rhs) + } + }) + },)* + lhs => Self::$new_variant { + lhs: Box::new(lhs), + op: $op, + rhs: Box::new($rhs) + } + } + }}; +} + +impl Expression { + /// Parse the first expression from the input. Might not be the full + /// expression. + fn parse_first(input: ParseStream<'_>) -> syn::Result { + if input.peek(Paren) { + let content; + let paren_token = syn::parenthesized!(content in input); + + let elems = Punctuated::parse_terminated(&content)?; + return Ok(if elems.len() == 1 && !elems.trailing_punct() { + // parenthezised expression, not a tuple + Self::Term { + paren_token, + expr: Box::new(elems.into_iter().next().unwrap()) + } + } else { + // tuple + Self::TupleCtor { paren_token, elems } + }); + } + + if input.peek(LitBool) { + return Ok(Self::Bool(input.parse()?)); + } + + if input.peek(LitInt) { + return Ok(Self::Literal(input.parse()?)); + } + + if input.peek(Token![!]) { + return Ok(Self::BoolNegation { + not_token: input.parse()?, + expr: Box::new(Self::parse_first(input)?) + }); + } + + if input.peek(Token![-]) { + return Ok(Self::IntNegation { + minus_token: input.parse()?, + expr: Box::new(Self::parse_first(input)?) + }); + } + + if input.peek(Token![*]) { + return Ok(Self::Deref { + star_token: input.parse()?, + expr: Box::new(Self::parse_first(input)?) + }); + } + + let ident: Ident = input.parse()?; + + if input.peek(Token![.]) { + let mut left = Self::Ident(ident); + while input.peek(Token![.]) { + left = Self::tuple_index(left, input.parse()?, input.parse()?); + } + return Ok(left); + } + + Ok(if ident == "Rc" { + let content; + let paren_token = syn::parenthesized!(content in input); + Self::RcCtor { + span: ident.span(), + paren_token, + expr: Box::new(content.parse()?) + } + } else if ident == "Some" { + let content; + let paren_token = syn::parenthesized!(content in input); + Self::OptionCtor { + span: ident.span(), + expr: Some((paren_token, Box::new(content.parse()?))) + } + } else if ident == "None" { + Self::OptionCtor { + span: ident.span(), + expr: None + } + } else if input.peek(Paren) { + let content; + let paren_token = syn::parenthesized!(content in input); + let inputs = Punctuated::parse_terminated(&content)?; + Self::FnCall { + ident, + generics: None, + paren_token, + inputs + } + } else if input.peek(Token![::]) { + let _turbofish: Token![::] = input.parse()?; + let generics = Some(input.parse()?); + let content; + let paren_token = syn::parenthesized!(content in input); + let inputs = Punctuated::parse_terminated(&content)?; + Self::FnCall { + ident, + generics, + paren_token, + inputs + } + } else { + Self::Ident(ident) + }) + } + + // this is the strongest binding binary operator, and since all unary + // operators are left of their argument we don't need to take them + // into account + fn mul_or_div_op(lhs: Self, op: ArithmeticOp, rhs: Self) -> Self { + debug_assert!(op.is_mul_or_div_op()); + reorder_expr!(lhs, op, rhs => Self::Arithmetic { + // ¦a + b¦ * c = a + (b * c) + Self::Arithmetic { lhs_lhs, lhs_op, lhs_rhs } if lhs_op.is_add_or_sub_op(), + + // ¦a == b¦ * c = a == (b * c) + Self::Comparison { lhs_lhs, lhs_op, lhs_rhs }, + + // ¦a && b¦ * c = a && (b * c) + // (this is essential to allow comparison operators to be parsed next) + Self::Conjunction { lhs_lhs, lhs_op, lhs_rhs } + }) + } + + // this is the 2nd strongest binding binary operator, after mul or div and + // before any of the boolean operators + fn add_or_sub_op(lhs: Self, op: ArithmeticOp, rhs: Self) -> Self { + debug_assert!(op.is_add_or_sub_op()); + reorder_expr!(lhs, op, rhs => Self::Arithmetic { + // ¦a == b¦ * c = a == (b * c) + Self::Comparison { lhs_lhs, lhs_op, lhs_rhs }, + + // ¦a && b¦ * c = a && (b * c) + // (this is essential to allow comparison operators to be parsed next) + Self::Conjunction { lhs_lhs, lhs_op, lhs_rhs } + }) + } + + // this is the 3rd strongest binding binary operator, after both arithmetic + // operators + fn comparison_op(lhs: Self, op: ComparisonOp, rhs: Self) -> Self { + reorder_expr!(lhs, op, rhs => Self::Comparison { + // ¦a && b¦ == c = a && (b == c) + Self::Conjunction { lhs_lhs, lhs_op, lhs_rhs } + }) + } + + // this is the least strongest binding binary operator + fn conjunction_op(lhs: Self, op: ConjunctionOp, rhs: Self) -> Self { + reorder_expr!(lhs, op, rhs => Self::Conjunction {}) + } + + // tuple indexing binds stronger than unary operators + fn tuple_index(left: Self, dot_token: Token![.], index: Index) -> Self { + match left { + Self::BoolNegation { not_token, expr } => Self::BoolNegation { + not_token, + expr: Box::new(Self::tuple_index(*expr, dot_token, index)) + }, + Self::IntNegation { minus_token, expr } => Self::IntNegation { + minus_token, + expr: Box::new(Self::tuple_index(*expr, dot_token, index)) + }, + Self::Deref { star_token, expr } => Self::Deref { + star_token, + expr: Box::new(Self::tuple_index(*expr, dot_token, index)) + }, + left => Self::TupleIndex { + expr: Box::new(left), + dot_token, + index + } + } + } + + fn parse_next(left: Self, input: ParseStream<'_>) -> syn::Result<(Self, bool)> { + if input.is_empty() { + return Ok((left, true)); + } + + Ok(if input.peek(Token![.]) { + ( + Self::tuple_index(left, input.parse()?, input.parse()?), + false + ) + } else if input.peek(Token![+]) { + parse_next_op!(left, input, Self::add_or_sub_op, ArithmeticOp::Add) + } else if input.peek(Token![-]) { + parse_next_op!(left, input, Self::add_or_sub_op, ArithmeticOp::Sub) + } else if input.peek(Token![*]) { + parse_next_op!(left, input, Self::mul_or_div_op, ArithmeticOp::Mul) + } else if input.peek(Token![/]) { + parse_next_op!(left, input, Self::mul_or_div_op, ArithmeticOp::Div) + } else if input.peek(Token![==]) { + parse_next_op!(left, input, Self::comparison_op, ComparisonOp::Eq) + } else if input.peek(Token![!=]) { + parse_next_op!(left, input, Self::comparison_op, ComparisonOp::Neq) + } else if input.peek(Token![<=]) { + parse_next_op!(left, input, Self::comparison_op, ComparisonOp::Le) + } else if input.peek(Token![<]) { + parse_next_op!(left, input, Self::comparison_op, ComparisonOp::Lt) + } else if input.peek(Token![>=]) { + parse_next_op!(left, input, Self::comparison_op, ComparisonOp::Ge) + } else if input.peek(Token![>]) { + parse_next_op!(left, input, Self::comparison_op, ComparisonOp::Gt) + } else if input.peek(Token![&&]) { + parse_next_op!(left, input, Self::conjunction_op, ConjunctionOp::And) + } else if input.peek(Token![||]) { + parse_next_op!(left, input, Self::conjunction_op, ConjunctionOp::Or) + } else { + (left, true) + }) + } +} + +impl Parse for Expression { + fn parse(input: ParseStream<'_>) -> syn::Result { + let mut expr = Self::parse_first(input)?; + loop { + let last; + (expr, last) = Self::parse_next(expr, input)?; + if last { + break; + } + } + Ok(expr) + } +} diff --git a/src/ast/expression/tests.rs b/src/ast/expression/tests.rs new file mode 100644 index 0000000..69849c8 --- /dev/null +++ b/src/ast/expression/tests.rs @@ -0,0 +1,259 @@ +use super::*; + +#[track_caller] +fn parse_expr(input: &str) -> Expression { + println!("Input: {input}"); + syn::parse_str(input).expect("Failed to parse input") +} + +#[track_caller] +fn assert_ident(expr: Expression, name: &str) { + match expr { + Expression::Ident(ident) if ident == name => {}, + expr => panic!("Expected `{name}`, found `{expr}`") + } +} + +#[track_caller] +fn assert_bool(expr: Expression, value: bool) { + match expr { + Expression::Bool(lit) if lit.value() == value => {}, + expr => panic!("Expected `{value}`, found `{expr}`") + } +} + +#[track_caller] +fn assert_int(expr: Expression, value: isize) { + match expr { + Expression::Literal(lit) + if lit + .base10_parse::() + .map(|lit| lit == value) + .unwrap_or(false) => {}, + expr => panic!("Expected `{value}`, found `{expr}`") + } +} + +#[track_caller] +fn assert_bool_negation(expr: Expression) -> Expression { + match expr { + Expression::BoolNegation { expr, .. } => *expr, + expr => { + panic!("Expected `!{{ .. }}`, found `{expr}`") + } + } +} + +#[track_caller] +fn assert_int_negation(expr: Expression) -> Expression { + match expr { + Expression::IntNegation { expr, .. } => *expr, + expr => { + panic!("Expected `-{{ .. }}`, found `{expr}`") + } + } +} + +#[track_caller] +fn assert_arithmetic(expr: Expression, op_char: char) -> (Expression, Expression) { + match expr { + Expression::Arithmetic { lhs, op, rhs } if op.char() == op_char => (*lhs, *rhs), + expr => panic!("Expected `{{ .. }} {op_char} {{ .. }}`, found `{expr}`") + } +} + +#[track_caller] +fn assert_comparison(expr: Expression, op_str: &str) -> (Expression, Expression) { + match expr { + Expression::Comparison { lhs, op, rhs } if op.str() == op_str => (*lhs, *rhs), + expr => panic!("Expected `{{ .. }} {op_str} {{ .. }}`, found `{expr}`") + } +} + +#[track_caller] +fn assert_conjunction(expr: Expression, op_str: &str) -> (Expression, Expression) { + match expr { + Expression::Conjunction { lhs, op, rhs } if op.str() == op_str => (*lhs, *rhs), + expr => panic!("Expected `{{ .. }} {op_str} {{ .. }}`, found `{expr}`") + } +} + +#[track_caller] +fn assert_tuple_index(expr: Expression, idx: u32) -> Expression { + match expr { + Expression::TupleIndex { expr, index, .. } if index.index == idx => *expr, + expr => panic!("Expected `{{ .. }}.{idx}`, found `{expr}`") + } +} + +#[track_caller] +fn assert_deref(expr: Expression) -> Expression { + match expr { + Expression::Deref { expr, .. } => *expr, + expr => panic!("*{{ .. }}`, found `{expr}`") + } +} + +#[test] +fn parse_true() { + let expr = parse_expr("true"); + assert_bool(expr, true); +} + +#[test] +fn parse_false() { + let expr = parse_expr("false"); + assert_bool(expr, false); +} + +#[test] +fn parse_zero() { + let expr = parse_expr("0"); + assert_int(expr, 0); +} + +#[test] +fn parse_ten() { + let expr = parse_expr("10"); + assert_int(expr, 10); +} + +#[test] +fn parse_minus_ten() { + let expr = parse_expr("-10"); + assert_int(expr, -10); +} + +#[test] +fn parse_not_true() { + let expr = parse_expr("!true"); + let expr = assert_bool_negation(expr); + assert_bool(expr, true); +} + +#[test] +fn parse_minus_minus_ten() { + let expr = parse_expr("--10"); + let expr = assert_int_negation(expr); + assert_int(expr, -10); +} + +#[test] +fn parse_one_plus_two() { + let expr = parse_expr("1 + 2"); + let (lhs, rhs) = assert_arithmetic(expr, '+'); + assert_int(lhs, 1); + assert_int(rhs, 2); +} + +#[test] +fn parse_one_plus_two_times_three() { + let expr = parse_expr("1 + 2 * 3"); + let (lhs, rhs) = assert_arithmetic(expr, '+'); + assert_int(lhs, 1); + let (lhs, rhs) = assert_arithmetic(rhs, '*'); + assert_int(lhs, 2); + assert_int(rhs, 3); +} + +#[test] +fn parse_one_times_two_plus_three_times_four() { + let expr = parse_expr("1 * 2 + 3 * 4"); + let (lhs, rhs) = assert_arithmetic(expr, '+'); + { + let (lhs, rhs) = assert_arithmetic(lhs, '*'); + assert_int(lhs, 1); + assert_int(rhs, 2); + } + { + let (lhs, rhs) = assert_arithmetic(rhs, '*'); + assert_int(lhs, 3); + assert_int(rhs, 4); + } +} + +#[test] +fn parse_one_times_two_eq_three_plus_four() { + let expr = parse_expr("1 * 2 == 3 + 4"); + let (lhs, rhs) = assert_comparison(expr, "=="); + { + let (lhs, rhs) = assert_arithmetic(lhs, '*'); + assert_int(lhs, 1); + assert_int(rhs, 2); + } + { + let (lhs, rhs) = assert_arithmetic(rhs, '+'); + assert_int(lhs, 3); + assert_int(rhs, 4); + } +} + +#[test] +fn parse_true_and_one_plus_two_eq_three_times_four() { + let expr = parse_expr("true && 1 + 2 == 3 * 4"); + let (lhs, rhs) = assert_conjunction(expr, "&&"); + assert_bool(lhs, true); + let (lhs, rhs) = assert_comparison(rhs, "=="); + { + let (lhs, rhs) = assert_arithmetic(lhs, '+'); + assert_int(lhs, 1); + assert_int(rhs, 2); + } + { + let (lhs, rhs) = assert_arithmetic(rhs, '*'); + assert_int(lhs, 3); + assert_int(rhs, 4); + } +} + +#[test] +fn parse_not_true_and_false() { + let expr = parse_expr("!true && false"); + let (lhs, rhs) = assert_conjunction(expr, "&&"); + let lhs = assert_bool_negation(lhs); + assert_bool(lhs, true); + assert_bool(rhs, false); +} + +#[test] +fn parse_not_ident_0_and_false() { + let expr = parse_expr("!foo.0 && false"); + let (lhs, rhs) = assert_conjunction(expr, "&&"); + let lhs = assert_bool_negation(lhs); + let lhs = assert_tuple_index(lhs, 0); + assert_ident(lhs, "foo"); + assert_bool(rhs, false); +} + +#[test] +fn parse_deref_ident() { + let expr = parse_expr("*foo"); + let expr = assert_deref(expr); + assert_ident(expr, "foo"); +} + +#[test] +fn parse_deref_ident_0() { + let expr = parse_expr("*foo.0"); + let expr = assert_deref(expr); + let expr = assert_tuple_index(expr, 0); + assert_ident(expr, "foo"); +} + +#[test] +fn parse_deref_ident_eq_one() { + let expr = parse_expr("*foo == 1"); + let (lhs, rhs) = assert_comparison(expr, "=="); + let lhs = assert_deref(lhs); + assert_ident(lhs, "foo"); + assert_int(rhs, 1); +} + +#[test] +fn parse_zero_eq_foo_0() { + let expr = parse_expr("0 == foo.0"); + let (lhs, rhs) = assert_comparison(expr, "=="); + assert_int(lhs, 0); + let rhs = assert_tuple_index(rhs, 0); + assert_ident(rhs, "foo"); +} diff --git a/src/ast/function.rs b/src/ast/function.rs new file mode 100644 index 0000000..e295ab6 --- /dev/null +++ b/src/ast/function.rs @@ -0,0 +1,54 @@ +use super::{Block, Generics, Type}; +use proc_macro2::Ident; +use syn::{ + parse::{Parse, ParseStream}, + punctuated::Punctuated, + token::Paren, + Token +}; + +#[derive(Clone, Debug)] +pub struct FnArg { + pub ident: Ident, + pub colon_token: Token![:], + pub ty: Type +} + +impl Parse for FnArg { + fn parse(input: ParseStream<'_>) -> syn::Result { + Ok(Self { + ident: input.parse()?, + colon_token: input.parse()?, + ty: input.parse()? + }) + } +} + +#[derive(Clone, Debug)] +pub struct Fn { + pub fn_token: Token![fn], + pub ident: Ident, + pub generics: Option>, + pub paren_token: Paren, + pub inputs: Punctuated, + pub output: Option<(Token![->], Type)>, + pub block: Block +} + +impl Parse for Fn { + fn parse(input: ParseStream<'_>) -> syn::Result { + let content; + Ok(Self { + fn_token: input.parse()?, + ident: input.parse()?, + generics: input.peek(Token![<]).then(|| input.parse()).transpose()?, + paren_token: syn::parenthesized!(content in input), + inputs: Punctuated::parse_terminated(&content)?, + output: input + .peek(Token![->]) + .then(|| syn::Result::Ok((input.parse()?, input.parse()?))) + .transpose()?, + block: input.parse()? + }) + } +} diff --git a/src/ast/generics.rs b/src/ast/generics.rs new file mode 100644 index 0000000..1cbd8dc --- /dev/null +++ b/src/ast/generics.rs @@ -0,0 +1,53 @@ +use std::{ + fmt::{self, Display, Formatter}, + hash::{Hash, Hasher} +}; +use syn::{ + parse::{Parse, ParseStream}, + punctuated::Punctuated, + token::{Comma, Gt, Lt} +}; + +#[derive(Clone, Debug)] +pub struct Generics { + pub lt_token: Lt, + pub params: Punctuated, + pub gt_token: Gt +} + +impl Display for Generics { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + f.write_str("<")?; + for (i, param) in self.params.iter().enumerate() { + if i > 0 { + f.write_str(", ")?; + } + write!(f, "{param}")?; + } + f.write_str(">") + } +} + +impl PartialEq for Generics { + fn eq(&self, other: &Self) -> bool { + self.params == other.params + } +} + +impl Eq for Generics {} + +impl Hash for Generics { + fn hash(&self, state: &mut H) { + self.params.hash(state); + } +} + +impl Parse for Generics { + fn parse(input: ParseStream<'_>) -> syn::Result { + Ok(Self { + lt_token: input.parse()?, + params: Punctuated::parse_separated_nonempty(input)?, + gt_token: input.parse()? + }) + } +} diff --git a/src/ast/input_output.rs b/src/ast/input_output.rs new file mode 100644 index 0000000..a6ab5d4 --- /dev/null +++ b/src/ast/input_output.rs @@ -0,0 +1,94 @@ +use proc_macro2::{Ident, Span}; +use syn::{ + parse::{Parse, ParseStream}, + punctuated::Punctuated, + token::{Bang, Paren, Semi}, + Token +}; + +#[derive(Debug)] +pub struct InputOutputMacro { + pub span: Span, + pub not_token: Bang, + pub paren_token: Paren, + pub content: C, + pub semi_token: Semi +} + +#[derive(Debug)] +pub struct Input { + pub span: Span, + pub not_token: Token![!], + pub paren_token: Paren, + pub ident: Ident, + pub semi_token: Token![;] +} + +impl From> for Input { + fn from(this: InputOutputMacro) -> Self { + Self { + span: this.span, + not_token: this.not_token, + paren_token: this.paren_token, + ident: this.content, + semi_token: this.semi_token + } + } +} + +#[derive(Debug)] +pub struct Output { + pub span: Span, + pub not_token: Token![!], + pub paren_token: Paren, + pub idents: Punctuated, + pub semi_token: Token![;] +} + +impl From>> for Output { + fn from(this: InputOutputMacro>) -> Self { + Self { + span: this.span, + not_token: this.not_token, + paren_token: this.paren_token, + idents: this.content, + semi_token: this.semi_token + } + } +} + +fn parse( + input: ParseStream<'_>, + macro_ident: &str, + content_parser: F +) -> syn::Result> +where + F: FnOnce(ParseStream<'_>) -> syn::Result +{ + let content; + Ok(InputOutputMacro { + span: { + let ident: Ident = input.parse()?; + if ident != macro_ident { + bail!(ident.span() => "Expected `{macro_ident}`") + } + ident.span() + }, + not_token: input.parse()?, + paren_token: syn::parenthesized!(content in input), + content: content_parser(&content)?, + semi_token: input.parse()? + }) +} + +impl Parse for Input { + fn parse(input: ParseStream<'_>) -> syn::Result { + parse(input, "input", Parse::parse).map(Into::into) + } +} + +impl Parse for Output { + fn parse(input: ParseStream<'_>) -> syn::Result { + parse(input, "output", Punctuated::parse_separated_nonempty).map(Into::into) + } +} diff --git a/src/ast/mod.rs b/src/ast/mod.rs new file mode 100644 index 0000000..2517ae1 --- /dev/null +++ b/src/ast/mod.rs @@ -0,0 +1,184 @@ +use crate::verbose; +use proc_macro2::Ident; +use syn::{ + parse::{Parse, ParseStream}, + token::{Brace, Bracket}, + LitFloat, Token +}; + +mod assignment; +mod conditional; +mod declaration; +mod expression; +mod function; +mod generics; +mod input_output; +mod ret; +mod ty; +mod typedef; +mod while_loop; + +pub use assignment::Assignment; +pub use conditional::{Condition, ElseBranch, If}; +pub use declaration::Declaration; +pub use expression::{ArithmeticOp, ComparisonOp, Expression}; +pub use function::{Fn, FnArg}; +pub use generics::Generics; +pub use input_output::{Input, Output}; +pub use ret::Return; +pub use ty::{Type, TypeCustom, TypePredef, TypePrimitive, TypeTuple}; +pub use typedef::TypeDef; +pub use while_loop::{Break, Continue, While}; + +#[derive(Debug)] +pub struct Program { + pub types: Vec, + pub functions: Vec, + pub inputs: Vec, + pub stmts: Vec, + pub outputs: Vec +} + +fn peek_macro(input: ParseStream<'_>, macro_ident: &str) -> bool { + let fork = input.fork(); + let next = fork.parse::(); + let next2 = fork.parse::(); + matches!((next, next2), (Ok(ident), Ok(_)) if ident == macro_ident) +} + +impl Parse for Program { + fn parse(input: ParseStream<'_>) -> syn::Result { + let mut types = Vec::new(); + while input.peek(Token![type]) { + if verbose() { + eprint!("[ast] Parsing typedef ..."); + } + types.push(input.parse()?); + if verbose() { + eprintln!("done"); + } + } + + let mut functions = Vec::new(); + while input.peek(Token![fn]) { + if verbose() { + eprint!("[ast] Parsing function ..."); + } + functions.push(input.parse()?); + if verbose() { + eprintln!("done"); + } + } + + let mut inputs = Vec::new(); + while peek_macro(input, "input") { + if verbose() { + eprint!("[ast] Parsing input!() ..."); + } + inputs.push(input.parse()?); + if verbose() { + eprintln!("done"); + } + } + + let mut stmts = Vec::new(); + while !input.is_empty() && !peek_macro(input, "output") { + if verbose() { + eprint!("[ast] Parsing statement ..."); + } + stmts.push(input.parse()?); + if verbose() { + eprintln!("done"); + } + } + + let mut outputs = Vec::new(); + while !input.is_empty() { + if verbose() { + eprint!("[ast] Parsing output!() ..."); + } + outputs.push(input.parse()?); + if verbose() { + eprintln!("done"); + } + } + + if verbose() { + eprintln!("[ast] Done parsing"); + } + Ok(Self { + types, + functions, + inputs, + stmts, + outputs + }) + } +} + +#[derive(Clone, Debug)] +pub struct Block { + pub stmts: Vec +} + +impl Parse for Block { + fn parse(input: ParseStream<'_>) -> syn::Result { + let content; + syn::braced!(content in input); + let mut stmts = Vec::new(); + while !content.is_empty() { + stmts.push(content.parse()?); + } + Ok(Self { stmts }) + } +} + +#[derive(Clone, Debug)] +pub enum Statement { + Decl(Declaration), + Assign(Assignment), + If(If), + CoinFlip { head: Block, prob: f32, tail: Block }, + While(While), + Block(Block), + Return(Return), + Continue(Continue), + Break(Break) +} + +impl Parse for Statement { + fn parse(input: ParseStream<'_>) -> syn::Result { + Ok(if input.peek(Token![let]) { + Self::Decl(input.parse()?) + } else if input.peek(Token![if]) { + Self::If(input.parse()?) + } else if input.peek(Token![while]) { + Self::While(input.parse()?) + } else if input.peek(Token![return]) { + Self::Return(input.parse()?) + } else if input.peek(Token![continue]) { + Self::Continue(input.parse()?) + } else if input.peek(Token![break]) { + Self::Break(input.parse()?) + } else if input.peek(Brace) { + let block = input.parse()?; + if input.peek(Bracket) { + let content; + syn::bracketed!(content in input); + let prob: LitFloat = content.parse()?; + let tail = input.parse()?; + Self::CoinFlip { + head: block, + prob: prob.base10_parse()?, + tail + } + } else { + Self::Block(block) + } + } else if input.peek(syn::Ident) { + Self::Assign(input.parse()?) + } else { + bail!(input.span() => "Unexpected token"); + }) + } +} diff --git a/src/ast/ret.rs b/src/ast/ret.rs new file mode 100644 index 0000000..0417882 --- /dev/null +++ b/src/ast/ret.rs @@ -0,0 +1,24 @@ +use super::Expression; +use syn::{ + parse::{Parse, ParseStream}, + Token +}; + +#[derive(Clone, Debug)] +pub struct Return { + pub return_token: Token![return], + pub expr: Option, + pub semi_token: Token![;] +} + +impl Parse for Return { + fn parse(input: ParseStream<'_>) -> syn::Result { + Ok(Self { + return_token: input.parse()?, + expr: (!input.peek(Token![;])) + .then(|| input.parse()) + .transpose()?, + semi_token: input.parse()? + }) + } +} diff --git a/src/ast/ty.rs b/src/ast/ty.rs new file mode 100644 index 0000000..fa866bf --- /dev/null +++ b/src/ast/ty.rs @@ -0,0 +1,432 @@ +use super::Generics; +use proc_macro2::{Ident, Span}; +use std::{ + borrow::Cow, + fmt::{self, Display, Formatter, Write as _}, + hash::{Hash, Hasher} +}; +use syn::{ + parse::{Parse, ParseStream}, + punctuated::Punctuated, + spanned::Spanned, + token::Paren, + Token +}; + +#[derive(Clone, Debug)] +pub struct TypePrimitive { + pub span: Span +} + +impl Default for TypePrimitive { + fn default() -> Self { + Self { + span: Span::call_site() + } + } +} + +impl Hash for TypePrimitive { + fn hash(&self, _state: &mut H) {} +} + +impl Spanned for TypePrimitive { + fn span(&self) -> Span { + self.span + } +} + +#[derive(Clone, Debug, Default)] +pub struct TypeTuple { + pub paren_token: Paren, + pub elems: Punctuated +} + +impl PartialEq for TypeTuple { + fn eq(&self, other: &Self) -> bool { + let len = self.elems.len(); + if len != other.elems.len() { + return false; + } + for i in 0 .. len { + if self.elems[i] != other.elems[i] { + return false; + } + } + true + } +} + +impl Hash for TypeTuple { + fn hash(&self, state: &mut H) { + // Punctuated implements Hash by hashing the inner Vec, + // and Token![] types have an empty hash function + self.elems.hash(state); + } +} + +impl Spanned for TypeTuple { + fn span(&self) -> Span { + self.paren_token.span + } +} + +#[derive(Clone, Debug)] +pub struct TypePredef { + pub span: Span, + pub lt_token: Token![<], + pub inner_ty: Box, + pub gt_token: Token![>] +} + +impl PartialEq for TypePredef { + fn eq(&self, other: &Self) -> bool { + self.inner_ty == other.inner_ty + } +} + +impl Hash for TypePredef { + fn hash(&self, state: &mut H) { + self.inner_ty.hash(state); + } +} + +impl Spanned for TypePredef { + fn span(&self) -> Span { + self.span + } +} + +#[derive(Clone, Debug)] +pub struct TypeCustom { + pub ident: Ident, + pub generics: Option> +} + +impl PartialEq for TypeCustom { + fn eq(&self, other: &Self) -> bool { + self.ident == other.ident && self.generics == other.generics + } +} + +impl Hash for TypeCustom { + fn hash(&self, state: &mut H) { + // Ident implements Hash by hashing its string representation + self.ident.hash(state); + self.generics.hash(state); + } +} + +impl Spanned for TypeCustom { + fn span(&self) -> Span { + self.ident.span() + } +} + +#[derive(Clone, Debug, Hash)] +pub enum Type { + Bool(TypePrimitive), + Int(TypePrimitive), + Tuple(TypeTuple), + Rc(TypePredef), + Option(TypePredef), + Custom(TypeCustom), + + /// This type is only used when expanding expressions, it is + /// never parsed from input. + Any +} + +impl Display for Type { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + match self { + Self::Bool(_) => f.write_str("bool"), + Self::Int(_) => f.write_str("int"), + Self::Tuple(tuple) => { + f.write_str("(")?; + for (i, ty) in tuple.elems.iter().enumerate() { + if i > 0 { + f.write_str(" ")?; + } + write!(f, "{ty},")?; + } + f.write_str(")") + }, + Self::Rc(rc) => write!(f, "Rc<{}>", rc.inner_ty), + Self::Option(opt) => write!(f, "Option<{}>", opt.inner_ty), + Self::Custom(custom) => { + write!(f, "{}", custom.ident)?; + if let Some(g) = &custom.generics { + write!(f, "{g}")?; + } + Ok(()) + }, + Self::Any => f.write_str("_") + } + } +} + +impl Type { + /// Returns a mangled string representation of this type. Panics + /// if this type contains `_`. + pub fn mangled(&self) -> Cow<'static, str> { + match self { + Self::Bool(_) => "B".into(), + Self::Int(_) => "I".into(), + Self::Tuple(tuple) => { + let mut buf = format!("T{}", tuple.elems.len()); + for ty in tuple.elems.iter() { + write!(buf, "_{}", ty.mangled()).unwrap(); + } + buf.into() + }, + Self::Rc(rc) => format!("R{}", rc.inner_ty.mangled()).into(), + Self::Option(opt) => format!("O{}", opt.inner_ty.mangled()).into(), + Self::Custom(custom) => { + let ident = custom.ident.to_string(); + let mut buf = format!("C{}_{}", ident.len(), ident); + if let Some(g) = &custom.generics { + write!(buf, "_G{}", g.params.len()).unwrap(); + for param in g.params.iter() { + write!(buf, "_{}", param.mangled()).unwrap() + } + } + buf.into() + }, + Self::Any => panic!("Cannot mangle type containing _") + } + } +} + +impl From for Cow<'_, Type> { + fn from(this: Type) -> Self { + Cow::Owned(this) + } +} + +impl<'a> From<&'a Type> for Cow<'a, Type> { + fn from(this: &'a Type) -> Self { + Cow::Borrowed(this) + } +} + +impl PartialEq for Type { + fn eq(&self, other: &Self) -> bool { + match (self, other) { + (Self::Bool(_), Self::Bool(_)) | (Self::Int(_), Self::Int(_)) => true, + (Self::Tuple(this), Self::Tuple(other)) => this == other, + (Self::Rc(this), Self::Rc(other)) => this == other, + (Self::Option(this), Self::Option(other)) => this == other, + (Self::Custom(this), Self::Custom(other)) => this == other, + (Self::Any, _) | (_, Self::Any) => true, + _ => false + } + } +} + +impl Eq for Type {} + +impl Type { + /// Return the inner type for `Rc` and `Option`. Panics otherwise. + pub fn inner_ty(&self) -> &Type { + match self { + Self::Rc(this) | Self::Option(this) => &this.inner_ty, + _ => panic!("This type does not have an inner type") + } + } + + /// Return the inner type for `Rc` and `Option`. Panics otherwise. + pub fn into_inner_ty(self) -> Type { + match self { + Self::Rc(this) | Self::Option(this) => *this.inner_ty, + _ => panic!("This type does not have an inner type") + } + } + + /// Return the n-th inner type for tuples. Panics if self is not a tuple or + /// if n is out of bounds. + pub fn nth_ty(&self, n: usize) -> &Type { + match self { + Self::Tuple(tuple) => tuple.elems.iter().nth(n).expect("out of bounds"), + _ => panic!("This type is not a tuple") + } + } +} + +impl Parse for Type { + fn parse(input: ParseStream<'_>) -> syn::Result { + if input.peek(Paren) { + let content; + let paren_token = syn::parenthesized!(content in input); + return Ok(Self::Tuple(TypeTuple { + paren_token, + elems: Punctuated::parse_terminated(&content)? + })); + } + + let ident: Ident = input.parse()?; + if ident == "bool" { + return Ok(Self::Bool(TypePrimitive { span: ident.span() })); + } else if ident == "int" { + return Ok(Self::Int(TypePrimitive { span: ident.span() })); + } + + if !input.peek(Token![<]) { + if ident == "Rc" || ident == "Option" { + bail!(ident.span() => "Missing generic type parameter") + } + return Ok(Self::Custom(TypeCustom { + ident, + generics: None + })); + } + + let generics: Generics = input.parse()?; + + if ident == "Rc" || ident == "Option" { + let mut iter = generics.params.into_iter(); + let inner_ty = Box::new(iter.next().unwrap()); + if let Some(p) = iter.next() { + bail!(p.span() => "Too many generic type paramaters"); + } + let predef = TypePredef { + span: ident.span(), + lt_token: generics.lt_token, + inner_ty, + gt_token: generics.gt_token + }; + return Ok(if ident == "Rc" { + Self::Rc(predef) + } else { + Self::Option(predef) + }); + } + + Ok(Self::Custom(TypeCustom { + ident, + generics: Some(generics) + })) + } +} + +impl Spanned for Type { + fn span(&self) -> Span { + match self { + Self::Bool(this) | Self::Int(this) => this.span(), + Self::Tuple(this) => this.span(), + Self::Rc(this) | Self::Option(this) => this.span(), + Self::Custom(this) => this.span(), + Self::Any => Span::call_site() + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn parse_bool() { + let ty: Type = syn::parse_str("bool").unwrap(); + assert!(matches!(ty, Type::Bool(_))); + } + + #[test] + fn parse_int() { + let ty: Type = syn::parse_str("int").unwrap(); + assert!(matches!(ty, Type::Int(_))); + } + + #[test] + fn parse_unit() { + let ty: Type = syn::parse_str("()").unwrap(); + assert!(matches!(ty, Type::Tuple(_))); + match ty { + Type::Tuple(tuple) => assert!(tuple.elems.is_empty()), + _ => unreachable!() + } + } + + #[test] + fn parse_tuple_1() { + let ty: Type = syn::parse_str("(int)").unwrap(); + assert!(matches!(ty, Type::Tuple(_))); + match ty { + Type::Tuple(tuple) => { + let mut iter = tuple.elems.into_iter(); + assert!(matches!(iter.next(), Some(Type::Int(_)))); + assert!(iter.next().is_none()); + }, + _ => unreachable!() + } + } + + #[test] + fn parse_tuple_2() { + let ty: Type = syn::parse_str("(int, Option)").unwrap(); + assert!(matches!(ty, Type::Tuple(_))); + match ty { + Type::Tuple(tuple) => { + let mut iter = tuple.elems.into_iter(); + assert!(matches!(iter.next(), Some(Type::Int(_)))); + assert!(matches!(iter.next(), Some(Type::Option(_)))); + assert!(iter.next().is_none()); + }, + _ => unreachable!() + } + } + + #[test] + fn parse_rc() { + let ty: Type = syn::parse_str("Rc").unwrap(); + assert!(matches!(ty, Type::Rc(_))); + match ty { + Type::Rc(rc) => assert!(matches!(*rc.inner_ty, Type::Int(_))), + _ => unreachable!() + } + } + + #[test] + fn parse_option() { + let ty: Type = syn::parse_str("Option").unwrap(); + assert!(matches!(ty, Type::Option(_))); + match ty { + Type::Option(option) => assert!(matches!(*option.inner_ty, Type::Int(_))), + _ => unreachable!() + } + } + + #[test] + fn parse_custom_without_generics() { + let ty: Type = syn::parse_str("Foo").unwrap(); + assert!(matches!(ty, Type::Custom(_))); + match ty { + Type::Custom(custom) => { + assert_eq!(custom.ident, "Foo"); + assert!(custom.generics.is_none()); + }, + _ => unreachable!() + } + } + + #[test] + fn parse_custom_with_generics() { + let ty: Type = syn::parse_str("Foo").unwrap(); + assert!(matches!(ty, Type::Custom(_))); + match ty { + Type::Custom(custom) => { + assert_eq!(custom.ident, "Foo"); + assert!(custom.generics.is_some()); + match custom.generics { + Some(generics) => { + let mut iter = generics.params.into_iter(); + assert!(matches!(iter.next(), Some(Type::Int(_)))); + assert!(iter.next().is_none()); + }, + None => unreachable!() + } + }, + _ => unreachable!() + } + } +} diff --git a/src/ast/typedef.rs b/src/ast/typedef.rs new file mode 100644 index 0000000..60a9620 --- /dev/null +++ b/src/ast/typedef.rs @@ -0,0 +1,44 @@ +use super::{Generics, Type}; +use proc_macro2::{Ident, Span}; +use syn::{ + parse::{Parse, ParseStream}, + spanned::Spanned, + Token +}; + +#[derive(Debug)] +pub struct TypeDef { + pub type_token: Token![type], + pub ident: Ident, + pub generics: Option>, + pub eq_token: Token![=], + pub ty: Type, + pub semi_token: Token![;] +} + +impl Parse for TypeDef { + fn parse(input: ParseStream<'_>) -> syn::Result { + let type_token = input.parse()?; + let ident = input.parse()?; + + let mut generics = None; + if input.peek(Token![<]) { + generics = Some(input.parse()?); + } + + Ok(Self { + type_token, + ident, + generics, + eq_token: input.parse()?, + ty: input.parse()?, + semi_token: input.parse()? + }) + } +} + +impl Spanned for TypeDef { + fn span(&self) -> Span { + self.ident.span() + } +} diff --git a/src/ast/while_loop.rs b/src/ast/while_loop.rs new file mode 100644 index 0000000..07675bd --- /dev/null +++ b/src/ast/while_loop.rs @@ -0,0 +1,52 @@ +use super::{Block, Condition}; +use syn::{ + parse::{Parse, ParseStream}, + Token +}; + +#[derive(Clone, Debug)] +pub struct While { + pub while_token: Token![while], + pub condition: Condition, + pub loop_body: Block +} + +impl Parse for While { + fn parse(input: ParseStream<'_>) -> syn::Result { + Ok(Self { + while_token: input.parse()?, + condition: input.parse()?, + loop_body: input.parse()? + }) + } +} + +#[derive(Clone, Debug)] +pub struct Continue { + pub continue_token: Token![continue], + pub semi_token: Token![;] +} + +impl Parse for Continue { + fn parse(input: ParseStream<'_>) -> syn::Result { + Ok(Self { + continue_token: input.parse()?, + semi_token: input.parse()? + }) + } +} + +#[derive(Clone, Debug)] +pub struct Break { + pub break_token: Token![break], + pub semi_token: Token![;] +} + +impl Parse for Break { + fn parse(input: ParseStream<'_>) -> syn::Result { + Ok(Self { + break_token: input.parse()?, + semi_token: input.parse()? + }) + } +} diff --git a/src/compile/assign.rs b/src/compile/assign.rs new file mode 100644 index 0000000..99c8cea --- /dev/null +++ b/src/compile/assign.rs @@ -0,0 +1,100 @@ +use super::{expr::expand_expr_with_type, resolve_ty::resolve_tycustom, State}; +use crate::{ + ast::{Assignment, Expression, Type}, + diagnostic::Diagnostic, + verbose +}; +use std::{borrow::Cow, iter::Peekable}; +use syn::{spanned::Spanned, Member}; + +fn cont( + state: &State<'_>, + current_ty: &Type, + mut remaining: Peekable, + expr: Expression +) -> Result<(Cow<'static, str>, Cow<'static, str>), Diagnostic> +where + I: Iterator +{ + if remaining.peek().is_none() { + if verbose() { + eprintln!("[assign] expanding assignment with type {current_ty} and expression {expr}"); + } + let expr = expand_expr_with_type(state, current_ty, expr)?; + return Ok((" = __temp_value;".into(), expr)); + } + + Ok(match current_ty { + Type::Bool(_) => { + return Err(Diagnostic::no_such_field( + remaining.next().unwrap().span(), + "bool is a primitive type and does not have any fields" + )); + }, + + Type::Int(_) => { + return Err(Diagnostic::no_such_field( + remaining.next().unwrap().span(), + "int is a primitive type and does not have any fields" + )); + }, + + Type::Tuple(tuple) => { + let index = match remaining.next().unwrap() { + Member::Named(named) => { + return Err(Diagnostic::no_such_field( + named.span(), + "tuples don't have named fields" + ) + .with_help("To access the first element in a tuple, use `.0`")); + }, + Member::Unnamed(index) if index.index as usize >= tuple.elems.len() => { + return Err(Diagnostic::no_such_field( + index.span(), + "this tuple does not have enough elements" + ) + .with_note("The first element in a tuple has index 0")); + }, + Member::Unnamed(index) => index.index as usize + }; + let (path, expr) = cont(state, current_ty.nth_ty(index), remaining, expr)?; + (format!(".{index}{path}").into(), expr) + }, + + Type::Rc(rc) => { + let (path, expr) = cont(state, &rc.inner_ty, remaining, expr)?; + ( + format!(".with_mut(|__temp| {{ __temp{path} }});").into(), + expr + ) + }, + + Type::Option(_) => { + return Err(Diagnostic::no_such_field( + remaining.next().unwrap().span(), + "Option does not have any fields" + )); + }, + + Type::Custom(custom) => { + let ty = resolve_tycustom(state, custom)?; + return cont(state, &ty, remaining, expr); + }, + + Type::Any => unimplemented!() + }) +} + +pub(super) fn expand_assign( + state: &State<'_>, + assign: Assignment +) -> Result { + let mut lhs = assign.lhs.into_iter().peekable(); + let first = match lhs.next().unwrap() { + Member::Named(ident) => ident, + Member::Unnamed(_) => unreachable!() + }; + let first_ty = state.var(&first)?; + let (path, expr) = cont(state, first_ty, lhs, assign.rhs)?; + Ok(format!("{{ let __temp_value = {expr}; {first}{path} }}",)) +} diff --git a/src/compile/conditional.rs b/src/compile/conditional.rs new file mode 100644 index 0000000..6be5021 --- /dev/null +++ b/src/compile/conditional.rs @@ -0,0 +1,83 @@ +use super::{ + expand_block, + expect_ty::expect_option, + expr::{expand_expr, expand_expr_with_type}, + State +}; +use crate::{ + ast::{Condition, ElseBranch, If, Type, TypePrimitive, While}, + diagnostic::Diagnostic +}; +use proc_macro2::Span; +use std::borrow::Cow; +use syn::Token; + +pub(super) fn expand_condition( + state: &mut State<'_>, + condition: Condition, + if_span: Span +) -> Result, Diagnostic> { + Ok(match condition { + Condition::Bool(expr) => expand_expr_with_type( + state, + &Type::Bool(TypePrimitive { span: if_span }), + expr + )?, + Condition::LetSome { + some_span, + ident, + expr, + .. + } => { + let expr = expand_expr(state, expr)?; + expect_option(&expr.ty, expr.expr_span, some_span)?; + let buf = format!("let ::stdlib::Option::Some(mut {ident}) = {}", expr.code); + let expr_ty = expr.ty.into_owned(); + state.add_variable(ident, expr_ty.into_inner_ty()); + buf.into() + } + }) +} + +fn expand_else_branch( + state: &State<'_>, + else_branch: Option<(Token![else], ElseBranch)> +) -> Result { + let mut buf = String::new(); + if let Some((_, else_branch)) = else_branch { + buf += " else "; + match else_branch { + ElseBranch::If(else_if) => { + buf += "{"; + buf += &expand_if(state, *else_if)?; + buf += "}"; + }, + ElseBranch::Block(block) => { + buf += &expand_block(state, block)?; + } + } + } + Ok(buf) +} + +pub(super) fn expand_if(state: &State<'_>, if_expr: If) -> Result { + let mut sub = state.substate(); + let condition = expand_condition(&mut sub, if_expr.condition, if_expr.if_token.span)?; + let block = expand_block(&sub, if_expr.then_branch)?; + + let mut buf = format!("if {condition} {block}"); + buf += &expand_else_branch(state, if_expr.else_branch)?; + Ok(buf) +} + +pub(super) fn expand_while( + state: &State<'_>, + while_expr: While +) -> Result { + let mut sub = state.substate().into_loop(); + let condition = + expand_condition(&mut sub, while_expr.condition, while_expr.while_token.span)?; + let block = expand_block(&sub, while_expr.loop_body)?; + + Ok(format!("while {condition} {block}")) +} diff --git a/src/compile/decl.rs b/src/compile/decl.rs new file mode 100644 index 0000000..dd09009 --- /dev/null +++ b/src/compile/decl.rs @@ -0,0 +1,19 @@ +use super::{ + expr::expand_expr_with_type, resolve_ty::resolve_ty, ty::expand_type, State +}; +use crate::{ast::Declaration, diagnostic::Diagnostic}; + +pub(super) fn expand_decl( + state: &mut State<'_>, + decl: Declaration +) -> Result { + let expr = expand_expr_with_type(state, &decl.ty, decl.expr)?; + let ident = decl.ident; + let ty = expand_type(state, &decl.ty)?; + let buf = format!("let mut {ident}: {ty} = {expr};"); + state.add_variable( + ident, + resolve_ty(&decl.ty, state.generic_types()).into_owned() + ); + Ok(buf) +} diff --git a/src/compile/expect_ty.rs b/src/compile/expect_ty.rs new file mode 100644 index 0000000..9245c8d --- /dev/null +++ b/src/compile/expect_ty.rs @@ -0,0 +1,157 @@ +use crate::{ast::Type, diagnostic::Diagnostic}; +use proc_macro2::Span; +use syn::spanned::Spanned as _; + +pub(super) fn expect_ty( + expected_ty: &Type, + expression_ty: &Type, + expression_span: Span +) -> Result<(), Diagnostic> { + if expected_ty == expression_ty { + Ok(()) + } else { + Err(Diagnostic::type_mismatch( + expected_ty, + expected_ty.span(), + expression_ty, + expression_span + )) + } +} + +pub(super) fn expect_bool( + expr_ty: &Type, + expr_span: Span, + why_span: Span +) -> Result<(), Diagnostic> { + match expr_ty { + Type::Bool(_) => Ok(()), + _ => Err(Diagnostic::type_mismatch( + "bool", why_span, expr_ty, expr_span + )) + } +} + +pub(super) fn expect_int( + expr_ty: &Type, + expr_span: Span, + why_span: Span +) -> Result<(), Diagnostic> { + match expr_ty { + Type::Int(_) => Ok(()), + _ => Err(Diagnostic::type_mismatch( + "int", why_span, expr_ty, expr_span + )) + } +} + +pub(super) fn expect_tuple_exact( + ty: &Type, + elems_len: usize, + expr_span: Span, + why_span: Span +) -> Result<(), Diagnostic> { + match ty { + Type::Tuple(tuple) if tuple.elems.len() == elems_len => Ok(()), + Type::Tuple(tuple) => Err(Diagnostic::new( + expr_span, + "Type Mismatch", + format!("but expression has {elems_len} elements") + ) + .with_label( + ty.span(), + format!("Expected tuple with {} elements,", tuple.elems.len()) + )), + _ => Err(Diagnostic::type_mismatch( + ty, + expr_span, + format!("({})", ", ".repeat(elems_len)), + why_span + )) + } +} + +pub(super) fn expect_tuple_at_least( + ty: &Type, + elems_len: usize, + expr_span: Span, + why_span: Span +) -> Result<(), Diagnostic> { + match ty { + Type::Tuple(tuple) if tuple.elems.len() >= elems_len => Ok(()), + Type::Tuple(tuple) => Err(Diagnostic::new( + expr_span, + "Type Mismatch", + format!("but expression requires at least {elems_len} elements") + ) + .with_label( + ty.span(), + format!("Found tuple with {} elements,", tuple.elems.len()) + )), + _ => Err(Diagnostic::type_mismatch( + format!("({}..)", "_, ".repeat(elems_len)), + why_span, + ty, + expr_span + )) + } +} + +pub(super) fn expect_rc( + expr_ty: &Type, + expr_span: Span, + why_span: Span +) -> Result<(), Diagnostic> { + match expr_ty { + Type::Rc(_) => Ok(()), + _ => Err(Diagnostic::type_mismatch( + "Rc<_>", why_span, expr_ty, expr_span + )) + } +} + +pub(super) fn expect_option( + expr_ty: &Type, + expr_span: Span, + why_span: Span +) -> Result<(), Diagnostic> { + match expr_ty { + Type::Option(_) => Ok(()), + _ => Err(Diagnostic::type_mismatch( + "Option<_>", + why_span, + expr_ty, + expr_span + )) + } +} + +pub(super) fn expect_rc_ctor( + expected_ty: &Type, + expr_span: Span +) -> Result<(), Diagnostic> { + match expected_ty { + Type::Rc(_) => Ok(()), + _ => Err(Diagnostic::type_mismatch( + expected_ty, + expected_ty.span(), + "Rc<_>", + expr_span + )) + } +} + +pub(super) fn expect_option_ctor( + expected_ty: &Type, + expr_span: Span +) -> Result<(), Diagnostic> { + match expected_ty { + Type::Option(_) => Ok(()), + _ => Err(Diagnostic::type_mismatch( + expected_ty, + expected_ty.span(), + "Option<_>", + expr_span + )) + } +} diff --git a/src/compile/expr.rs b/src/compile/expr.rs new file mode 100644 index 0000000..94863e2 --- /dev/null +++ b/src/compile/expr.rs @@ -0,0 +1,304 @@ +use super::{ + expect_ty::{ + expect_bool, expect_int, expect_option_ctor, expect_rc, expect_rc_ctor, + expect_tuple_at_least, expect_tuple_exact, expect_ty + }, + resolve_ty::{resolve_ty, resolve_tycustom}, + State +}; +use crate::{ + ast::{Expression, Type, TypePredef, TypeTuple}, + diagnostic::Diagnostic +}; +use proc_macro2::Span; +use std::borrow::Cow; +use syn::{punctuated::Punctuated, spanned::Spanned}; + +pub(super) struct ExpandedExpression<'a> { + pub(super) code: Cow<'static, str>, + pub(super) expr_span: Span, + pub(super) ty: Cow<'a, Type> +} + +impl<'a, C, S, T> From<(C, S, T)> for ExpandedExpression<'a> +where + C: Into>, + S: Spanned, + T: Into> +{ + fn from((code, expr_span, ty): (C, S, T)) -> Self { + Self { + code: code.into(), + expr_span: expr_span.span(), + ty: ty.into() + } + } +} + +pub(super) fn expand_expr<'a>( + state: &'a State<'_>, + expr: Expression +) -> Result, Diagnostic> { + let span = expr.span(); + let expanded: ExpandedExpression<'a> = match expr { + Expression::Term { expr, .. } => { + let expr = expand_expr(state, *expr)?; + (format!("({})", expr.code), span, expr.ty).into() + }, + + Expression::Ident(ident) => ( + format!("::core::clone::Clone::clone(&{ident})"), + span, + state.var(&ident)? + ) + .into(), + + Expression::Bool(lit) => { + let code = match lit.value { + true => "true", + false => "false" + }; + (code, span, Type::Bool(Default::default())).into() + }, + + Expression::Literal(lit) => { + (lit.to_string(), span, Type::Int(Default::default())).into() + }, + + Expression::BoolNegation { not_token, expr } => { + let bool = Type::Bool(Default::default()); + let expr = expand_expr(state, *expr)?; + expect_bool(&expr.ty, expr.expr_span, not_token.span)?; + (format!("!({})", expr.code), span, bool).into() + }, + + Expression::IntNegation { minus_token, expr } => { + let int = Type::Int(Default::default()); + let expr = expand_expr(state, *expr)?; + expect_int(&expr.ty, expr.expr_span, minus_token.span)?; + (format!("-({})", expr.code), span, int).into() + }, + + Expression::Arithmetic { lhs, op, rhs } => { + let int = Type::Int(Default::default()); + let lhs = expand_expr(state, *lhs)?; + expect_int(&lhs.ty, lhs.expr_span, op.span())?; + let rhs = expand_expr(state, *rhs)?; + expect_int(&rhs.ty, rhs.expr_span, op.span())?; + (format!("{} {op} {}", lhs.code, rhs.code), span, int).into() + }, + + Expression::Comparison { lhs, op, rhs } => { + let bool = Type::Bool(Default::default()); + let lhs = expand_expr(state, *lhs)?; + expect_int(&lhs.ty, lhs.expr_span, op.span())?; + let rhs = expand_expr(state, *rhs)?; + expect_int(&rhs.ty, rhs.expr_span, op.span())?; + (format!("{} {op} {}", lhs.code, rhs.code), span, bool).into() + }, + + Expression::Conjunction { lhs, op, rhs } => { + let bool = Type::Bool(Default::default()); + let lhs = expand_expr(state, *lhs)?; + expect_bool(&lhs.ty, lhs.expr_span, op.span())?; + let rhs = expand_expr(state, *rhs)?; + expect_bool(&rhs.ty, rhs.expr_span, op.span())?; + (format!("{} {op} {}", lhs.code, rhs.code), span, bool).into() + }, + + Expression::TupleCtor { paren_token, elems } => { + let mut ty_elems = Punctuated::new(); + let mut buf = "(".to_owned(); + for v in elems.into_iter() { + let elem = expand_expr(state, v)?; + buf += &elem.code; + buf += ", "; + ty_elems.push(elem.ty.into_owned()); + } + buf += ")"; + let tuple = Type::Tuple(TypeTuple { + paren_token, + elems: ty_elems + }); + (buf, span, tuple).into() + }, + + Expression::TupleIndex { expr, index, .. } => { + let left = expand_expr(state, *expr)?; + let idx = index.index as usize; + expect_tuple_at_least(&left.ty, idx + 1, span, index.span())?; + ( + format!("{}.{}", left.code, index.index), + span, + resolve_ty(left.ty.nth_ty(idx), state.generic_types()).into_owned() + ) + .into() + }, + + Expression::RcCtor { span, expr, .. } => { + let inner = expand_expr(state, *expr)?; + let rc = Type::Rc(TypePredef { + span, + lt_token: Default::default(), + inner_ty: Box::new( + resolve_ty(&inner.ty, state.generic_types()).into_owned() + ), + gt_token: Default::default() + }); + (format!("::stdlib::Rc::new({})", inner.code), span, rc).into() + }, + + Expression::OptionCtor { span, expr: None } => { + let option = Type::Option(TypePredef { + span, + lt_token: Default::default(), + inner_ty: Box::new(Type::Any), + gt_token: Default::default() + }); + ("::stdlib::Option::None", span, option).into() + }, + + Expression::OptionCtor { + span, + expr: Some((_, expr)) + } => { + let inner = expand_expr(state, *expr)?; + let option = Type::Option(TypePredef { + span, + lt_token: Default::default(), + inner_ty: Box::new( + resolve_ty(&inner.ty, state.generic_types()).into_owned() + ), + gt_token: Default::default() + }); + ( + format!("::stdlib::Option::Some({})", inner.code), + span, + option + ) + .into() + }, + + Expression::Deref { star_token, expr } => { + let rc = expand_expr(state, *expr)?; + expect_rc(&rc.ty, rc.expr_span, star_token.span)?; + ( + format!("::stdlib::Rc::into_inner({})", rc.code), + span, + rc.ty.inner_ty().to_owned() + ) + .into() + }, + + Expression::FnCall { + ident, + generics, + inputs, + .. + } => { + let fun = state.fun(&ident, &generics)?; + + let fun_inputs_len = fun.inputs_len(); + let inputs_len = inputs.len(); + if fun_inputs_len > inputs_len { + return Err(Diagnostic::new( + ident.span(), + "Too few arguments", + format!("but only {inputs_len} arguments were supplied") + ) + .with_label( + fun.span(), + format!("This function takes {fun_inputs_len} arguments,") + )); + } + + let mut buf = format!("{}(", fun.ident); + for (input_ty, input_expr) in fun.inputs().zip(inputs.iter()) { + let resolved_ty = resolve_ty(&input_ty, state.generic_types()); + buf += &expand_expr_with_type(state, &resolved_ty, input_expr.clone())?; + buf += ", "; + } + buf += ")"; + + (buf, span, fun.output().into_owned()).into() + } + }; + + let expanded_ty = resolve_ty(&expanded.ty, state.generic_types()); + let expanded_ty: &Type = &expanded_ty; + Ok(if matches!(expanded_ty, Type::Custom(_)) { + let mut ty = expanded.ty.into_owned(); + let mut buf = expanded.code.into_owned(); + while let Type::Custom(custom) = ty { + buf = format!("{}::into_inner({buf})", custom.ident); + ty = resolve_tycustom(state, &custom)?.into_owned(); + } + (buf, expanded.expr_span, ty).into() + } else { + expanded + }) +} + +pub(super) fn expand_expr_with_type( + state: &State<'_>, + ty: &Type, + expr: Expression +) -> Result, Diagnostic> { + let ty = resolve_ty(ty, state.generic_types()); + let ty: &Type = &ty; + + // special case: custom outer type, requires wrapping in new + if let Type::Custom(custom) = ty { + let inner_ty = resolve_tycustom(state, custom)?; + let ident = &custom.ident; + let expr = expand_expr_with_type(state, &inner_ty, expr)?; + return Ok(format!("{ident}::new({expr})").into()); + } + + Ok(match expr { + // special case: possibly custom type(s) inside a term + Expression::Term { expr, .. } => { + let expr = expand_expr_with_type(state, ty, *expr)?; + format!("({expr})").into() + }, + + // special case: possibly custom type(s) inside a tuple + Expression::TupleCtor { + paren_token, elems, .. + } => { + expect_tuple_exact(ty, elems.len(), paren_token.span, ty.span())?; + let mut buf = "(".to_owned(); + for (n, v) in elems.into_iter().enumerate() { + let elem = expand_expr_with_type(state, ty.nth_ty(n), v)?; + buf += &elem; + buf += ", "; + } + buf += ")"; + buf.into() + }, + + // special case: possibly custom type inside an Rc + Expression::RcCtor { span, expr, .. } => { + expect_rc_ctor(ty, span)?; + let inner = expand_expr_with_type(state, ty.inner_ty(), *expr)?; + format!("::stdlib::Rc::new({inner})").into() + }, + + // special case: possibly custom type inside a Some + Expression::OptionCtor { + span, + expr: Some((_, expr)) + } => { + expect_option_ctor(ty, span)?; + let inner = expand_expr_with_type(state, ty.inner_ty(), *expr)?; + format!("::stdlib::Option::Some({inner})").into() + }, + + // otherwise, just expand the expression and expect its type + _ => { + let expr = expand_expr(state, expr)?; + expect_ty(ty, &expr.ty, expr.expr_span)?; + expr.code + } + }) +} diff --git a/src/compile/function.rs b/src/compile/function.rs new file mode 100644 index 0000000..ac50c53 --- /dev/null +++ b/src/compile/function.rs @@ -0,0 +1,66 @@ +use super::{expand_stmt, resolve_ty::resolve_ty, ty::expand_type, State}; +use crate::{ + ast::{Fn, Generics, Type, TypeTuple}, + diagnostic::Diagnostic +}; +use std::fmt::Write as _; + +pub(super) fn expand_function( + buf: &mut String, + state: &State<'_>, + fun: &Fn, + generics: Option>, + ident: String +) -> Result<(), Diagnostic> { + let mut sub = state.substate(); + if let Some((fun_generics, inst_generics)) = fun + .generics + .as_ref() + .and_then(|fg| generics.as_ref().map(|ig| (fg, ig))) + { + for (ident, ty) in fun_generics.params.iter().zip(inst_generics.params.iter()) { + sub.add_ty(ident.clone(), ty.clone()); + } + } + + // function signature + *buf += "#[allow(non_snake_case)]\n"; + write!(buf, "fn {ident}").unwrap(); + *buf += "("; + for arg in &fun.inputs { + write!(buf, "mut {}: {}, ", arg.ident, expand_type(&sub, &arg.ty)?).unwrap(); + sub.add_variable( + arg.ident.clone(), + resolve_ty(&arg.ty, sub.generic_types()).into_owned() + ); + } + *buf += ")"; + if let Some((_, out)) = fun.output.as_ref() { + write!(buf, " -> {}", expand_type(&sub, out)?).unwrap(); + sub.set_return_ty(out.clone()); + } else { + sub.set_return_ty(Type::Tuple(TypeTuple::default())) + } + + // function body + *buf += " {\n"; + for s in &fun.block.stmts { + *buf += &expand_stmt(&mut sub, s.clone())?; + } + if fun.output.is_some() { + *buf += "\n#[allow(unreachable_code)]\n{\n"; + write!(buf, "::stdlib::fn_no_return({:?})", fun.ident.to_string()).unwrap(); + for arg in &fun.inputs { + write!( + buf, + ".add_input(::core::stringify!({ident}), &{ident})", + ident = arg.ident + ) + .unwrap(); + } + *buf += ".panic()\n}\n"; + } + *buf += "}\n"; + + Ok(()) +} diff --git a/src/compile/mod.rs b/src/compile/mod.rs new file mode 100644 index 0000000..027f610 --- /dev/null +++ b/src/compile/mod.rs @@ -0,0 +1,276 @@ +use crate::{ + ast::{Block, Expression, Program, Return, Statement, Type}, + diagnostic::Diagnostic, + verbose +}; +use askama::Template; +use std::{borrow::Cow, fmt::Write as _}; + +mod assign; +mod conditional; +mod decl; +mod expect_ty; +mod expr; +mod function; +mod resolve_ty; +mod state; +mod ty; + +use assign::expand_assign; +use conditional::{expand_if, expand_while}; +use decl::expand_decl; +use expect_ty::expect_tuple_exact; +use expr::expand_expr_with_type; +use function::expand_function; +use state::State; +use ty::expand_type; + +pub enum CompileResult { + Ok { + main_rs: syn::File, + funs_rs: syn::File, + types_rs: syn::File + }, + InternalErr(String, syn::Error) +} + +mod tpl { + use crate::ast::{Generics, Output}; + use askama::Template; + use proc_macro2::Ident; + use std::borrow::Cow; + + mod filters { + use std::fmt::Display; + pub fn if_some(opt: &Option) -> askama::Result { + Ok(if let Some(value) = opt.as_ref() { + value.to_string() + } else { + String::new() + }) + } + } + + #[derive(Template)] + #[template(path = "main.rs.j2", escape = "none")] + pub(super) struct MainRs<'a> { + pub(super) inputs: Vec>, + pub(super) outputs: &'a [Output] + } + + #[derive(Template)] + #[template(path = "types.rs.j2", escape = "none")] + pub(super) struct TypesRs<'a> { + pub(super) typedefs: Vec> + } + + pub(super) struct TypeDef<'a> { + pub(super) ident: &'a Ident, + pub(super) generics: Option<&'a Generics>, + pub(super) inner_ty: Cow<'static, str> + } + + impl<'a> TypeDef<'a> { + pub(super) fn new( + ident: &'a Ident, + generics: Option<&'a Generics>, + inner_ty: Cow<'static, str> + ) -> Self { + Self { + ident, + generics, + inner_ty + } + } + } + + pub(super) struct Input<'a> { + pub(super) ident: &'a Ident, + short_long: &'static str, + quote: char + } + + impl<'a> Input<'a> { + pub(super) fn new(ident: &'a Ident) -> Self { + let (short_long, quote) = match ident.to_string() { + str if str.len() == 1 && str != "h" && str != "n" && str != "v" => { + ("short", '\'') + }, + _ => ("long", '"') + }; + Self { + ident, + short_long, + quote + } + } + } +} + +#[allow(clippy::write_with_newline)] +pub fn compile(input: &str) -> Result { + let program: Program = syn::parse_str(input)?; + let mut state = State::new(program.types, program.functions); + + let main_rs = tpl::MainRs { + inputs: program + .inputs + .iter() + .map(|arg| tpl::Input::new(&arg.ident)) + .collect(), + outputs: &program.outputs + }; + let main_rs = main_rs.render().unwrap(); + + let types_rs = tpl::TypesRs { + typedefs: state + .types() + .into_iter() + .map(|ty| { + Ok(tpl::TypeDef::new( + &ty.ident, + ty.generics.as_ref(), + expand_type(&state, &ty.ty)? + )) + }) + .collect::, Diagnostic>>()? + }; + let types_rs = types_rs.render().unwrap(); + + let mut buf = String::new(); + buf += "#![no_implicit_prelude]\n"; + buf += "#![allow(unused_mut, while_true)]\n\n"; + buf += "use crate::types::*;\n\n"; + + buf += "pub(crate) fn run(crate::Values {\n"; + for input in program.inputs { + write!(buf, " mut {},\n", input.ident).unwrap(); + state.add_variable(input.ident, Type::Int(Default::default())); + } + buf += "}: crate::Values) -> crate::Output {\n"; + for s in program.stmts { + buf += " "; + buf += &expand_stmt(&mut state, s)?; + buf += "\n"; + } + buf += " crate::Output {\n"; + for output in program.outputs { + for ident in output.idents { + write!(buf, " {ident}: ").unwrap(); + buf += &expand_expr_with_type( + &state, + &Type::Int(Default::default()), + Expression::Ident(ident) + )?; + buf += ",\n"; + } + } + buf += " }\n"; + buf += "}\n\n"; + + while let Some(idx) = state.next_function_instance() { + state.with_function_instance(idx, |fun, generics, ident| { + if verbose() { + eprint!("[compile] Expanding function instance {ident} ..."); + } + let res = expand_function(&mut buf, &state, fun, generics, ident); + if verbose() { + eprintln!("done"); + } + res + })?; + } + + Ok(CompileResult::Ok { + main_rs: match syn::parse_file(&main_rs) { + Ok(main_rs) => main_rs, + Err(err) => return Ok(CompileResult::InternalErr(main_rs, err)) + }, + funs_rs: match syn::parse_file(&buf) { + Ok(funs_rs) => funs_rs, + Err(err) => return Ok(CompileResult::InternalErr(buf, err)) + }, + types_rs: match syn::parse_file(&types_rs) { + Ok(types_rs) => types_rs, + Err(err) => return Ok(CompileResult::InternalErr(types_rs, err)) + } + }) +} + +fn expand_stmt( + state: &mut State<'_>, + stmt: Statement +) -> Result, Diagnostic> { + Ok(match stmt { + Statement::Decl(decl) => expand_decl(state, decl)?.into(), + Statement::Assign(assign) => expand_assign(state, assign)?.into(), + Statement::If(if_expr) => expand_if(state, if_expr)?.into(), + Statement::CoinFlip { head, prob, tail } => { + let mut buf = format!("match ::stdlib::Coin::flip({}) {{", prob); + buf += "::stdlib::Coin::Head => "; + buf += &expand_block(state, head)?; + buf += ", ::stdlib::Coin::Tail => "; + buf += &expand_block(state, tail)?; + buf += "}"; + buf.into() + }, + Statement::While(while_expr) => expand_while(state, while_expr)?.into(), + Statement::Block(block) => expand_block(state, block)?.into(), + Statement::Return(Return { + return_token, expr, .. + }) => { + let return_ty = state.return_ty().ok_or_else(|| { + Diagnostic::new( + return_token.span, + "Cannot return", + "Return statements may only be used in function bodies" + ) + })?; + match expr { + Some(expr) => { + format!("return {};", expand_expr_with_type(state, return_ty, expr)?) + .into() + }, + None => { + expect_tuple_exact( + return_ty, + 0, + return_token.span, + return_token.span + )?; + "return;".into() + } + } + }, + Statement::Continue(cont) => { + if !state.is_loop() { + return Err(Diagnostic::new( + cont.continue_token.span, + "Not a loop", + "You may only use `continue` in a loop body" + )); + } + "continue;".into() + }, + Statement::Break(brake) => { + if !state.is_loop() { + return Err(Diagnostic::new( + brake.break_token.span, + "Not a loop", + "You may only use `break` in a loop body" + )); + } + "break;".into() + } + }) +} + +fn expand_block(state: &State<'_>, block: Block) -> Result { + let mut sub = state.substate(); + let mut buf = "{".to_owned(); + for s in block.stmts { + buf += &expand_stmt(&mut sub, s)?; + } + buf += "}"; + Ok(buf) +} diff --git a/src/compile/resolve_ty.rs b/src/compile/resolve_ty.rs new file mode 100644 index 0000000..613255a --- /dev/null +++ b/src/compile/resolve_ty.rs @@ -0,0 +1,126 @@ +use super::State; +use crate::{ + ast::{Generics, Type, TypeCustom, TypePredef, TypeTuple}, + diagnostic::Diagnostic +}; +use proc_macro2::Ident; +use std::{borrow::Cow, collections::HashMap}; + +pub(super) trait GenericsMap { + type Out; + + fn get(&self, ident: &Ident) -> Option; +} + +impl<'a> GenericsMap for (&Generics, &'a Generics) { + type Out = &'a Type; + + fn get(&self, ident: &Ident) -> Option { + self.0 + .params + .iter() + .position(|param| param == ident) + .map(|index| self.1.params.iter().nth(index).unwrap()) + } +} + +impl<'a> GenericsMap for &'a HashMap { + type Out = &'a Type; + + fn get(&self, ident: &Ident) -> Option { + HashMap::get(self, ident) + } +} + +impl GenericsMap for Option { + type Out = T::Out; + + fn get(&self, ident: &Ident) -> Option { + self.as_ref().and_then(|this| this.get(ident)) + } +} + +impl GenericsMap for (T, U) +where + T: GenericsMap, + U: GenericsMap +{ + type Out = T::Out; + + fn get(&self, ident: &Ident) -> Option { + self.0.get(ident).or_else(|| self.1.get(ident)) + } +} + +fn resolve_typredef<'a, T>(ty: &'a TypePredef, generics: T) -> TypePredef +where + T: GenericsMap + Copy +{ + TypePredef { + span: ty.span, + lt_token: ty.lt_token, + inner_ty: Box::new(resolve_ty(&ty.inner_ty, generics).into_owned()), + gt_token: ty.gt_token + } +} + +pub(super) fn resolve_ty<'a, T>(ty: &'a Type, generics: T) -> Cow<'a, Type> +where + T: GenericsMap + Copy +{ + match ty { + Type::Bool(_) | Type::Int(_) => ty.into(), + Type::Tuple(tuple) => Type::Tuple(TypeTuple { + paren_token: tuple.paren_token, + elems: tuple + .elems + .iter() + .map(|elem| resolve_ty(elem, generics).into_owned()) + .collect() + }) + .into(), + Type::Rc(rc) => Type::Rc(resolve_typredef(rc, generics)).into(), + Type::Option(option) => Type::Option(resolve_typredef(option, generics)).into(), + Type::Custom(custom) => { + if let Some(resolved_ty) = generics.get(&custom.ident) { + resolved_ty.into() + } else { + Type::Custom(TypeCustom { + ident: custom.ident.clone(), + generics: custom.generics.as_ref().map(|g| Generics { + lt_token: g.lt_token, + params: g + .params + .iter() + .map(|ty| resolve_ty(ty, generics).into_owned()) + .collect(), + gt_token: g.gt_token + }) + }) + .into() + } + }, + Type::Any => Type::Any.into() + } +} + +pub(super) fn resolve_tycustom<'a, 'b: 'a>( + state: &'a State<'b>, + custom: &'a TypeCustom +) -> Result, Diagnostic> { + if custom.generics.is_none() { + if let Some(resolved_ty) = state.generic_types().get(&custom.ident) { + return Ok(resolved_ty.into()); + } + } + let inner_ty = state.ty(&custom.ident)?; + Ok(resolve_ty( + &inner_ty.ty, + inner_ty.generics.as_ref().and_then(|def_generics| { + custom + .generics + .as_ref() + .map(|generics| (def_generics, generics)) + }) + )) +} diff --git a/src/compile/state.rs b/src/compile/state.rs new file mode 100644 index 0000000..40d2b7c --- /dev/null +++ b/src/compile/state.rs @@ -0,0 +1,399 @@ +use super::resolve_ty::{resolve_ty, GenericsMap}; +use crate::{ + ast::{Fn, Generics, Type, TypeDef, TypeTuple}, + diagnostic::Diagnostic, + verbose +}; +use indexmap::IndexMap; +use proc_macro2::{Ident, Span}; +use std::{ + borrow::Cow, + cell::RefCell, + collections::{HashMap, HashSet}, + fmt::Write as _ +}; +use syn::spanned::Spanned; + +fn mangled_fn_name(function: &Fn, generics: &Option>) -> String { + let mut ident = format!("fun_{}", function.ident); + if let Some(g) = generics { + for ty in &g.params { + write!(ident, "_{}", ty.mangled()).unwrap(); + } + } + ident +} + +struct FunctionMap { + /// A list of function definitions. + functions: Vec, + + /// A mapping of function identifier to the function definition index. + idents: HashMap, + + /// A list of generics for this function that are being used with the + /// mangled name of the function. + used_generics: RefCell>, String>>>, + + /// A list of function instances that have been emitted already. + emitted_generics: Vec> +} + +impl FunctionMap { + /// Create a new function map with these available functions of which + /// none have any instances that are being used. + fn new(functions: Vec) -> Self { + let idents = functions + .iter() + .enumerate() + .map(|(i, fun)| (fun.ident.clone(), i)) + .collect(); + let len = functions.len(); + Self { + functions, + idents, + used_generics: RefCell::new(vec![IndexMap::new(); len]), + emitted_generics: vec![HashSet::new(); len] + } + } + + /// Create a new, empty function map. + fn new_empty() -> Self { + Self { + functions: Vec::new(), + idents: HashMap::new(), + used_generics: RefCell::new(Vec::new()), + emitted_generics: Vec::new() + } + } + + /// Get an instance of a function and return its mangled name. If no + /// function with said ident exists, `None` is being returned. + fn get( + &self, + ident: &Ident, + generics: &Option> + ) -> Option<(&Fn, String)> { + let idx = *self.idents.get(ident)?; + if !self + .used_generics + .borrow() + .get(idx) + .unwrap() + .contains_key(generics) + { + self.used_generics + .borrow_mut() + .get_mut(idx) + .unwrap() + .insert( + generics.clone(), + mangled_fn_name(&self.functions[idx], generics) + ); + } + Some(( + &self.functions[idx], + self.used_generics + .borrow() + .get(idx) + .unwrap() + .get(generics) + .unwrap() + .clone() + )) + } +} + +pub(super) struct FunctionIndex { + fun_idx: usize, + inst_idx: usize +} + +pub(super) struct State<'a> { + /// The parent state, or `None`. + parent: Option<&'a State<'a>>, + + /// True if this substate represents a loop body. + is_loop: bool, + + /// A mapping of custom type identifier to the corresponding typedef. + types: HashMap, + + /// A mapping of generic ident to concrete instance. Used only for + /// function. + generic_types: HashMap, + + /// When the state represents a function, this stores the return + /// type of the function. + return_ty: Option, + + /// All the functions, which is a bit more complex since we need to + /// track the generic instances that are being used in the program. + functions: FunctionMap, + + /// A mapping of variable identifier to the type of the variable. + variables: HashMap +} + +impl State<'static> { + pub(super) fn new(types: Vec, functions: Vec) -> Self { + Self { + parent: None, + is_loop: false, + types: types.into_iter().map(|ty| (ty.ident.clone(), ty)).collect(), + generic_types: HashMap::new(), + return_ty: None, + functions: FunctionMap::new(functions), + variables: HashMap::new() + } + } + + pub(super) fn types(&self) -> impl Iterator { + if self.parent.is_some() { + panic!("This function may only be called on the root state"); + } + self.types.values() + } + + pub(super) fn next_function_instance(&mut self) -> Option { + if self.parent.is_some() { + panic!("This function may only be called on the root state"); + } + + for (fun_idx, instances) in + self.functions.used_generics.get_mut().iter().enumerate() + { + for (inst_idx, _) in instances.iter().enumerate() { + if self.functions.emitted_generics[fun_idx].contains(&inst_idx) { + continue; + } + self.functions.emitted_generics[fun_idx].insert(inst_idx); + return Some(FunctionIndex { fun_idx, inst_idx }); + } + } + + None + } + + pub(super) fn with_function_instance( + &self, + idx: FunctionIndex, + callback: F + ) -> T + where + F: FnOnce(&Fn, Option>, String) -> T + { + if self.parent.is_some() { + panic!("This function may only be called on the root state"); + } + + let used_generics = self.functions.used_generics.borrow(); + + let fun = &self.functions.functions[idx.fun_idx]; + let (generics, ident) = used_generics + .get(idx.fun_idx) + .unwrap() + .get_index(idx.inst_idx) + .unwrap(); + let generics = generics.clone(); + let ident = ident.clone(); + + // since the callback might need to add another function instance + // or do a lookup, we cannot keep the borrow past this point. + drop(used_generics); + callback(fun, generics, ident) + } +} + +impl<'a> State<'a> { + pub(super) fn is_loop(&self) -> bool { + self.is_loop || self.parent.map(State::is_loop).unwrap_or(false) + } + + pub(super) fn into_loop(mut self) -> Self { + self.is_loop = true; + self + } + + pub(super) fn ty(&self, ident: &Ident) -> Result<&TypeDef, Diagnostic> { + if let Some(ty) = self.types.get(ident) { + return Ok(ty); + } + match self.parent { + Some(parent) => parent.ty(ident), + None => Err(Diagnostic::new( + ident.span(), + "Unknown type", + "This type has not been defined" + )) + } + } + + fn has_generic_types(&self) -> bool { + !self.generic_types.is_empty() + || self + .parent + .map(|parent| parent.has_generic_types()) + .unwrap_or(false) + } + + pub(super) fn generic_types(&self) -> &HashMap { + if !self.generic_types.is_empty() { + return &self.generic_types; + } + + match self.parent.as_ref() { + Some(parent) => parent.generic_types(), + None => &self.generic_types + } + } + + pub(super) fn add_ty(&mut self, ident: Ident, ty: Type) { + if self.generic_types.is_empty() { + // ensure that only one (sub)state in the chain has generic types + if let Some(parent) = self.parent.as_ref() { + if parent.has_generic_types() { + panic!("One of the parent state already has generic types"); + } + } + } + + self.generic_types.insert(ident, ty); + } + + pub(super) fn return_ty(&self) -> Option<&Type> { + self.return_ty + .as_ref() + .or_else(|| self.parent.and_then(|parent| parent.return_ty())) + } + + pub(super) fn set_return_ty(&mut self, ty: Type) { + if self.return_ty().is_some() { + panic!( + "This state or one of its parent states already has a return type set" + ); + } + + self.return_ty = Some(ty); + } + + pub(super) fn fun( + &self, + ident: &Ident, + generics: &Option> + ) -> Result, Diagnostic> { + let generics = generics.as_ref().map(|generics| Generics { + lt_token: generics.lt_token, + params: generics + .params + .iter() + .map(|param| resolve_ty(param, self.generic_types()).into_owned()) + .collect(), + gt_token: generics.gt_token + }); + + if let Some((fun, fun_ident)) = self.functions.get(ident, &generics) { + match (fun.generics.as_ref(), generics.as_ref()) { + (None, None) => {}, + (Some(fun_generics), Some(generics)) + if fun_generics.params.len() == generics.params.len() => {}, + _ => return Err(Diagnostic::new( + ident.span(), + "Mismatched generics", + "The generics at the call site differ with the function declaration" + )) + }; + return Ok(FunctionInstance { + fun, + generics, + ident: fun_ident + }); + } + match self.parent { + Some(parent) => parent.fun(ident, &generics), + None => Err(Diagnostic::new( + ident.span(), + "Unknown function", + "This function has not been defined" + )) + } + } + + pub(super) fn var(&self, ident: &Ident) -> Result<&Type, Diagnostic> { + if let Some(ty) = self.variables.get(ident) { + return Ok(ty); + } + match self.parent { + Some(parent) => parent.var(ident), + None => Err(Diagnostic::new( + ident.span(), + "No such variable", + "Variable not defined in scope" + )) + } + } + + pub(super) fn add_variable(&mut self, ident: Ident, ty: Type) { + if verbose() { + eprintln!("[state] adding variable `{ident}` with type `{ty}`"); + } + self.variables.insert(ident, ty); + } + + pub(super) fn substate<'b>(&'b self) -> State<'b> + where + 'a: 'b + { + State { + parent: Some(self), + is_loop: false, + types: HashMap::new(), + generic_types: HashMap::new(), + return_ty: None, + functions: FunctionMap::new_empty(), + variables: HashMap::new() + } + } +} + +pub(super) struct FunctionInstance<'a> { + fun: &'a Fn, + generics: Option>, + pub ident: String +} + +impl Spanned for FunctionInstance<'_> { + fn span(&self) -> Span { + self.fun.ident.span() + } +} + +impl FunctionInstance<'_> { + pub(super) fn inputs_len(&self) -> usize { + self.fun.inputs.len() + } + + fn generics(&self) -> impl GenericsMap + Copy { + self.fun + .generics + .as_ref() + .and_then(|fg| self.generics.as_ref().map(|ig| (fg, ig))) + } + + pub(super) fn inputs(&self) -> impl Iterator> { + let generics = self.generics(); + self.fun + .inputs + .iter() + .map(move |arg| resolve_ty(&arg.ty, generics)) + } + + pub(super) fn output(&self) -> Cow<'_, Type> { + let generics = self.generics(); + self.fun + .output + .as_ref() + .map(|(_, ty)| resolve_ty(ty, generics)) + .unwrap_or_else(|| Type::Tuple(TypeTuple::default()).into()) + } +} diff --git a/src/compile/ty.rs b/src/compile/ty.rs new file mode 100644 index 0000000..b905a07 --- /dev/null +++ b/src/compile/ty.rs @@ -0,0 +1,61 @@ +use super::State; +use crate::{ast::Type, diagnostic::Diagnostic}; +use std::borrow::Cow; + +pub(super) fn expand_type( + state: &State<'_>, + ty: &Type +) -> Result, Diagnostic> { + Ok(match ty { + Type::Bool(_) => "::stdlib::bool".into(), + Type::Int(_) => "::stdlib::int".into(), + + Type::Tuple(tuple) => { + let mut buf = "(".to_owned(); + for elem in &tuple.elems { + buf += &expand_type(state, elem)?; + buf += ", " + } + buf += ")"; + buf.into() + }, + + Type::Rc(rc) => { + format!("::stdlib::Rc<{}>", expand_type(state, &rc.inner_ty)?).into() + }, + + Type::Option(option) => format!( + "::stdlib::Option<{}>", + expand_type(state, &option.inner_ty)? + ) + .into(), + + Type::Custom(custom) => { + if let Some(ty) = state.generic_types().get(&custom.ident) { + if let Some(g) = custom.generics.as_ref() { + return Err(Diagnostic::new( + g.lt_token.span, + "Invalid Type", + "This generic type parameter cannot accept generics" + )); + } + expand_type(state, ty)? + } else { + let mut buf = format!("{}", custom.ident); + if let Some(g) = &custom.generics { + buf += "::<"; + for (i, ty) in g.params.iter().enumerate() { + if i > 0 { + buf += ", "; + } + buf += &expand_type(state, ty)?; + } + buf += ">"; + } + buf.into() + } + }, + + Type::Any => unimplemented!() + }) +} diff --git a/src/diagnostic.rs b/src/diagnostic.rs new file mode 100644 index 0000000..81b5c75 --- /dev/null +++ b/src/diagnostic.rs @@ -0,0 +1,220 @@ +use ariadne::{Label, Report, ReportKind}; +use proc_macro2::{LineColumn, Span}; +use std::{ + fmt::{Debug, Display}, + io::{self, Write} +}; + +#[derive(Clone, Copy, Eq, PartialEq)] +pub struct Source<'a> { + pub code: &'a str, + pub filename: Option<&'a str> +} + +impl<'a> Source<'a> { + pub fn new(code: &'a str) -> Self { + Self { + code, + filename: None + } + } + + pub fn with_filename(mut self, filename: &'a str) -> Self { + self.filename = Some(filename); + self + } + + fn offset(&self, at: LineColumn) -> usize { + let line_offset: usize = self + .code + .split('\n') + .take(at.line - 1) + .map(|line| line.len() + 1) + .sum(); + line_offset + at.column + } +} + +pub struct SourceSpan<'a> { + source: Source<'a>, + start: usize, + end: usize +} + +impl<'a> ariadne::Span for SourceSpan<'a> { + type SourceId = Source<'a>; + + fn source(&self) -> &Self::SourceId { + &self.source + } + + fn start(&self) -> usize { + self.start + } + + fn end(&self) -> usize { + self.end + } +} + +struct SourceCache { + source: ariadne::Source +} + +impl SourceCache { + fn new(source: Source<'_>) -> Self { + Self { + source: ariadne::Source::from(source.code) + } + } +} + +impl ariadne::Cache> for SourceCache { + fn fetch(&mut self, _: &Source<'_>) -> Result<&ariadne::Source, Box> { + Ok(&self.source) + } + + fn display<'a>(&self, id: &'a Source<'_>) -> Option> { + id.filename + .map(|filename| Box::new(filename) as Box) + } +} + +pub struct Diagnostic { + span: Span, + msg: String, + labels: Vec<(Span, String)>, + note: Option, + help: Option +} + +impl Diagnostic { + pub fn new(span: Span, topic: T, msg: M) -> Self + where + T: Into, + M: Into + { + Self { + span, + msg: topic.into(), + labels: vec![(span, msg.into())], + note: None, + help: None + } + } + + pub fn with_label(mut self, span: Span, msg: M) -> Self + where + M: Into + { + self.labels.push((span, msg.into())); + self + } + + pub fn with_note(mut self, note: N) -> Self + where + N: Into + { + self.note = Some(note.into()); + self + } + + pub fn with_help(mut self, help: H) -> Self + where + H: Into + { + self.help = Some(help.into()); + self + } + + pub fn no_such_field(span: Span, msg: M) -> Self + where + M: Into + { + Self::new(span, "No such field", msg) + } + + pub fn type_mismatch( + expected_ty: T, + expected_span: Span, + expression_ty: U, + expression_span: Span + ) -> Self + where + T: Display, + U: Display + { + Self { + span: expression_span, + msg: "Type Mismatch".into(), + labels: if expected_span.start() < expression_span.start() { + vec![ + (expected_span, format!("Expected type `{expected_ty}`,")), + ( + expression_span, + format!("but expression is of type `{expression_ty}`") + ), + ] + } else { + vec![ + ( + expression_span, + format!("Expression is of type `{expression_ty}`,") + ), + (expected_span, format!("but expected type `{expected_ty}`")), + ] + }, + note: None, + help: None + } + } + + pub fn ice(err: syn::Error) -> Self { + Self::new(err.span(), "Internal Compiler Error", err.to_string()).with_note( + "The compiler produced invalid intermediate Rust code. This is a bug." + ) + } +} + +impl From for Diagnostic { + fn from(err: syn::Error) -> Self { + Self::new(err.span(), "Syntax Error", err.to_string()) + } +} + +impl Diagnostic { + fn into_report(self, source: Source<'_>) -> Report> { + let start_offset = source.offset(self.span.start()); + let mut report = + Report::build(ReportKind::Error, source, start_offset).with_message(self.msg); + for (label_span, label_msg) in self.labels { + report.add_label( + Label::new(SourceSpan { + source, + start: source.offset(label_span.start()), + end: source.offset(label_span.end()) + }) + .with_message(label_msg) + ); + } + if let Some(note) = self.note { + report.set_note(note); + } + if let Some(help) = self.help { + report.set_help(help); + } + + report.finish() + } + + pub fn write(self, source: Source<'_>, w: W) -> io::Result<()> + where + W: Write + { + self.into_report(source).write(SourceCache::new(source), w) + } + + pub fn eprint(self, source: Source<'_>) -> io::Result<()> { + self.into_report(source).eprint(SourceCache::new(source)) + } +} diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..69aaac8 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,26 @@ +#![allow(clippy::mixed_read_write_in_expression)] +#![warn(rust_2018_idioms, rustdoc::broken_intra_doc_links)] +#![deny(elided_lifetimes_in_paths)] +#![forbid(unsafe_code)] + +use std::sync::atomic::{AtomicBool, Ordering}; + +pub static VERBOSE: AtomicBool = AtomicBool::new(false); + +fn verbose() -> bool { + VERBOSE.load(Ordering::Relaxed) +} + +pub fn set_verbose(verbose: bool) { + VERBOSE.store(verbose, Ordering::Relaxed) +} + +macro_rules! bail { + ($span:expr => $msg:expr) => { + return Err(syn::Error::new($span, format!($msg))) + }; +} + +pub mod ast; +pub mod compile; +pub mod diagnostic; diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..c7dc868 --- /dev/null +++ b/src/main.rs @@ -0,0 +1,179 @@ +#![warn(rust_2018_idioms, rustdoc::broken_intra_doc_links)] +#![deny(elided_lifetimes_in_paths)] +#![forbid(unsafe_code)] + +use anyhow::Context as _; +use bat::PrettyPrinter; +use clap::Parser; +use compiler::{ + compile::{compile, CompileResult}, + diagnostic::{Diagnostic, Source}, + set_verbose +}; +use std::{ + fs::{self, File}, + io::{Cursor, Read as _, Write as _}, + path::PathBuf, + process::{self, Command} +}; +use tempfile::tempdir; + +const STDLIB_ARCHIVE: &[u8] = include_bytes!(env!("ARCHIVE_PATH")); + +const MANIFEST: &[u8] = br#" +[workspace] +members = [".", "stdlib"] + +[package] +name = "program" +version = "0.0.0" +publish = false +edition = "2021" + +[[bin]] +name = "program" +path = "main.rs" + +[dependencies] +bpaf = "0.4.7" +indexmap = "1.9" +indicatif = { version = "0.17", features = ["rayon"] } +rayon = "1.5" +stdlib = { path = "stdlib" } + +[profile.release] +debug = true +lto = true +"#; + +#[derive(Parser)] +#[clap(about, author, version = concat!(env!("VERGEN_GIT_COMMIT_COUNT"), "-g", env!("VERGEN_GIT_SHA_SHORT")))] +struct Args { + /// Enable verbose output. + #[clap(short, long, action)] + verbose: bool, + + /// Enable compilation in release mode. + #[clap(long, action)] + release: bool, + + /// The input file to compile. + #[clap(value_parser)] + input_file: PathBuf, + + /// The output binary. + #[clap(short, value_parser, default_value = "a.out")] + output_file: PathBuf +} + +fn main() -> anyhow::Result<()> { + // parse command line arguments + let args = Args::parse(); + set_verbose(args.verbose); + + // create temporary directory to assemble all files + let tempdir = tempdir().context("Failed to create temporary directory")?; + let tmpdir = tempdir.path(); + + // extract the stdlib archive into the tempdir + let mut archive = tar::Archive::new(Cursor::new(STDLIB_ARCHIVE)); + archive + .unpack(&tmpdir) + .context("Failed to unpack the stdlib")?; + + // create the manifest + let manifest_path = tmpdir.join("Cargo.toml"); + File::create(&manifest_path) + .context("Failed to create the manifest")? + .write_all(MANIFEST) + .context("Failed to write the manifest")?; + + // create the main file from the transcribed program + let mut input = String::new(); + File::open(&args.input_file) + .context("Failed to open the input file")? + .read_to_string(&mut input) + .context("Failed to read the input file")?; + let (main_rs, funs_rs, types_rs) = match compile(&input) { + Ok(CompileResult::Ok { + main_rs, + funs_rs, + types_rs + }) => (main_rs, funs_rs, types_rs), + Ok(CompileResult::InternalErr(code, err)) => { + if args.verbose { + PrettyPrinter::new() + .input_from_bytes(code.as_bytes()) + .language("Rust") + .line_numbers(true) + .print() + .unwrap(); + } + Diagnostic::ice(err) + .eprint(Source::new(&code)) + .context("Failed to print ICE")?; + process::exit(1); + }, + Err(err) => { + let filename = args.input_file.file_name().unwrap().to_str().unwrap(); + err.eprint(Source::new(&input).with_filename(filename)) + .context("Failed to print error message")?; + process::exit(1); + } + }; + let main_rs = prettyplease::unparse(&main_rs); + let funs_rs = prettyplease::unparse(&funs_rs); + let types_rs = prettyplease::unparse(&types_rs); + if args.verbose { + PrettyPrinter::new() + .input(bat::Input::from_bytes(types_rs.as_bytes()).name("types.rs")) + .input(bat::Input::from_bytes(funs_rs.as_bytes()).name("funs.rs")) + .input(bat::Input::from_bytes(main_rs.as_bytes()).name("main.rs")) + .language("Rust") + .line_numbers(true) + .print() + .unwrap(); + } + fs::write(tmpdir.join("main.rs"), main_rs).context("Failed to write main.rs file")?; + fs::write(tmpdir.join("funs.rs"), funs_rs).context("Failed to write funs.rs file")?; + fs::write(tmpdir.join("types.rs"), types_rs) + .context("Failed to write types.rs file")?; + + // run cargo + let target_dir = args + .input_file + .parent() + .expect("How can a file not be in a directory?") + .join("target") + .join( + args.input_file + .file_name() + .expect("How can a file not have a filename?") + ); + fs::create_dir_all(&target_dir)?; + let mut cmd = Command::new("cargo"); + cmd.arg("build") + .arg("--target-dir") + .arg(&target_dir) + .arg("--manifest-path") + .arg(&manifest_path); + if args.release { + cmd.arg("--release"); + } + cmd.env("RUST_BACKTRACE", "0"); + let success = cmd + .status() + .context("Failed to execute cargo build")? + .success(); + if !success { + anyhow::bail!("cargo returned non-successful exit code"); + } + fs::copy( + target_dir + .join(args.release.then_some("release").unwrap_or("debug")) + .join("program"), + &args.output_file + )?; + + Ok(()) +} diff --git a/stdlib/Cargo.toml b/stdlib/Cargo.toml new file mode 100644 index 0000000..1826e6d --- /dev/null +++ b/stdlib/Cargo.toml @@ -0,0 +1,16 @@ +# -*- eval: (cargo-minor-mode 1) -*- + +[package] +name = "stdlib" +version = "0.0.0" +publish = false +edition = "2021" +authors = ["Dominic Meiser "] + +[dependencies] +indexmap = "1.9" +itoa = "1" +num-integer = "0.1.45" +paste = "1" +rand = { version = "0.8", features = ["getrandom"], default-features = false } +svgwriter = "0.1" diff --git a/stdlib/src/lib.rs b/stdlib/src/lib.rs new file mode 100644 index 0000000..e61158b --- /dev/null +++ b/stdlib/src/lib.rs @@ -0,0 +1,145 @@ +use rand::{ + distributions::{Distribution as _, Uniform}, + rngs::OsRng, + Rng +}; +use std::{ + fmt::{self, Debug, Formatter, Write as _}, + panic::panic_any +}; +pub use std::{ + option::Option, + primitive::{bool, isize as int} +}; + +mod output; +pub use output::{ + BarChartPrinter, BubbleChartPrinter, ListPrinter, OutputList, OutputPrinter, + OutputPrinterAny, OutputPrinterTwoVars, OutputTarget +}; + +#[doc(hidden)] +pub struct FnNoReturn { + msg: String +} + +/// Called as the last part of every function to abnormally terminate in case there is a +/// missing return statement. +#[track_caller] +pub fn fn_no_return(fun_ident: &str) -> FnNoReturn { + FnNoReturn { + msg: format!( + r#"Function `{}` has finished without returning a value, but it declares a return type. + +Help: The function was called with the following arguments:"#, + fun_ident + ) + } +} + +impl FnNoReturn { + pub fn add_input(mut self, key: &'static str, value: &T) -> Self + where + T: Debug + { + write!(self.msg, "\n\t{key}: {value:#?}").unwrap(); + self + } + + pub fn panic(self) -> ! { + panic_any(self.msg) + } +} + +/// Return the default (pseudo) random number generator. +fn rng() -> impl Rng { + OsRng::default() +} + +/// Used for input argument parsing. +#[derive(Debug, Clone, Copy)] +pub enum ArgValue { + Value(int), + Range { min: int, max: int } +} + +impl ArgValue { + pub fn get_value(self) -> int { + match self { + Self::Value(value) => value, + Self::Range { min, max } => { + Uniform::new_inclusive(min, max).sample(&mut rng()) + }, + } + } +} + +/// The result of a coin flip. +pub enum Coin { + Head, + Tail +} + +impl Coin { + /// Flip a coin, returning [`Coin::Head`] with probability `prob`. + pub fn flip(prob: f32) -> Self { + debug_assert!(prob > 0.0); + debug_assert!(prob < 1.0); + + if rng().gen::() < prob { + Self::Head + } else { + Self::Tail + } + } +} + +/// The `Rc` reference-counting smart pointer of the language. +pub struct Rc(std::rc::Rc>); + +impl Rc { + pub fn new(inner: T) -> Self { + Self(std::rc::Rc::new(std::cell::RefCell::new(inner))) + } + + pub fn into_inner(this: Rc) -> T + where + T: Clone + { + match std::rc::Rc::try_unwrap(this.0) { + Ok(cell) => cell.into_inner(), + Err(rc) => rc.borrow().clone() + } + } + + pub fn with_mut(&mut self, callback: F) + where + F: FnOnce(&mut T) + { + let mut this = self.0.borrow_mut(); + callback(&mut this); + } +} + +impl Clone for Rc { + fn clone(&self) -> Self { + Self(std::rc::Rc::clone(&self.0)) + } +} + +impl Debug for Rc { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + f.debug_tuple("Rc") + .field(&DebugPtr(self.0.as_ptr())) + .field(&self.0.borrow()) + .finish() + } +} + +struct DebugPtr(*const T); + +impl Debug for DebugPtr { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + write!(f, "{:#X}", self.0 as usize) + } +} diff --git a/stdlib/src/output/bar.rs b/stdlib/src/output/bar.rs new file mode 100644 index 0000000..7ca7696 --- /dev/null +++ b/stdlib/src/output/bar.rs @@ -0,0 +1,356 @@ +use super::{OutputList, OutputPrinter, Svg}; +use crate::int; +use std::{ + cmp::Ordering, + collections::BTreeMap, + fmt::Write as _, + io::{self, Write}, + mem +}; +use svgwriter::{ + tags::{Group, Path, Rect, Style, TSpan, TagWithPresentationAttributes as _, Text}, + Data, Transform +}; + +/// Output printer for SVG Bar Charts. Works best with one +/// output variable. +pub struct BarChartPrinter { + /// Set to true to enable SVG/XML pretty printing. + pretty: bool +} + +impl BarChartPrinter { + pub fn new(pretty: bool) -> Self { + Self { pretty } + } +} + +#[derive(Default)] +struct YAxis { + percentages: Vec, + max: f32 +} + +impl YAxis { + fn add_percentage(&mut self, percentage: f32) { + self.percentages.push(percentage); + self.max = self.max.max(percentage).ceil(); + } +} + +#[derive(Clone, Copy, Eq)] +enum Range { + Single(int), + Range { start: int, len: int } +} + +impl Range { + fn start(&self) -> int { + match self { + Self::Single(value) => *value, + Self::Range { start, .. } => *start + } + } + + fn end(&self) -> int { + match self { + Self::Single(value) => *value, + Self::Range { start, len } => start + len - 1 + } + } +} + +impl PartialEq for Range { + fn eq(&self, other: &Self) -> bool { + self.start() == other.start() && self.end() == other.end() + } +} + +impl PartialOrd for Range { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + +impl Ord for Range { + fn cmp(&self, other: &Self) -> Ordering { + if self == other { + Ordering::Equal + } else if self.start() == other.start() { + self.end().cmp(&other.end()) + } else { + self.start().cmp(&other.start()) + } + } +} + +#[derive(Default)] +struct XAxis { + values: BTreeMap, + max: Option, + min: Option +} + +impl XAxis { + fn add_value(&mut self, value: int, qty: usize) { + let range = Range::Single(value); + if let Some(range_qty) = self.values.get_mut(&range) { + *range_qty += qty; + } else { + self.values.insert(range, qty); + } + + if self.max.map(|max| max < value).unwrap_or(true) { + self.max = Some(value); + } + if self.min.map(|min| min > value).unwrap_or(true) { + self.min = Some(value); + } + } + + fn with_step(&mut self, step: int) { + let values = mem::take(&mut self.values); + for (range, qty) in values { + let value = match range { + Range::Single(value) => value, + _ => panic!("with_step may only be called once") + }; + let offset = value - self.min.unwrap(); + let start = self.min.unwrap() + (offset / step) * step; + let range = Range::Range { start, len: step }; + + if let Some(range_qty) = self.values.get_mut(&range) { + *range_qty += qty; + } else { + self.values.insert(range, qty); + } + } + } +} + +impl OutputPrinter for BarChartPrinter { + fn print(self, list: OutputList, mut out: W) -> io::Result<()> + where + I: IntoIterator, + E: IntoIterator, + W: Write + { + let mut yaxis = YAxis::default(); + let mut xaxis = XAxis::default(); + let mut xaxis_name = None; + + for (entry, qty) in list.samples { + for (key, value) in entry { + if xaxis_name.is_none() { + xaxis_name = Some(key); + } else if xaxis_name.unwrap() != key { + continue; + } + + xaxis.add_value(value, qty); + } + } + let xaxis_name = xaxis_name.expect("Missing output variable"); + + // group the xaxis together + let mut xaxis_step = 1; + while (xaxis.max.unwrap() - xaxis.min.unwrap()) / xaxis_step > 12 { + xaxis_step += 1; + } + xaxis.with_step(xaxis_step); + + // build the yaxis + for qty in xaxis.values.values().copied() { + yaxis.add_percentage(qty as f32 / list.n as f32 * 100.0); + } + + let chart_margin = 30; + let chart_margin_x = 70; + let chart_margin_y = 100; + let axis_step_dist = 45; + let bar_width = 20; + let yaxis_steps = 7; + // note that the number of datapoints is equivalent on both axis, therefore + // we can use the yaxis to calculate the width + let chart_width = (yaxis.percentages.len() as i32 + 1) * axis_step_dist; + let chart_height = yaxis_steps * axis_step_dist + axis_step_dist / 2; + let svg_width = chart_width + chart_margin + chart_margin_x; + let svg_height = chart_height + chart_margin + chart_margin_y; + let mut svg = Svg::new(svg_width, svg_height); + + // add svg style + let mut style = concat!( + "*{font-family:\"Source Serif 4\",serif}", + ".ui{stroke:#000;stroke-width:1px;fill:#000;font-size:12pt}", + ".ui text,.bg,.b{stroke-width:0}", + ".p{font-size:10pt}", + ".bg{fill:#fff}", + ".b{stroke:#f60;fill:#f60}", + // dark theme + "@media(prefers-color-scheme: dark){", + ".ui{stroke:#ccc;fill:#ccc}", + ".bg{fill:#222}", + "}" + ) + .to_owned(); + // allow svg to be printed to pdf by headless chromium + write!( + style, + "@page{{margin:0;size:calc(20cm*{svg_width}/{svg_height}) 20cm}}" + ) + .unwrap(); + svg.push(Style::new().append(style)); + + // add svg background + svg.push( + Rect::new() + .with_class("bg") + .with_x(0) + .with_y(0) + .with_width(chart_width + chart_margin + chart_margin_x) + .with_height(chart_height + chart_margin + chart_margin_y) + ); + + let mut group = Group::new() + .with_transform( + Transform::new().translate(chart_margin_x, chart_height + chart_margin) + ) + .with_class("ui"); + + // build vertical bars + let mut bars_data = Data::new(); + let mut bars_text = Text::new().with_text_anchor("middle"); + for (i, percentage) in yaxis.percentages.iter().enumerate() { + let offset = (i + 1) as i32 * axis_step_dist; + let height = percentage / yaxis.max * (yaxis_steps * axis_step_dist) as f32; + bars_data + .move_to(offset - bar_width / 2, 0) + .horiz_line_by(bar_width) + .vert_line_by(-height) + .horiz_line_by(-bar_width) + .close(); + bars_text.push( + TSpan::new() + .with_x(offset) + .with_y(-height - 5.0) + .append(format!("{percentage:.2}%")) + ); + } + group.push(Path::new().with_d(bars_data).with_class("b")); + group.push(bars_text.with_class("b p")); + + // build y axis line + let mut axis_line = Data::new(); + axis_line + .move_to(0, -chart_height) + .line_by(3, 5) + .horiz_line_by(-6) + .close() + .vert_line_to(0); + for (i, _) in (axis_step_dist .. chart_height) + .step_by(axis_step_dist as usize) + .enumerate() + { + axis_line + .move_by((i == 0).then_some(-3).unwrap_or(-6), -axis_step_dist) + .horiz_line_by(6); + } + + // build x axis line + axis_line + .move_to(chart_width, 0) + .line_by(-5, 3) + .vert_line_by(-6) + .close() + .horiz_line_to(0); + for (i, _) in (axis_step_dist .. chart_width) + .step_by(axis_step_dist as usize) + .enumerate() + { + axis_line + .move_by(axis_step_dist, (i == 0).then_some(-3).unwrap_or(-6)) + .vert_line_by(6); + } + group.push(Path::new().with_d(axis_line).with_stroke_linejoin("arcs")); + + // build y axis descriptions + let mut yaxis_text = Text::new() + .with_text_anchor("end") + .with_dominant_baseline("middle"); + for i in 0 .. yaxis_steps { + yaxis_text.push( + TSpan::new() + .with_x(-5) + .with_y((i as i32 + 1) * -axis_step_dist) + .append(format!( + "{:.2}%", + yaxis.max / yaxis_steps as f32 * (i + 1) as f32 + )) + ); + } + group.push(yaxis_text); + + // build x axis descriptions + let mut xaxis_text = Text::new() + .with_class("b") + .with_text_anchor("middle") + .with_dominant_baseline("hanging"); + for (i, range) in xaxis.values.keys().copied().enumerate() { + let x = (i + 1) as i32 * axis_step_dist; + let y = 5; + match range { + Range::Single(value) => { + xaxis_text.push( + TSpan::new() + .with_x(x) + .with_y(y) + .append(format!("{}", value)) + ); + }, + Range::Range { start, len } => { + xaxis_text.push( + TSpan::new() + .with_x(x) + .with_y(y) + .append(format!("{}", start)) + ); + xaxis_text.push(TSpan::new().with_x(x).with_dy(".8em").append("-")); + xaxis_text.push( + TSpan::new() + .with_x(x) + .with_dy("1em") + .append(format!("{}", start + len - 1)) + ); + } + } + } + group.push(xaxis_text); + + // build y axis legend + let mut ylegend = Text::new().with_x(-10).with_y(-chart_height - 5); + ylegend.push(format!("Percentage of {} samples", list.n)); + group.push(ylegend); + + // build x axis legend + let mut xlegend = Text::new() + .with_x(0) + .with_y(chart_margin_y - chart_margin) + .with_dominant_baseline("hanging"); + xlegend.push("Legend: "); + xlegend.push( + TSpan::new() + .with_class("b") + .append(format!("{}", xaxis.min.unwrap_or(1))) + ); + xlegend.push(format!(": {xaxis_name}")); + group.push(xlegend); + + svg.push(group); + let svg = if self.pretty { + svg.to_string_pretty() + } else { + svg.to_string() + }; + writeln!(out, "{}", svg) + } +} diff --git a/stdlib/src/output/bubble.js b/stdlib/src/output/bubble.js new file mode 100644 index 0000000..63fe8d5 --- /dev/null +++ b/stdlib/src/output/bubble.js @@ -0,0 +1,21 @@ +var svg,xn,yn,tt,ttp,ttx,tty; +function e(c){return document.getElementById(c)} +function i(xn0,yn0){ + xn=xn0;yn=yn0; + svg=document.getElementsByTagName("svg")[0]; + tt=e("tt");ttp=e("ttp");ttx=e("ttx");tty=e("tty"); + c() +} +function tr(x,y){tt.setAttribute('transform','translate('+x+','+y+')')} +function s(ev,p,x,y) { + ttp.textContent=p+"%"; + ttx.textContent=xn+": "+x; + tty.textContent=yn+": "+y; + var r = ev.target.getBoundingClientRect(); + var p = ev.target.ownerSVGElement.createSVGPoint(); + p.x=r.x+r.width/2; + p.y=r.top; + p=p.matrixTransform(svg.getScreenCTM().inverse()); + tr(p.x,p.y-5); +} +function c(){tr(-999,-999)} \ No newline at end of file diff --git a/stdlib/src/output/bubble.rs b/stdlib/src/output/bubble.rs new file mode 100644 index 0000000..32dad36 --- /dev/null +++ b/stdlib/src/output/bubble.rs @@ -0,0 +1,379 @@ +use super::{OutputList, OutputPrinter, Svg}; +use crate::int; +use indexmap::IndexMap; +use std::{fmt::Write as _, io}; +use svgwriter::{ + tags::{ + Circle, Group, Path, Rect, Script, Style, TSpan, TagWithCoreAttributes as _, + TagWithGlobalEventAttributes as _, TagWithPresentationAttributes as _, Text + }, + Data, Transform +}; +use num_integer::div_ceil; + +/// Output printer for SVG Bar Charts. Works only with two +/// output variables. +pub struct BubbleChartPrinter { + /// Set to true to enable SVG/XML pretty printing. + pretty: bool +} + +impl BubbleChartPrinter { + pub fn new(pretty: bool) -> Self { + Self { pretty } + } +} + +#[derive(Default)] +struct Axis { + values: Vec, + min: Option, + max: Option +} + +impl Axis { + fn add_value(&mut self, value: int) { + self.values.push(value); + if self.max.map(|max| max < value).unwrap_or(true) { + self.max = Some(value); + } + if self.min.map(|min| min > value).unwrap_or(true) { + self.min = Some(value); + } + } + + fn min(&self) -> int { + self.min.unwrap_or(0) + } + + fn max(&self) -> int { + self.max.unwrap_or(0) + } + + fn step(&self, preferred_step_count: usize) -> (int, usize) { + let range = self.max() - self.min(); + if range == 0 { + return (0, 1); + } + + let mut step = 1; + loop { + let count = div_ceil(range, step) + 1; + if (count - preferred_step_count as int).unsigned_abs() as usize + <= preferred_step_count / 2 + { + return (step, count as usize); + } + step += 1; + } + } +} + +impl OutputPrinter for BubbleChartPrinter { + fn print(self, list: OutputList, mut out: W) -> io::Result<()> + where + I: IntoIterator, + E: IntoIterator, + W: io::Write + { + let mut percentages = Vec::new(); + let mut min_percentage: f32 = 100.0; + let mut max_percentage: f32 = 0.0; + let mut axis: IndexMap<&'static str, Axis> = IndexMap::new(); + for (entry, qty) in list.samples { + let percentage = qty as f32 / list.n as f32 * 100.0; + percentages.push(percentage); + min_percentage = min_percentage.min(percentage); + max_percentage = max_percentage.max(percentage); + for (key, value) in entry { + if !axis.contains_key(key) { + axis.insert(key, Axis::default()); + } + axis.get_mut(key).unwrap().add_value(value); + } + } + + let (yaxis_name, yaxis) = axis + .swap_remove_index(1) + .expect("Cannot use BubbleChartPrinter with less than two variables"); + let (xaxis_name, xaxis) = axis.swap_remove_index(0).unwrap(); + + let chart_margin_x = 60; + let chart_margin_y = 60; + let bubble_min_r: f32 = 2.5; + let bubble_max_r: f32 = 15.0; + let axis_step_dist = 40; + let (xaxis_step, xaxis_step_count) = xaxis.step(8); + let (yaxis_step, yaxis_step_count) = yaxis.step(6); + let chart_width = axis_step_dist * xaxis_step_count as i32; + let chart_height = axis_step_dist * yaxis_step_count as i32; + let svg_width = chart_width + 2 * chart_margin_x; + let svg_height = chart_height + 2 * chart_margin_y; + let mut svg = Svg::new(svg_width, svg_height) + .with_onload(format!("i({xaxis_name:?},{yaxis_name:?})")); + + // add svg style and script + let mut style = concat!( + "*{font-family:\"Source Serif 4\",serif}", + ".ui,#tt{stroke:#000;stroke-width:1px;fill:#000;font-size:12pt}", + "#tt{font-size:10pt}", + "#tt path{fill:rgba(238,238,238,0.85)}", + "#ttp{font-weight:bold}", + "text,.bg,.b{stroke-width:0}", + ".bg{fill:#fff}", + ".b,#ttx,#tty{fill:#f60}", + ".b circle,circle.b{opacity:0.8}", + // dark theme + "@media(prefers-color-scheme: dark){", + "#tt path{fill:rgba(34,34,34,0.7)}", + ".ui,#tt{stroke:#ccc;fill:#ccc}", + ".bg{fill:#222}", + "}" + ) + .to_owned(); + // allow svg to be printed to pdf by headless chromium + write!( + style, + "@page{{margin:0;size:calc(20cm*{svg_width}/{svg_height}) 20cm}}" + ) + .unwrap(); + svg.push(Style::new().append(style)); + svg.push( + Script::new() + .with_ty("text/ecmascript") + .append(include_str!("bubble.js").trim()) + ); + + // add svg background + svg.push( + Rect::new() + .with_class("bg") + .with_x(0) + .with_y(0) + .with_width(chart_width + 2 * chart_margin_x) + .with_height(chart_height + 2 * chart_margin_y) + .with_onmouseover("c()") + ); + + let mut group = Group::new() + .with_transform( + Transform::new().translate(chart_margin_x, chart_height + chart_margin_y) + ) + .with_class("ui"); + + // build bubbles + let mut bubbles = Group::new().with_class("b").with_transform( + Transform::new() + .translate(axis_step_dist / 2, axis_step_dist / -2) + .scale(1.0, -1.0) + ); + for (i, percentage) in percentages.iter().enumerate() { + let xvalue = xaxis.values[i]; + let yvalue = yaxis.values[i]; + + let xpos = + (xvalue - xaxis.min()) as f32 / xaxis_step as f32 * axis_step_dist as f32; + let ypos = + (yvalue - yaxis.min()) as f32 / yaxis_step as f32 * axis_step_dist as f32; + // we want the area to grow proportional to the percentage, so the + // radius grows proportional to the square root of the percentage + let radius = bubble_min_r + + ((bubble_max_r - bubble_min_r) + * ((percentage - min_percentage) + / (max_percentage - min_percentage)) + .sqrt()); + bubbles.push( + Circle::new() + .with_cx(xpos) + .with_cy(ypos) + .with_r(radius) + .with_onmouseover(format!( + "s(evt,\"{:.2}\",{},{})", + percentage, xvalue, yvalue + )) + ); + } + group.push(bubbles); + + // build y axis line + let mut axis_line0 = Data::new(); + let mut axis_line1 = Data::new(); + axis_line0.move_to(0, -chart_height).vert_line_to(0); + axis_line1 + .move_to(chart_width, -chart_height) + .vert_line_to(0); + for i in 0 .. yaxis_step_count { + let (offset, step) = match i { + 0 => (-3, -axis_step_dist / 2), + _ => (-6, -axis_step_dist) + }; + axis_line0.move_by(offset, step).horiz_line_by(6); + axis_line1.move_by(offset, step).horiz_line_by(6); + } + + // build x axis line + axis_line0.move_to(chart_width, 0).horiz_line_to(0); + axis_line1 + .move_to(chart_width, -chart_height) + .horiz_line_to(0); + for i in 0 .. xaxis_step_count { + let (offset, step) = match i { + 0 => (-3, axis_step_dist / 2), + _ => (-6, axis_step_dist) + }; + axis_line0.move_by(step, offset).vert_line_by(6); + axis_line1.move_by(step, offset).vert_line_by(6); + } + group.push( + Group::new() + .with_stroke_linejoin("arcs") + .append(Path::new().with_d(axis_line0)) + .append(Path::new().with_d(axis_line1)) + ); + + // build y axis descriptions + let mut yaxis_text0 = Text::new() + .with_text_anchor("end") + .with_dominant_baseline("middle"); + let mut yaxis_text1 = Text::new().with_dominant_baseline("middle"); + for i in 0 .. yaxis_step_count { + let text = format!("{}", yaxis.min() + i as int * yaxis_step,); + yaxis_text0.push( + TSpan::new() + .with_x(-5) + .with_y(i as i32 * -axis_step_dist - axis_step_dist / 2) + .append(text.clone()) + ); + yaxis_text1.push( + TSpan::new() + .with_x(chart_width + 5) + .with_y(i as i32 * -axis_step_dist - axis_step_dist / 2) + .append(text) + ); + } + group.push(yaxis_text0); + group.push(yaxis_text1); + + // build x axis descriptions + let mut xaxis_text0 = Text::new().with_text_anchor("middle"); + let mut xaxis_text1 = Text::new() + .with_text_anchor("middle") + .with_dominant_baseline("hanging"); + for i in 0 .. xaxis_step_count { + let text = format!("{}", xaxis.min() + i as int * xaxis_step,); + xaxis_text0.push( + TSpan::new() + .with_x(i as i32 * axis_step_dist + axis_step_dist / 2) + .with_y(-chart_height - 5) + .append(text.clone()) + ); + xaxis_text1.push( + TSpan::new() + .with_x(i as i32 * axis_step_dist + axis_step_dist / 2) + .with_y(5) + .append(text) + ); + } + group.push(xaxis_text0); + group.push(xaxis_text1); + + // build y axis legend + group.push( + Group::new() + .with_transform( + Transform::new() + .translate(15 - chart_margin_x, chart_height / -2) + .rotate(270.0) + ) + .append( + Text::new() + .with_text_anchor("middle") + .with_dominant_baseline("middle") + .append(yaxis_name) + ) + ); + group.push( + Group::new() + .with_transform( + Transform::new() + .translate(chart_width + chart_margin_x - 15, chart_height / -2) + .rotate(270.0) + ) + .append( + Text::new() + .with_text_anchor("middle") + .with_dominant_baseline("middle") + .append(yaxis_name) + ) + ); + + // build x axis legend + group.push( + Text::new() + .with_text_anchor("middle") + .with_dominant_baseline("middle") + .append( + TSpan::new() + .with_x(chart_width / 2) + .with_y(chart_margin_y - 15) + .append(xaxis_name) + ) + .append( + TSpan::new() + .with_x(chart_width / 2) + .with_y(15 - chart_margin_y - chart_height) + .append(xaxis_name) + ) + ); + + svg.push(group); + + // build tooltip + let mut tt = Group::new() + .with_id("tt") + .with_transform(Transform::new().scale(0.0, 0.0)); + { + let tip_straight_h = 5; + let tip_curve_h = 2; + let width = 80; + let height = 50; + let border_r = 5; + let bottom_hline = width / 2 - tip_straight_h - 2 * tip_curve_h; + let mut data = Data::new(); + data.move_to(0, 0) + .line_by(tip_straight_h, -tip_straight_h) + .quad_by(tip_curve_h, -tip_curve_h, 2 * tip_curve_h, -tip_curve_h) + .horiz_line_by(bottom_hline) + .arc_by(border_r, border_r, 0, false, false, border_r, -border_r) + .vert_line_by(-height) + .arc_by(border_r, border_r, 0, false, false, -border_r, -border_r) + .horiz_line_by(-width) + .arc_by(border_r, border_r, 0, false, false, -border_r, border_r) + .vert_line_by(height) + .arc_by(border_r, border_r, 0, false, false, border_r, border_r) + .horiz_line_by(bottom_hline) + .quad_by(tip_curve_h, 0, 2 * tip_curve_h, tip_curve_h) + .close(); + tt.push(Path::new().with_d(data)); + tt.push( + Text::new() + .with_dominant_baseline("hanging") + .with_transform(Transform::new().translate( + width / -2, + -height - tip_straight_h - tip_curve_h - border_r + )) + .append(TSpan::new().with_id("ttp")) + .append(TSpan::new().with_id("ttx").with_x(0).with_dy("1.2em")) + .append(TSpan::new().with_id("tty").with_x(0).with_dy("1.2em")) + ); + } + svg.push(tt); + + let svg = if self.pretty { + svg.to_string_pretty() + } else { + svg.to_string() + }; + writeln!(out, "{}", svg) + } +} diff --git a/stdlib/src/output/list.rs b/stdlib/src/output/list.rs new file mode 100644 index 0000000..c8d210b --- /dev/null +++ b/stdlib/src/output/list.rs @@ -0,0 +1,46 @@ +use super::{OutputList, OutputPrinter}; +use crate::int; +use std::io::{self, Write}; + +/// Default output printer. +pub struct ListPrinter; + +impl OutputPrinter for ListPrinter { + fn print(self, list: OutputList, mut out: W) -> io::Result<()> + where + I: IntoIterator, + E: IntoIterator, + W: Write + { + writeln!(out, "Output Variable Analysis:")?; + for (key, stats) in list.stats() { + writeln!(out, " {key}: {stats:?}")?; + } + + writeln!(out, "Samples:")?; + let mut qty_buf = itoa::Buffer::new(); + let len_str = qty_buf.format(list.n); + let qty_spacing = len_str.len(); + for (entry, qty) in list.samples { + let qty_str = qty_buf.format(qty); + let qty_str_len = qty_str.len(); + write!(out, " ")?; + for _ in 0 .. qty_spacing - qty_str_len { + write!(out, " ")?; + } + write!( + out, + "{qty_str} ({:>6.2}%): {{ ", + qty as f32 / list.n as f32 * 100.0 + )?; + for (i, (key, value)) in entry.into_iter().enumerate() { + if i > 0 { + write!(out, ", ")?; + } + write!(out, "{key}: {value}")?; + } + writeln!(out, " }}")?; + } + Ok(()) + } +} diff --git a/stdlib/src/output/mod.rs b/stdlib/src/output/mod.rs new file mode 100644 index 0000000..0623c2e --- /dev/null +++ b/stdlib/src/output/mod.rs @@ -0,0 +1,322 @@ +#![allow(clippy::new_without_default)] + +use crate::int; +use indexmap::IndexMap; +use std::{ + error::Error, + fmt::{self, Display, Formatter}, + fs::File, + io::{self, stdout, Write}, + ops::{Deref, DerefMut}, + path::PathBuf, + str::FromStr +}; +use svgwriter::{tags::TagWithGlobalEventAttributes, Graphic, Value}; + +mod bar; +mod bubble; +mod list; + +pub use bar::BarChartPrinter; +pub use bubble::BubbleChartPrinter; +pub use list::ListPrinter; + +#[derive(Debug)] +pub struct NoSuchVariant(String); + +impl Display for NoSuchVariant { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + write!(f, "No such variant: {}", self.0) + } +} + +impl Error for NoSuchVariant {} + +/// Used for argument parsing +#[derive(Clone, Copy, Debug, Default)] +pub enum OutputPrinterAny { + #[default] + List, + Bar +} + +impl FromStr for OutputPrinterAny { + type Err = NoSuchVariant; + + fn from_str(s: &str) -> Result { + match s { + "list" => Ok(Self::List), + "bar" => Ok(Self::Bar), + _ => Err(NoSuchVariant(s.to_owned())) + } + } +} + +/// Used for argument parsing +#[derive(Clone, Copy, Debug, Default)] +pub enum OutputPrinterTwoVars { + #[default] + List, + Bar, + Bubble +} + +impl FromStr for OutputPrinterTwoVars { + type Err = NoSuchVariant; + + fn from_str(s: &str) -> Result { + match s { + "list" => Ok(Self::List), + "bar" => Ok(Self::Bar), + "bubble" => Ok(Self::Bubble), + _ => Err(NoSuchVariant(s.to_owned())) + } + } +} + +/// Used for output printing. +pub trait OutputPrinter { + fn print(self, list: OutputList, out: W) -> io::Result<()> + where + I: IntoIterator, + E: IntoIterator, + W: Write; +} + +/// Used for output printing. +pub struct OutputList { + /// An iterator over all samples. Must yield pairs of (values, qty), + /// where values is an iterator over (variable, value). + pub samples: I, + /// The total quantity of all samples. + pub n: usize, + /// The minimum of each output variable. + min: IndexMap<&'static str, int>, + /// The maximum of each output variable. + max: IndexMap<&'static str, int>, + /// The mean of each output variable. + mean: IndexMap<&'static str, f64>, + /// The variance of each output variable. + variance: IndexMap<&'static str, f64> +} + +#[derive(Debug)] +pub struct Stats { + pub min: int, + pub max: int, + pub mean: f64, + pub variance: f64, + pub standard_deviation: f64 +} + +impl OutputList +where + I: IntoIterator, + for<'a> &'a I: IntoIterator, + E: IntoIterator, + for<'a> &'a E: IntoIterator +{ + pub fn new(samples: I, n: usize) -> Self { + let mut min = IndexMap::new(); + let mut max = IndexMap::new(); + let mut mean = IndexMap::new(); + let mut qty = 0; + for (sample, sample_qty) in &samples { + let new_qty = (qty + sample_qty) as f64; + for (var, value) in sample { + if min.get(var).map(|v| *v > value).unwrap_or(true) { + min.insert(var, value); + } + if max.get(var).map(|v| *v < value).unwrap_or(true) { + max.insert(var, value); + } + mean.insert( + var, + mean.get(var).copied().unwrap_or(0.0) * (qty as f64 / new_qty) + + value as f64 * (*sample_qty as f64 / new_qty) + ); + } + qty += sample_qty; + } + debug_assert_eq!(qty, n); + + let mut variance = IndexMap::new(); + let mut qty; + let mut samples_iter = (&samples).into_iter(); + let (first_sample, first_sample_qty) = + samples_iter.next().expect("I need at least one sample"); + if *first_sample_qty > 1 { + qty = *first_sample_qty; + for (var, value) in first_sample { + variance.insert( + var, + (value as f64 - mean[var]).powi(2) * (qty as f64 / (qty - 1) as f64) + ); + } + } else { + let (second_sample, second_sample_qty) = + samples_iter.next().expect("I need at least two samples"); + let first_sample: IndexMap<&'static str, int> = + first_sample.into_iter().collect(); + qty = first_sample_qty + second_sample_qty; + for (var, second_value) in second_sample { + let first_value = first_sample[var]; + variance.insert( + var, + (first_value as f64 - mean[var]).powi(2) + * (*first_sample_qty as f64 / (qty - 1) as f64) + + (second_value as f64 - mean[var]).powi(2) + * (*second_sample_qty as f64 / (qty - 1) as f64) + ); + } + } + for (sample, sample_qty) in samples_iter { + let new_qty = (qty + sample_qty) as f64; + for (var, value) in sample { + variance.insert( + var, + variance[var] * ((qty - 1) as f64 / (new_qty - 1.0)) + + (value as f64 - mean[var]).powi(2) + * (*sample_qty as f64 / (new_qty - 1.0)) + ); + } + qty += sample_qty; + } + debug_assert_eq!(qty, n); + + Self { + samples, + n, + min, + max, + mean, + variance + } + } +} + +impl OutputList { + pub fn min(&self, var: &'static str) -> int { + self.min.get(var).copied().unwrap_or(0) + } + + pub fn max(&self, var: &'static str) -> int { + self.max.get(var).copied().unwrap_or(0) + } + + pub fn mean(&self, var: &'static str) -> f64 { + self.mean.get(var).copied().unwrap_or(0.0) + } + + pub fn variance(&self, var: &'static str) -> f64 { + self.variance.get(var).copied().unwrap_or(f64::INFINITY) + } + + pub fn stats(&self) -> impl Iterator + '_ { + self.mean.iter().map(|(key, mean)| { + let variance = self.variance(key); + (*key, Stats { + min: self.min(key), + max: self.max(key), + mean: *mean, + variance, + standard_deviation: variance.sqrt() + }) + }) + } +} + +/// Used for output printing. +pub trait OutputTarget { + fn print(self, list: OutputList) + where + I: IntoIterator, + E: IntoIterator; +} + +fn print_with(list: OutputList, printer: P, path: Option) +where + I: IntoIterator, + E: IntoIterator, + P: OutputPrinter +{ + match path { + Some(path) => { + let file = File::create(&path).expect("Failed to create output file"); + printer.print(list, file).expect("Failed to print output"); + eprintln!("Wrote output to {}", path.display()); + }, + None => { + printer + .print(list, stdout()) + .expect("Failed to print output"); + } + } +} + +impl OutputTarget for (OutputPrinterAny, Option) { + fn print(self, list: OutputList) + where + I: IntoIterator, + E: IntoIterator + { + match self.0 { + OutputPrinterAny::List => print_with(list, ListPrinter, self.1), + OutputPrinterAny::Bar => { + print_with(list, BarChartPrinter::new(self.1.is_none()), self.1) + }, + } + } +} + +impl OutputTarget for (OutputPrinterTwoVars, Option) { + fn print(self, list: OutputList) + where + I: IntoIterator, + E: IntoIterator + { + match self.0 { + OutputPrinterTwoVars::List => print_with(list, ListPrinter, self.1), + OutputPrinterTwoVars::Bar => { + print_with(list, BarChartPrinter::new(self.1.is_none()), self.1) + }, + OutputPrinterTwoVars::Bubble => { + print_with(list, BubbleChartPrinter::new(self.1.is_none()), self.1) + }, + } + } +} + +/// Helper for creating svgs +struct Svg(Graphic); + +impl Deref for Svg { + type Target = Graphic; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl DerefMut for Svg { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.0 + } +} + +impl Svg { + fn new(width: i32, height: i32) -> Self { + let mut svg = Graphic::new(); + svg.set_view_box(format!("0 0 {width} {height}")); + svg.set_height("20cm"); + Self(svg) + } + + fn with_onload(mut self, onload: V) -> Self + where + V: Value + 'static + { + self.0.set_onload(onload); + self + } +} diff --git a/templates/main.rs.j2 b/templates/main.rs.j2 new file mode 100644 index 0000000..f838b9c --- /dev/null +++ b/templates/main.rs.j2 @@ -0,0 +1,298 @@ +#![allow(unused_imports)] +#![warn(unreachable_pub)] +#![forbid(elided_lifetimes_in_paths, unsafe_code)] + +use bpaf::construct; +use indexmap::IndexMap; +use indicatif::{ParallelProgressIterator as _, ProgressBar, ProgressStyle}; +use rayon::iter::{FromParallelIterator, IntoParallelIterator, ParallelIterator as _}; +use stdlib::{ArgValue, OutputPrinterAny, OutputPrinterTwoVars, int}; +use std::{io::stdout, path::PathBuf}; + +#[derive(Clone, Debug)] +struct Args { + runs: u64, + + {%- for arg in inputs %} + in_{{arg.ident}}: ArgValue, + {%- endfor %} + + {%- for out in outputs %} + out{{loop.index0}}: (OutputPrinter + {%- if out.idents.len() == 2 %}TwoVars{% else %}Any{% endif -%} + , Option), + {%- endfor %} +} + +struct Values { + {%- for arg in inputs %} + {{arg.ident}}: int, + {%- endfor %} +} + +impl Args { + fn values(&self) -> Values { + Values { + {%- for arg in inputs %} + {{arg.ident}}: self.in_{{arg.ident}}.get_value(), + {%- endfor %} + } + } + + fn parse() -> Self { + let runs = bpaf::short('n') + .help("Specify the number of runs (defaults to 1)") + .argument("RUNS") + .from_str() + .fallback(1); + + {%- for arg in inputs %} + + let in_{{arg.ident}} = { + let value = bpaf::{{arg.short_long}}({{arg.quote}}{{arg.ident}}{{arg.quote}}) + .help("Set a value for {{arg.ident}}") + .argument("VALUE") + .from_str() + .map(ArgValue::Value); + let min = bpaf::long("{{arg.ident}}-min") + .help("Set a range for {{arg.ident}} with this minimum value (inclusive)") + .argument("MIN-VALUE") + .from_str(); + let max = bpaf::long("{{arg.ident}}-max") + .help("Set a range for {{arg.ident}} with this maximum value (inclusive)") + .argument("MAX-VALUE") + .from_str(); + let range = construct!(ArgValue::Range { min, max }); + value.or_else(range) + }; + + {%- endfor %} + + {%- for out in outputs %} + + let out{{loop.index0}} = { + let printer = bpaf::long(" + {%- for var in out.idents %}{{var}}-{% endfor %}printer") + .help("Set the output printer for output ( + {%- for var in out.idents %}{{var}}, {% endfor %})") + .argument("PRINTER") + .from_str() + .default(); + let path = bpaf::long(" + {%- for var in out.idents %}{{var}}-{% endfor %}out") + .help("Write the output to a file") + .argument("PATH") + .from_str() + .map(Some) + .default(); + construct!(printer, path) + }; + + {%- endfor %} + + let parser = construct!(Self { + runs, + {%- for arg in inputs %} + in_{{arg.ident}}, + {%- endfor %} + {%- for out in outputs %} + out{{loop.index0}} + {%- endfor %} + }); + let info = bpaf::Info::default(); + info.for_parser(parser).run() + } +} + +/// This is the output captured immediately after running one sample. +#[derive(Eq, Hash, PartialEq)] +struct Output { + {%- for out in outputs %} + {%- for var in out.idents %} + {{var}}: int, + {%- endfor %} + {%- endfor %} +} + +{%- for out in outputs %} + +/// This type is the output of one tuple from one sample. +#[derive(Eq, Hash, PartialEq)] +struct Output{{loop.index0}}( + {%- for var in out.idents %}int, {% endfor -%} +); + +impl IntoIterator for Output{{loop.index0}} { + type Item = (&'static str, int); + type IntoIter = OutputIntoIter{{loop.index0}}; + + fn into_iter(self) -> Self::IntoIter { + OutputIntoIter{{loop.index0}} { + output: self, + idx: 0 + } + } +} + +impl<'a> IntoIterator for &'a Output{{loop.index0}} { + type Item = (&'static str, int); + type IntoIter = OutputIter{{loop.index0}}<'a>; + + fn into_iter(self) -> Self::IntoIter { + OutputIter{{loop.index0}} { + output: self, + idx: 0 + } + } +} + +fn outputiter{{loop.index0}}_next( + output: &Output{{loop.index0}}, + idx: &mut usize +) -> Option<(&'static str, int)> { + let next = match *idx { + {%- for var in out.idents %} + {{loop.index0}} => (stringify!({{var}}), output.{{loop.index0}}), + {%- endfor %} + _ => return None + }; + *idx += 1; + Some(next) +} + +/// This type associates the output values with their variable names. +#[doc(hidden)] +struct OutputIntoIter{{loop.index0}} { + output: Output{{loop.index0}}, + idx: usize +} + +impl Iterator for OutputIntoIter{{loop.index0}} { + type Item = (&'static str, int); + + fn next(&mut self) -> Option { + outputiter{{loop.index0}}_next(&self.output, &mut self.idx) + } +} + +/// This type associates the output values with their variable names. +#[doc(hidden)] +struct OutputIter{{loop.index0}}<'a> { + output: &'a Output{{loop.index0}}, + idx: usize +} + +impl Iterator for OutputIter{{loop.index0}}<'_> { + type Item = (&'static str, int); + + fn next(&mut self) -> Option { + outputiter{{loop.index0}}_next(self.output, &mut self.idx) + } +} + +{%- endfor %} + +/// This type collects the output from each sample and processes them +/// into output values. It cannot use output iterators directly as we +/// need an `Eq` implementation. +#[derive(Default)] +struct OutputEntries { + {%- for out in outputs %} + entries{{loop.index0}}: IndexMap, + {%- endfor %} + len: usize +} + +impl FromParallelIterator for OutputEntries { + fn from_par_iter(par_iter: I) -> Self + where + I: IntoParallelIterator + { + par_iter + .into_par_iter() + .fold(Self::default, |mut list, {% if outputs.is_empty() %}_{% else %}out{% endif %}| { + list.len += 1; + {%- for out in outputs %} + list.entries{{loop.index0}} + .entry(Output{{loop.index0}}( + {%- for var in out.idents %}out.{{var}}, {% endfor -%} + )) + .and_modify(|qty| *qty += 1) + .or_insert(1); + {%- endfor %} + list + }) + .reduce(Self::default, |mut list, other| { + list.len += other.len; + {%- for out in outputs %} + for (out, count) in other.entries{{loop.index0}} { + list.entries{{loop.index0}} + .entry(out) + .and_modify(|qty| *qty += count) + .or_insert(count); + } + {%- endfor %} + list + }) + } +} + +/// This type is used to divide the output entries into output iterators. +struct OutputLists { + {%- for out in outputs %} + out{{loop.index0}}: stdlib::OutputList< + IndexMap + >, + {%- endfor %} +} + +impl From for OutputLists { + {%- if outputs.is_empty() %} + #[allow(unused_variables)] + {%- endif %} + fn from(this: OutputEntries) -> Self { + {%- for out in outputs %} + let mut out{{loop.index0}} = this.entries{{loop.index0}}; + out{{loop.index0}}.sort_unstable_by( + |_, lhs_qty, _, rhs_qty| rhs_qty.cmp(lhs_qty) + ); + + {% endfor -%} + Self { + {%- for out in outputs %} + out{{loop.index0}}: stdlib::OutputList::new( + out{{loop.index0}}, + this.len + ), + {%- endfor %} + } + } +} + +mod funs; +mod types; + +fn main() { + let args = Args::parse(); + let pb = ProgressBar::new(args.runs); + pb.set_prefix("Collecting samples ..."); + pb.set_style(ProgressStyle::default_bar() + .template("{prefix} {percent:>3}% [{wide_bar}] {pos:>7}/{len:7} [{elapsed_precise}<{eta_precise}; {per_sec} samples/s]") + .unwrap() + .progress_chars("#> ")); + {%- if outputs.is_empty() %} + #[allow(unused_variables)] + {%- endif %} + let out: OutputLists = (0 .. args.runs) + .into_par_iter() + .progress_with(pb) + .map(|_| funs::run(args.values())) + .collect::() + .into(); + {%- for out in outputs %} + {%- if !loop.first %} + println!(); + {%- endif %} + stdlib::OutputTarget::print(args.out{{loop.index0}}, out.out{{loop.index0}}); + {%- endfor %} +} diff --git a/templates/types.rs.j2 b/templates/types.rs.j2 new file mode 100644 index 0000000..f1f8e44 --- /dev/null +++ b/templates/types.rs.j2 @@ -0,0 +1,58 @@ +#![no_implicit_prelude] + +{%- for ty in typedefs %} + +pub(crate) struct {{ty.ident}}{{ty.generics|if_some}}({{ty.inner_ty}}); + +#[allow(dead_code)] +impl{{ty.generics|if_some}} {{ty.ident}}{{ty.generics|if_some}} { + pub(crate) fn new(arg: {{ty.inner_ty}}) -> Self { + Self(arg) + } + + pub(crate) fn into_inner(this: Self) -> {{ty.inner_ty}} { + this.0 + } +} + +impl{{ty.generics|if_some}} ::core::clone::Clone for {{ty.ident}}{{ty.generics|if_some}} +where + {{ty.inner_ty}}: ::core::clone::Clone +{ + fn clone(&self) -> Self { + Self::new(self.0.clone()) + } +} + +impl{{ty.generics|if_some}} ::core::ops::Deref for {{ty.ident}}{{ty.generics|if_some}} { + type Target = {{ty.inner_ty}}; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl{{ty.generics|if_some}} ::core::ops::DerefMut for {{ty.ident}}{{ty.generics|if_some}} { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.0 + } +} + +impl{{ty.generics|if_some}} ::core::fmt::Debug for {{ty.ident}}{{ty.generics|if_some}} +{%- match ty.generics %} +{%- when Some with (generics) %} +where + {%- for param in generics.params %} + {{param}}: ::core::fmt::Debug, + {%- endfor %} +{%- when _ %} +{%- endmatch %} +{ + fn fmt(&self, f: &mut ::core::fmt::Formatter<'_>) -> ::core::fmt::Result { + f.debug_tuple(::core::stringify!({{ty.ident}})) + .field(&self.0) + .finish() + } +} + +{%- endfor %} \ No newline at end of file