diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..219c13a --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,6 @@ +version: 2 +updates: + - package-ecosystem: "cargo" + directory: "/" + schedule: + interval: "daily" diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..5ef9059 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,90 @@ +name: CI + +on: + push: + branches: + - main + pull_request: + branches: + - "**" + +jobs: + doc: + name: Doc + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + - uses: dtolnay/rust-toolchain@stable + with: + toolchain: stable + - uses: Swatinem/rust-cache@v2 + - run: cargo doc --no-deps + + fmt: + name: Rustfmt + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + - uses: dtolnay/rust-toolchain@stable + with: + toolchain: nightly + components: rustfmt + - uses: Swatinem/rust-cache@v2 + - run: cargo fmt --all -- --check + + clippy: + name: Clippy + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + - uses: dtolnay/rust-toolchain@stable + with: + toolchain: stable + - uses: Swatinem/rust-cache@v2 + - run: cargo clippy --all-features --all-targets -- -D clippy::all + + unused_deps: + name: Unused dependencies + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + - uses: dtolnay/rust-toolchain@stable + with: + toolchain: nightly + - name: Install cargo-udeps + uses: baptiste0928/cargo-install@v3 + with: + crate: cargo-udeps + + - run: cargo +nightly udeps + + test: + name: Test + defaults: + run: + shell: bash + runs-on: ${{ matrix.os }} + strategy: + matrix: + os: + - ubuntu-latest + - macos-latest + - windows-latest + rust: + - stable + - nightly + + steps: + - uses: actions/checkout@v4 + - uses: dtolnay/rust-toolchain@stable + with: + toolchain: ${{ matrix.rust }} + + - uses: Swatinem/rust-cache@v2 + + - name: Test + run: cargo test --all-targets --no-fail-fast diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ea8c4bf --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +/target diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..8829998 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,775 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "anstream" +version = "0.6.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8acc5369981196006228e28809f761875c0327210a891e941f4c683b3a99529b" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55cc3b69f167a1ef2e161439aa98aed94e6028e5f9a59be9a6ffb47aef1651f9" + +[[package]] +name = "anstyle-parse" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b2d16507662817a6a20a9ea92df6652ee4f94f914589377d69f3b21bc5798a9" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79947af37f4177cfead1110013d678905c37501914fba0efea834c3fe9a8d60c" +dependencies = [ + "windows-sys 0.59.0", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2109dbce0e72be3ec00bed26e6a7479ca384ad226efdd66db8fa2e3a38c83125" +dependencies = [ + "anstyle", + "windows-sys 0.59.0", +] + +[[package]] +name = "autocfg" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" + +[[package]] +name = "bitflags" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "clap" +version = "4.5.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb3b4b9e5a7c7514dfa52869339ee98b3156b0bfb4e8a77c4ff4babb64b1604f" +dependencies = [ + "clap_builder", + "clap_derive", +] + +[[package]] +name = "clap_builder" +version = "4.5.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b17a95aa67cc7b5ebd32aa5370189aa0d79069ef1c64ce893bd30fb24bff20ec" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_derive" +version = "4.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ac6a0c7b1a9e9a5186361f67dfa1b88213572f427fb9ab038efb2bd8c582dab" +dependencies = [ + "heck", + "proc-macro2", + "quote 1.0.37", + "syn 2.0.89", +] + +[[package]] +name = "clap_lex" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "afb84c814227b90d6895e01398aee0d8033c00e7466aca416fb6a8e0eb19d8a7" + +[[package]] +name = "colorchoice" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990" + +[[package]] +name = "console" +version = "0.15.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e1f83fc076bd6dd27517eacdf25fef6c4dfe5f1d7448bafaaf3a26f13b5e4eb" +dependencies = [ + "encode_unicode", + "lazy_static", + "libc", + "windows-sys 0.52.0", +] + +[[package]] +name = "cranelift-bitset" +version = "0.112.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38da1eb6f7d8cdfa92f05acfae63c9a1d7a337e49ce7a2d0769c7fa03a2613a5" + +[[package]] +name = "cranelift-bitset" +version = "0.114.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "005884e3649c3e5ff2dc79e8a94b138f11569cc08a91244a292714d2a86e9156" + +[[package]] +name = "cranelift-entity" +version = "0.112.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70b85ed43567e13782cd1b25baf42a8167ee57169a60dfd3d7307c6ca3839da0" +dependencies = [ + "cranelift-bitset 0.112.3", +] + +[[package]] +name = "cranelift-entity" +version = "0.114.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "305399fd781a2953ac78c1396f02ff53144f39c33eb7fc7789cf4e8936d13a96" +dependencies = [ + "cranelift-bitset 0.114.0", +] + +[[package]] +name = "crossbeam-deque" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "613f8cc01fe9cf1a3eb3d7f488fd2fa8388403e97039e2f73692932e291a770d" +dependencies = [ + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22ec99545bb0ed0ea7bb9b8e1e9122ea386ff8a48c0922e43f36d45ab09e0e80" + +[[package]] +name = "crunchy" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a81dae078cea95a014a339291cec439d2f232ebe854a9d672b796c6afafa9b7" + +[[package]] +name = "dashmap" +version = "6.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5041cc499144891f3790297212f32a74fb938e5136a14943f338ef9e0ae276cf" +dependencies = [ + "cfg-if", + "crossbeam-utils", + "hashbrown 0.14.5", + "lock_api", + "once_cell", + "parking_lot_core", + "rayon", +] + +[[package]] +name = "dir-test" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b12781621d53fd9087021f5a338df5c57c04f84a6231c1f4726f45e2e333470b" +dependencies = [ + "dir-test-macros", +] + +[[package]] +name = "dir-test-macros" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1340852f50b2285d01a7f598cc5d08b572669c3e09e614925175cc3c26787b91" +dependencies = [ + "glob", + "proc-macro2", + "quote 1.0.37", + "syn 2.0.89", +] + +[[package]] +name = "dot2" +version = "1.0.0" +source = "git+https://github.com/sanpii/dot2.rs.git#fcd0f682b44e20b6eab96eade4ccfe6c1d119b97" + +[[package]] +name = "dyn-clone" +version = "1.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d6ef0072f8a535281e4876be788938b528e9a1d43900b82c2569af7da799125" + +[[package]] +name = "either" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0" + +[[package]] +name = "encode_unicode" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a357d28ed41a50f9c765dbfe56cbc04a64e53e5fc58ba79fbc34c10ef3df831f" + +[[package]] +name = "equivalent" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" + +[[package]] +name = "fixed-hash" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "835c052cb0c08c1acf6ffd71c022172e18723949c8282f2b9f27efbc51e64534" +dependencies = [ + "static_assertions", +] + +[[package]] +name = "glob" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b" + +[[package]] +name = "hashbrown" +version = "0.14.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" + +[[package]] +name = "hashbrown" +version = "0.15.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289" + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "indexmap" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "707907fe3c25f5424cce2cb7e1cbcafee6bdbe735ca90ef77c29e84591e5b9da" +dependencies = [ + "equivalent", + "hashbrown 0.15.2", +] + +[[package]] +name = "insta" +version = "1.41.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e9ffc4d4892617c50a928c52b2961cb5174b6fc6ebf252b2fac9d21955c48b8" +dependencies = [ + "console", + "lazy_static", + "linked-hash-map", + "similar", +] + +[[package]] +name = "is_terminal_polyfill" +version = "1.70.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + +[[package]] +name = "libc" +version = "0.2.166" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2ccc108bbc0b1331bd061864e7cd823c0cab660bbe6970e66e2c0614decde36" + +[[package]] +name = "linked-hash-map" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f" + +[[package]] +name = "lock_api" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17" +dependencies = [ + "autocfg", + "scopeguard", +] + +[[package]] +name = "once_cell" +version = "1.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775" + +[[package]] +name = "parking_lot_core" +version = "0.9.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-targets", +] + +[[package]] +name = "pest" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fce5d8b5cc33983fc74f78ad552b5522ab41442c4ca91606e4236eb4b5ceefc" + +[[package]] +name = "pest_derive" +version = "1.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca3294f437119209b084c797604295f40227cffa35c57220b1e99a6ff3bf8ee4" +dependencies = [ + "pest", + "quote 0.3.15", + "syn 0.11.11", +] + +[[package]] +name = "phf" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ade2d8b8f33c7333b51bcf0428d37e217e9f32192ae4772156f65063b8ce03dc" +dependencies = [ + "phf_macros", + "phf_shared", +] + +[[package]] +name = "phf_generator" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48e4cc64c2ad9ebe670cb8fd69dd50ae301650392e81c05f9bfcb2d5bdbc24b0" +dependencies = [ + "phf_shared", + "rand", +] + +[[package]] +name = "phf_macros" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3444646e286606587e49f3bcf1679b8cef1dc2c5ecc29ddacaffc305180d464b" +dependencies = [ + "phf_generator", + "phf_shared", + "proc-macro2", + "quote 1.0.37", + "syn 2.0.89", +] + +[[package]] +name = "phf_shared" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90fcb95eef784c2ac79119d1dd819e162b5da872ce6f3c3abe1e8ca1c082f72b" +dependencies = [ + "siphasher", +] + +[[package]] +name = "primitive-types" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d15600a7d856470b7d278b3fe0e311fe28c2526348549f8ef2ff7db3299c87f5" +dependencies = [ + "fixed-hash", + "uint", +] + +[[package]] +name = "proc-macro2" +version = "1.0.92" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37d3544b3f2748c54e147655edb5025752e2303145b5aefb3c3ea2c78b973bb0" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "0.3.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a6e920b65c65f10b2ae65c831a81a073a89edd28c7cce89475bff467ab4167a" + +[[package]] +name = "quote" +version = "1.0.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5b9d34b8991d19d98081b46eacdd8eb58c6f2b201139f7c5f643cc155a633af" +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" + +[[package]] +name = "rayon" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b418a60154510ca1a002a752ca9714984e21e4241e804d32555251faf8b78ffa" +dependencies = [ + "either", + "rayon-core", +] + +[[package]] +name = "rayon-core" +version = "1.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1465873a3dfdaa8ae7cb14b4383657caab0b3e8a0aa9ae8e04b044854c8dfce2" +dependencies = [ + "crossbeam-deque", + "crossbeam-utils", +] + +[[package]] +name = "redox_syscall" +version = "0.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b6dfecf2c74bce2466cabf93f6664d6998a69eb21e39f4207930065b27b771f" +dependencies = [ + "bitflags", +] + +[[package]] +name = "rustc-hash" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "583034fd73374156e66797ed8e5b0d5690409c9226b22d87cb7f19821c05d152" + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "similar" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1de1d4f81173b03af4c0cbed3c898f6bff5b870e4a7f5d6f4057d62a7a4b686e" + +[[package]] +name = "siphasher" +version = "0.3.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38b58827f4464d87d377d175e90bf58eb00fd8716ff0a62f80356b5e61555d0d" + +[[package]] +name = "smallvec" +version = "1.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" + +[[package]] +name = "sonatina-ir" +version = "0.0.3-alpha" +source = "git+https://github.com/Y-Nak/sonatina?branch=refactor-ir-writer#b737cb687c130ff9a7eddc0f94399013bb2117a1" +dependencies = [ + "cranelift-entity 0.114.0", + "dashmap", + "dot2", + "dyn-clone", + "indexmap", + "primitive-types", + "rayon", + "rustc-hash", + "smallvec", + "sonatina-macros", + "sonatina-triple", +] + +[[package]] +name = "sonatina-macros" +version = "0.0.3-alpha" +source = "git+https://github.com/Y-Nak/sonatina?branch=refactor-ir-writer#b737cb687c130ff9a7eddc0f94399013bb2117a1" +dependencies = [ + "proc-macro2", + "quote 1.0.37", + "syn 2.0.89", +] + +[[package]] +name = "sonatina-triple" +version = "0.0.3-alpha" +source = "git+https://github.com/Y-Nak/sonatina?branch=refactor-ir-writer#b737cb687c130ff9a7eddc0f94399013bb2117a1" +dependencies = [ + "thiserror", +] + +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "syn" +version = "0.11.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3b891b9015c88c576343b9b3e41c2c11a51c219ef067b264bd9c8aa9b441dad" +dependencies = [ + "quote 0.3.15", + "synom", + "unicode-xid", +] + +[[package]] +name = "syn" +version = "2.0.89" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44d46482f1c1c87acd84dea20c1bf5ebff4c757009ed6bf19cfd36fb10e92c4e" +dependencies = [ + "proc-macro2", + "quote 1.0.37", + "unicode-ident", +] + +[[package]] +name = "synom" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a393066ed9010ebaed60b9eafa373d4b1baac186dd7e008555b0f702b51945b6" +dependencies = [ + "unicode-xid", +] + +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote 1.0.37", + "syn 2.0.89", +] + +[[package]] +name = "uint" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "909988d098b2f738727b161a106cfc7cab00c539c2687a8836f8e565976fb53e" +dependencies = [ + "byteorder", + "crunchy", + "hex", + "static_assertions", +] + +[[package]] +name = "unicode-ident" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "adb9e6ca4f869e1180728b7950e35922a7fc6397f7b641499e8f3ef06e50dc83" + +[[package]] +name = "unicode-xid" +version = "0.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c1f860d7d29cf02cb2f3f359fd35991af3d30bac52c57d265a3c461074cb4dc" + +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_gnullvm", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "yul-sonatina" +version = "0.1.0" +dependencies = [ + "clap", + "cranelift-entity 0.112.3", + "dir-test", + "insta", + "sonatina-ir", + "sonatina-triple", + "yultsur", +] + +[[package]] +name = "yultsur" +version = "0.1.0" +source = "git+https://github.com/fe-lang/yultsur?branch=parser#f5c32c435f86175c0b7b1463445070bbb85213b3" +dependencies = [ + "pest", + "pest_derive", + "phf", +] diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..c79afc8 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "yul-sonatina" +version = "0.1.0" +edition = "2021" + +[dependencies] +yultsur = { git = "https://github.com/fe-lang/yultsur", branch = "parser" } +sonatina-ir = { git = "https://github.com/Y-Nak/sonatina", branch = "refactor-ir-writer", package = "sonatina-ir" } +sonatina-triple = { git = "https://github.com/Y-Nak/sonatina", branch = "refactor-ir-writer", package = "sonatina-triple" } +cranelift-entity = "0.112" +clap = { version = "4.5.21", features = ["derive"] } + +[dev-dependencies] +dir-test = "0.4.0" +insta = { version = "1.41" } diff --git a/build.rs b/build.rs new file mode 100644 index 0000000..8e048f9 --- /dev/null +++ b/build.rs @@ -0,0 +1,4 @@ +fn main() { + #[cfg(test)] + println!("cargo:rerun-if-changed=./test_files"); +} diff --git a/src/func.rs b/src/func.rs new file mode 100644 index 0000000..7da4cf9 --- /dev/null +++ b/src/func.rs @@ -0,0 +1,727 @@ +use std::collections::HashMap; + +use sonatina_ir::{ + builder::FunctionBuilder, + func_cursor::InstInserter, + inst::{arith::*, cast::*, cmp::*, control_flow::*, data::*, evm::*, logic::*}, + BlockId, Inst, Type, ValueId, Variable, I256, +}; +use yultsur::yul::{ + Block as YulBlock, Expression, FunctionCall, FunctionDefinition, Identifier, + Literal as YulLiteral, Statement, +}; + +use crate::{Ctx, Literal, ObjectItem}; + +pub struct FuncTranspiler<'ctx> { + ctx: &'ctx mut Ctx, + func_ctx: FuncCtx, + builder: FunctionBuilder, + ret_vars: Vec, +} + +impl<'ctx> FuncTranspiler<'ctx> { + pub fn new(ctx: &'ctx mut Ctx, builder: FunctionBuilder) -> Self { + Self { + ctx, + func_ctx: FuncCtx::new(), + builder, + ret_vars: Vec::new(), + } + } + + pub fn build(mut self, yul_func: &FunctionDefinition) { + for arg in &yul_func.parameters { + self.func_ctx.declare_var(&mut self.builder, &arg.name); + } + + for ret in &yul_func.returns { + self.ret_vars.push(ret.name.clone()); + self.func_ctx.declare_var(&mut self.builder, &ret.name); + } + + let entry_bb = self.builder.append_block(); + self.builder.switch_to_block(entry_bb); + + for (i, yul_var) in yul_func.parameters.iter().enumerate() { + let yul_var = self.func_ctx.lookup_var(&yul_var.name); + let arg_val = self.builder.args()[i]; + self.builder.def_var(yul_var, arg_val); + } + + self.block(&yul_func.body, true, None); + self.builder.seal_all(); + self.builder.finish(); + } + + /// Returns `true` if the statement is the terminator. + fn stmt(&mut self, yul_stmt: &Statement) { + let inst_set = self.builder.inst_set(); + + match yul_stmt { + Statement::Block(block) => self.block(block, true, None), + + Statement::FunctionDefinition(_) => { + // We can just ignore function definitions. + // Function definitions are handled by the context. + } + + Statement::VariableDeclaration(yul_decl) => { + let yul_vars = &yul_decl.variables; + for yul_var in yul_vars { + self.func_ctx.declare_var(&mut self.builder, &yul_var.name); + } + + let Some(expr) = &yul_decl.value else { + return; + }; + let value = self.expr(expr).unwrap(); + self.func_ctx.def_var(&mut self.builder, yul_vars, value); + } + + Statement::Assignment(assign) => { + let value = self.expr(&assign.value).unwrap(); + self.func_ctx + .def_var(&mut self.builder, &assign.variables, value); + } + + Statement::Expression(expr) => { + self.expr(expr); + let last_inst = self.builder.last_inst().unwrap(); + if self.builder.is_terminator(last_inst) { + self.builder.seal_block(); + } + } + + Statement::If(if_) => { + let then_bb = self.builder.append_block(); + let merge_bb = self.builder.append_block(); + + let cond = self.expr(&if_.condition).unwrap(); + let br = Br::new_unchecked(inst_set, cond, then_bb, merge_bb); + self.builder.insert_inst_no_result(br); + + // Enter then block. + self.builder.switch_to_block(then_bb); + self.block(&if_.body, true, Some(merge_bb)); + + // Switch to merge block. + self.builder.switch_to_block(merge_bb); + } + + Statement::Switch(switch) => { + let scrutinee = self.expr(&switch.expression).unwrap(); + + // Collect cases. + let labeled: Vec<(ValueId, BlockId, &YulBlock)> = switch + .cases + .iter() + .flat_map(|case| { + let yul_lit = case.literal.as_ref()?; + let lit_value = self.lit(yul_lit); + let bb = self.builder.append_block(); + Some((lit_value, bb, &case.body)) + }) + .collect(); + let default = switch.cases.iter().find_map(|case| { + if case.literal.is_some() { + return None; + } + let bb = self.builder.append_block(); + Some((bb, &case.body)) + }); + + // Make and append `br_table` inst. + let merge_bb = self.builder.append_block(); + + let table = labeled.iter().map(|(value, bb, _)| (*value, *bb)).collect(); + let br_inst = if let Some((default, _)) = default { + BrTable::new_unchecked(inst_set, scrutinee, Some(default), table) + } else { + BrTable::new_unchecked(inst_set, scrutinee, Some(merge_bb), table) + }; + self.builder.insert_inst_no_result(br_inst); + self.builder.seal_block(); + + // Transpile cases. + for (_, bb, yul_block) in labeled { + self.builder.switch_to_block(bb); + self.block(yul_block, true, Some(merge_bb)); + } + if let Some((bb, yul_block)) = default { + self.builder.switch_to_block(bb); + self.block(yul_block, true, Some(merge_bb)); + } + + self.builder.switch_to_block(merge_bb); + } + + Statement::ForLoop(for_) => { + let lp_header = self.builder.append_block(); + let lp_body = self.builder.append_block(); + let lp_post = self.builder.append_block(); + let lp_exit = self.builder.append_block(); + + // Lower pre block. + // Yul's scoping rule about `for` loop is weird. + // See https://docs.soliditylang.org/en/latest/yul.html#scoping-rules + self.enter_scope(&for_.pre); + self.block(&for_.pre, false, Some(lp_header)); + + // Lower loop header. + self.builder.switch_to_block(lp_header); + let cond = self.expr(&for_.condition).unwrap(); + let br = Br::new_unchecked(inst_set, cond, lp_body, lp_exit); + self.builder.insert_inst_no_result(br); + + // Lower loop body. + self.func_ctx.loop_stack.push((lp_post, lp_exit)); + self.builder.switch_to_block(lp_body); + self.block(&for_.body, true, Some(lp_post)); + self.func_ctx.loop_stack.pop(); + + // Lower loop post block. + self.builder.switch_to_block(lp_post); + self.block(&for_.post, true, Some(lp_header)); + + // Leave loop scope. + self.builder.switch_to_block(lp_header); + self.builder.seal_block(); + self.leave_scope(); + + self.builder.switch_to_block(lp_exit); + } + + Statement::Break => { + let break_dest = self.func_ctx.loop_stack.last().unwrap().1; + let jump = Jump::new_unchecked(inst_set, break_dest); + self.builder.insert_inst_no_result(jump); + self.builder.seal_block(); + } + + Statement::Continue => { + let break_dest = self.func_ctx.loop_stack.last().unwrap().0; + let jump = Jump::new_unchecked(inst_set, break_dest); + self.builder.insert_inst_no_result(jump); + self.builder.seal_block(); + } + + Statement::Leave => self.insert_return(), + } + } + + fn enter_scope(&mut self, yul_block: &YulBlock) { + self.ctx.enter_block(yul_block); + self.func_ctx.variables.push(HashMap::default()); + } + + fn leave_scope(&mut self) { + self.func_ctx.variables.pop().unwrap(); + self.ctx.leave_block(); + } + + fn block(&mut self, yul_block: &YulBlock, enter_scope: bool, fallback_to: Option) { + if enter_scope { + self.enter_scope(yul_block); + } + + for yul_stmt in &yul_block.statements { + self.stmt(yul_stmt); + let current_bb = self.builder.current_block().unwrap(); + if self.builder.is_sealed(current_bb) { + self.ctx.leave_block(); + return; + } + } + + let current_bb = self.builder.current_block().unwrap(); + if !self.builder.is_sealed(current_bb) { + self.builder.switch_to_block(current_bb); + match fallback_to { + Some(bb) => { + let inst_set = self.builder.inst_set(); + let jump = Jump::new_unchecked(inst_set, bb); + self.builder.insert_inst_no_result(jump); + } + None => { + self.insert_return(); + } + } + + self.builder.seal_block(); + } + + if enter_scope { + self.leave_scope(); + } + } + + fn insert_return(&mut self) { + let inst_set = self.builder.inst_set(); + + let ret_val = match self.ret_vars.len() { + 0 => None, + 1 => { + let var = self.func_ctx.lookup_var(&self.ret_vars[0]); + Some(self.builder.use_var(var)) + } + _ => { + let ret_ty = self.builder.func_sig().ret_ty(); + let mut ret_val = self.builder.make_undef_value(ret_ty); + for (i, yul_var) in self.ret_vars.iter().enumerate() { + let var = self.func_ctx.lookup_var(yul_var); + let elem = self.builder.use_var(var); + let idx = self.builder.make_imm_value(I256::from_usize(i)); + let insert_value = InsertValue::new_unchecked(inst_set, ret_val, idx, elem); + ret_val = self.builder.insert_inst(insert_value, ret_ty); + } + + Some(ret_val) + } + }; + + let ret_inst = Return::new_unchecked(inst_set, ret_val); + self.builder.insert_inst_no_result(ret_inst); + self.builder.seal_block(); + } + + fn expr(&mut self, yul_expr: &Expression) -> Option { + match yul_expr { + Expression::Literal(lit) => Some(self.lit(lit)), + Expression::Identifier(ident) => { + let var = self.func_ctx.lookup_var(&ident.name); + Some(self.builder.use_var(var)) + } + Expression::FunctionCall(call) => self.func_call(call), + } + } + + fn lit(&mut self, yul_lit: &YulLiteral) -> ValueId { + let lit = Literal::parse(yul_lit).as_imm(); + self.builder.make_imm_value(lit) + } + + fn func_call(&mut self, yul_func_call: &FunctionCall) -> Option { + let callee_name = yul_func_call.function.name.as_str(); + let m_ctx = self.builder.ctx(); + let inst_set = self.builder.ctx().inst_set; + + match callee_name { + "dataoffset" => { + let arg = &yul_func_call.arguments[0]; + let Literal::String(symbol) = (match arg { + Expression::Literal(yul_lit) => Literal::parse(yul_lit), + _ => unreachable!(), + }) else { + unreachable!() + }; + + let item = self.ctx.lookup_item(&symbol).unwrap(); + let ptr = match item { + ObjectItem::ContractCode(func_ref) => { + let gfp = GetFunctionPtr::new_unchecked(inst_set, func_ref); + self.builder + .insert_inst(gfp, func_ref.as_ptr_ty(self.builder.ctx())) + } + + ObjectItem::GlobalVariable(variable) => { + self.builder.make_global_value(variable) + } + }; + + return Some(self.ptoi(ptr)); + } + + "datasize" => { + let arg = &yul_func_call.arguments[0]; + let Literal::String(symbol) = (match arg { + Expression::Literal(yul_lit) => Literal::parse(yul_lit), + _ => unreachable!(), + }) else { + unreachable!() + }; + let item = self.ctx.lookup_item(&symbol).unwrap(); + + match item { + ObjectItem::ContractCode(func_ref) => { + let contract_size = EvmContractSize::new_unchecked(inst_set, func_ref); + return Some(self.builder.insert_inst(contract_size, Type::I256)); + } + + ObjectItem::GlobalVariable(variable) => { + let ty = variable.ty(self.builder.ctx()); + let size = m_ctx.type_layout.size_of(ty, m_ctx).unwrap(); + let value = self.builder.make_imm_value(I256::from_usize(size)); + return Some(value); + } + } + } + + // Fallback to normal instruction call handling. + _ => {} + }; + + let args: Vec<_> = yul_func_call + .arguments + .iter() + .map(|expr| self.expr(expr).unwrap()) + .collect(); + + let (inst, has_ret): (Box, _) = match callee_name { + "stop" => (Box::new(EvmStop::new_unchecked(inst_set)), false), + + "add" => ( + Box::new(Add::new_unchecked(inst_set, args[0], args[1])), + true, + ), + "sub" => ( + Box::new(Sub::new_unchecked(inst_set, args[0], args[1])), + true, + ), + "mul" => ( + Box::new(Mul::new_unchecked(inst_set, args[0], args[1])), + true, + ), + "div" => ( + Box::new(EvmUdiv::new_unchecked(inst_set, args[0], args[1])), + true, + ), + "sdiv" => ( + Box::new(EvmSdiv::new_unchecked(inst_set, args[0], args[1])), + true, + ), + "mod" => ( + Box::new(EvmUmod::new_unchecked(inst_set, args[0], args[1])), + true, + ), + "smod" => ( + Box::new(EvmSmod::new_unchecked(inst_set, args[0], args[1])), + true, + ), + "exp" => ( + Box::new(EvmExp::new_unchecked(inst_set, args[0], args[1])), + true, + ), + "not" => (Box::new(Not::new_unchecked(inst_set, args[0])), true), + "lt" => ( + Box::new(Lt::new_unchecked(inst_set, args[0], args[1])), + true, + ), + "gt" => ( + Box::new(Gt::new_unchecked(inst_set, args[0], args[1])), + true, + ), + "slt" => ( + Box::new(Slt::new_unchecked(inst_set, args[0], args[1])), + true, + ), + "sgt" => ( + Box::new(Sgt::new_unchecked(inst_set, args[0], args[1])), + true, + ), + "eq" => ( + Box::new(Eq::new_unchecked(inst_set, args[0], args[1])), + true, + ), + "iszero" => (Box::new(IsZero::new_unchecked(inst_set, args[0])), true), + "and" => ( + Box::new(And::new_unchecked(inst_set, args[0], args[1])), + true, + ), + "or" => ( + Box::new(Or::new_unchecked(inst_set, args[0], args[1])), + true, + ), + "byte" => ( + Box::new(EvmByte::new_unchecked(inst_set, args[0], args[1])), + true, + ), + "shl" => ( + Box::new(Shl::new_unchecked(inst_set, args[0], args[1])), + true, + ), + "shr" => ( + Box::new(Shr::new_unchecked(inst_set, args[0], args[1])), + true, + ), + "sar" => ( + Box::new(Sar::new_unchecked(inst_set, args[0], args[1])), + true, + ), + "addmod" => ( + Box::new(EvmAddMod::new_unchecked( + inst_set, args[0], args[1], args[2], + )), + true, + ), + "mulmod" => ( + Box::new(EvmMulMod::new_unchecked( + inst_set, args[0], args[1], args[2], + )), + true, + ), + "signextend" => todo!("Add EvmSignExtend?"), + "keccak256" => ( + Box::new(EvmKeccak256::new_unchecked(inst_set, args[0], args[1])), + true, + ), + "pop" => return None, + "mload" => { + let ptr = self.itop(args[0]); + ( + Box::new(Mload::new_unchecked(inst_set, ptr, Type::I256)), + true, + ) + } + "mstore" => { + let ptr = self.itop(args[0]); + ( + Box::new(Mstore::new_unchecked(inst_set, ptr, args[1], Type::I256)), + false, + ) + } + "mstore8" => ( + Box::new(EvmMstore8::new_unchecked(inst_set, args[0], args[1])), + false, + ), + "sload" => (Box::new(EvmSload::new_unchecked(inst_set, args[0])), true), + "sstore" => ( + Box::new(EvmSstore::new_unchecked(inst_set, args[0], args[1])), + false, + ), + "msize" => (Box::new(EvmMsize::new_unchecked(inst_set)), true), + "gas" => (Box::new(EvmGas::new_unchecked(inst_set)), true), + "address" => (Box::new(EvmGas::new_unchecked(inst_set)), true), + "balance" => (Box::new(EvmBalance::new_unchecked(inst_set, args[0])), true), + "selfbalance" => (Box::new(EvmSelfBalance::new_unchecked(inst_set)), true), + "caller" => (Box::new(EvmCaller::new_unchecked(inst_set)), true), + "callvalue" => (Box::new(EvmCallValue::new_unchecked(inst_set)), true), + "calldataload" => ( + Box::new(EvmCallDataLoad::new_unchecked(inst_set, args[0])), + true, + ), + "calldatasize" => (Box::new(EvmCallDataSize::new_unchecked(inst_set)), true), + "calldatacopy" => ( + Box::new(EvmCallDataCopy::new_unchecked( + inst_set, args[0], args[1], args[2], + )), + false, + ), + "codesize" => (Box::new(EvmCodeSize::new_unchecked(inst_set)), true), + "codecopy" => ( + Box::new(EvmCodeCopy::new_unchecked( + inst_set, args[0], args[1], args[2], + )), + false, + ), + "extcodesize" => ( + Box::new(EvmExtCodeSize::new_unchecked(inst_set, args[0])), + true, + ), + "extcodecopy" => ( + Box::new(EvmExtCodeCopy::new_unchecked( + inst_set, args[0], args[1], args[2], args[3], + )), + false, + ), + "returndatasize" => (Box::new(EvmReturnDataSize::new_unchecked(inst_set)), true), + "returndatacopy" => ( + Box::new(EvmReturnDataCopy::new_unchecked( + inst_set, args[0], args[1], args[2], + )), + false, + ), + "mcopy" => ( + Box::new(EvmMcopy::new_unchecked(inst_set, args[0], args[1], args[2])), + false, + ), + "extcodehash" => ( + Box::new(EvmExtCodeHash::new_unchecked(inst_set, args[0])), + true, + ), + "create" => ( + Box::new(EvmCreate::new_unchecked( + inst_set, args[0], args[1], args[2], + )), + true, + ), + "create2" => ( + Box::new(EvmCreate2::new_unchecked( + inst_set, args[0], args[1], args[2], args[3], + )), + true, + ), + "call" => ( + Box::new(EvmCall::new_unchecked( + inst_set, args[0], args[1], args[2], args[3], args[4], args[5], args[6], + )), + true, + ), + "callcode" => ( + Box::new(EvmCallCode::new_unchecked( + inst_set, args[0], args[1], args[2], args[3], args[4], args[5], args[6], + )), + true, + ), + "delegatecall" => ( + Box::new(EvmDelegateCall::new_unchecked( + inst_set, args[0], args[1], args[2], args[3], args[4], args[5], + )), + true, + ), + "staticcall" => ( + Box::new(EvmStaticCall::new_unchecked( + inst_set, args[0], args[1], args[2], args[3], args[4], args[5], + )), + true, + ), + "return" => ( + Box::new(EvmReturn::new_unchecked(inst_set, args[0], args[1])), + false, + ), + "revert" => ( + Box::new(EvmRevert::new_unchecked(inst_set, args[0], args[1])), + false, + ), + "selfdestruct" => ( + Box::new(EvmSelfDestruct::new_unchecked(inst_set, args[0])), + false, + ), + "invalid" => (Box::new(EvmInvalid::new_unchecked(inst_set)), false), + "log0" => ( + Box::new(EvmLog0::new_unchecked(inst_set, args[0], args[1])), + false, + ), + "log1" => ( + Box::new(EvmLog1::new_unchecked(inst_set, args[0], args[1], args[2])), + false, + ), + "log2" => ( + Box::new(EvmLog2::new_unchecked( + inst_set, args[0], args[1], args[2], args[3], + )), + false, + ), + "log3" => ( + Box::new(EvmLog3::new_unchecked( + inst_set, args[0], args[1], args[2], args[3], args[4], + )), + false, + ), + "log4" => ( + Box::new(EvmLog4::new_unchecked( + inst_set, args[0], args[1], args[2], args[3], args[4], args[5], + )), + false, + ), + "chainid" => (Box::new(EvmChainId::new_unchecked(inst_set)), true), + "basefee" => (Box::new(EvmBaseFee::new_unchecked(inst_set)), true), + "blobbasefee" => (Box::new(EvmBlobBaseFee::new_unchecked(inst_set)), true), + "origin" => (Box::new(EvmOrigin::new_unchecked(inst_set)), true), + "gasprice" => (Box::new(EvmGasPrice::new_unchecked(inst_set)), true), + "blobhash" => ( + Box::new(EvmBlockHash::new_unchecked(inst_set, args[0])), + true, + ), + "coinbase" => ( + Box::new(EvmCoinBase::new_unchecked(inst_set, args[0])), + true, + ), + "timestamp" => (Box::new(EvmTimestamp::new_unchecked(inst_set)), true), + "number" => (Box::new(EvmNumber::new_unchecked(inst_set)), true), + "difficulty" => panic!("`difficulty` is no longer supported"), + "prevrandao" => (Box::new(EvmPrevRandao::new_unchecked(inst_set)), true), + "gaslimit" => (Box::new(EvmGasLimit::new_unchecked(inst_set)), true), + "datacopy" => ( + Box::new(EvmCodeCopy::new_unchecked( + inst_set, args[0], args[1], args[2], + )), + false, + ), + + f => { + let callee = self.ctx.lookup_func(f).unwrap(); + let ret_ty = self.builder.module_builder.sig(callee, |sig| sig.ret_ty()); + ( + Box::new(Call::new_unchecked(inst_set, callee, args.into())), + !ret_ty.is_unit(), + ) + } + }; + + if has_ret { + Some(self.builder.insert_inst_dyn(inst, Type::I256)) + } else { + self.builder.insert_inst_no_result_dyn(inst); + None + } + } + + fn ptoi(&mut self, ptr: ValueId) -> ValueId { + let ptoi = PtrToInt::new_unchecked(self.builder.ctx().inst_set, ptr, Type::I256); + self.builder.insert_inst(ptoi, Type::I256) + } + + fn itop(&mut self, ptr: ValueId) -> ValueId { + let m_ctx = self.builder.ctx(); + let ptr_ty = Type::I256.to_ptr(m_ctx); + let ptoi = IntToPtr::new_unchecked(m_ctx.inst_set, ptr, ptr_ty); + self.builder.insert_inst(ptoi, ptr_ty) + } +} + +struct FuncCtx { + variables: Vec>, + // (ContinueDest, BreakDest) + loop_stack: Vec<(BlockId, BlockId)>, +} + +impl FuncCtx { + fn new() -> Self { + Self { + variables: vec![HashMap::default()], + loop_stack: Vec::new(), + } + } + + fn declare_var(&mut self, builder: &mut FunctionBuilder, name: &str) -> Variable { + let var = builder.declare_var(Type::I256); + self.variables + .last_mut() + .unwrap() + .insert(name.to_string(), var); + var + } + + fn def_var( + &mut self, + builder: &mut FunctionBuilder, + yul_vars: &[Identifier], + value: ValueId, + ) { + let inst_set = builder.inst_set(); + + if yul_vars.len() == 1 { + let var = self.lookup_var(&yul_vars[0].name); + builder.def_var(var, value); + } else { + for (i, yul_var) in yul_vars.iter().enumerate() { + let idx = builder.make_imm_value(I256::from_usize(i)); + let extract = ExtractValue::new_unchecked(inst_set, value, idx); + let elem_value = builder.insert_inst(extract, Type::I256); + let var = self.lookup_var(&yul_var.name); + builder.def_var(var, elem_value); + } + } + } + + fn lookup_var(&self, name: &str) -> Variable { + for vars in self.variables.iter().rev() { + if let Some(var) = vars.get(name) { + return *var; + } + } + + panic!("variable `{name}` is undefined"); + } +} diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..cabb7ed --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,376 @@ +pub mod func; + +use std::collections::HashMap; + +use func::FuncTranspiler; +use sonatina_ir::{ + builder::ModuleBuilder, + global_variable::GvInitializer, + isa::evm::Evm, + module::{FuncRef, ModuleCtx}, + GlobalVariable, GlobalVariableData, Linkage, Module, Signature, Type, I256, U256, +}; +use sonatina_triple::{Architecture, EvmVersion, OperatingSystem, TargetTriple, Vendor}; +use yultsur::{ + yul::{ + self, Block as YulBlock, Data, FunctionDefinition, Identifier, IdentifierID, InnerRoot, + Object, Statement, + }, + yul_parser::parse_root, +}; + +const TRIPLE: TargetTriple = TargetTriple::new( + Architecture::Evm, + Vendor::Ethereum, + OperatingSystem::Evm(EvmVersion::Cancun), +); + +pub fn compile(src: &str) -> Result { + let root = parse_root(src)?; + let mut ctx = Ctx::new(); + + match &root.inner { + InnerRoot::Object(obj) => { + ctx.compile_object(obj); + } + InnerRoot::Block(yul_block) => ctx.enter_block(yul_block), + }; + + Ok(ctx.finish()) +} + +#[derive(Debug, Clone)] +pub enum Scope { + Nested(Vec), + Block(usize), + Func(String), + Object(String), + Root, +} + +impl Scope { + fn pop(&mut self) -> Option { + match self { + Self::Nested(v) => v.pop(), + _ => None, + } + } + + fn push(&mut self, scope: Scope) { + assert!(!matches!(scope, Scope::Nested(_))); + match self { + Self::Nested(n) => n.push(scope), + _ => *self = Self::Nested(vec![self.clone(), scope]), + } + } + + fn make_prefixed_name(&self, name: &str) -> String { + let prefix = self.prefix(); + + if prefix.is_empty() { + name.to_string() + } else { + format!("{prefix}::{name}") + } + } + + fn prefix(&self) -> String { + match self { + Self::Nested(scopes) => { + let mut prefix = String::new(); + for s in scopes { + if !prefix.is_empty() { + prefix.push_str("::"); + } + prefix.push_str(&s.prefix()); + } + prefix + } + + Self::Block(bn) => { + format!("block{bn}") + } + + Self::Func(name) | Self::Object(name) => name.clone(), + + Self::Root => String::new(), + } + } + + fn object(obj: &Object) -> Self { + let name = strip_enclosing_quote(&obj.name); + Self::Object(name.to_string()) + } +} + +pub struct Ctx { + funcs: Vec>, + object_items: Vec>, + scope: Scope, + block_number: usize, + mb: ModuleBuilder, + declared_ret_tys: HashMap, +} + +impl Ctx { + pub fn new() -> Self { + let module_ctx = ModuleCtx::new(&Evm::new(TRIPLE)); + let mb = ModuleBuilder::new(module_ctx); + Self { + funcs: Vec::new(), + object_items: Vec::new(), + scope: Scope::Root, + block_number: 0, + mb, + declared_ret_tys: HashMap::new(), + } + } + + pub fn finish(self) -> Module { + self.mb.build() + } + + fn lookup_func(&self, name: &str) -> Option { + for scope in self.funcs.iter().rev() { + if let Some(func) = scope.get(name) { + return Some(*func); + } + } + None + } + + fn lookup_item(&self, name: &str) -> Option { + for scope in &self.object_items { + if let Some(item) = scope.get(name) { + return Some(*item); + } + } + None + } + + fn compile_object(&mut self, obj: &Object) -> FuncRef { + self.scope.push(Scope::object(obj)); + self.object_items.push(HashMap::new()); + + // Declare all data in this obejct as a global variable. + for data in &obj.data { + self.declare_global_var(data); + } + + for inner_obj in &obj.objects { + let fb = self.compile_object(inner_obj); + self.object_items.last_mut().unwrap().insert( + strip_enclosing_quote(&inner_obj.name).to_string(), + ObjectItem::ContractCode(fb), + ); + } + + // Lower the code section. + // NOTE: Code section is just a normal function in sonatina. + // + // Make a dummy yul funciton for contract code. + let name = Identifier { + id: IdentifierID::UnresolvedReference, + name: "__init__".to_string(), + location: None, + }; + let yul_func = FunctionDefinition { + name, + parameters: Vec::new(), + returns: Vec::new(), + body: obj.code.body.clone(), + location: obj.code.location.clone(), + }; + + let func_ref = self.declare_function(&yul_func); + self.object_items.last_mut().unwrap().insert( + strip_enclosing_quote(&obj.name).to_string(), + ObjectItem::ContractCode(func_ref), + ); + + // Transpile contract code. + let fb = self.mb.func_builder(func_ref); + FuncTranspiler::new(self, fb).build(&yul_func); + + self.object_items.pop(); + self.scope.pop(); + func_ref + } + + fn enter_block(&mut self, yul_block: &YulBlock) { + let mut func_defs: HashMap = HashMap::new(); + self.scope.push(Scope::Block(self.block_number)); + self.funcs.push(HashMap::new()); + + // Collect functions and blocks in this block. + let mut blocks = Vec::new(); + for stmt in &yul_block.statements { + match stmt { + Statement::FunctionDefinition(yul_func) => { + let func_ref = self.declare_function(yul_func); + self.funcs + .last_mut() + .unwrap() + .insert(yul_func.name.name.clone(), func_ref); + func_defs.insert(func_ref, yul_func); + } + Statement::Block(block) => blocks.push(block), + _ => {} + } + } + + self.block_number += 1; + // Transpile yul functions in this block. + for (func_ref, yul_func) in func_defs { + let fb = self.mb.func_builder(func_ref); + FuncTranspiler::new(self, fb).build(yul_func); + } + + // Search and transpile functions in the child blocks. + for block in blocks { + self.enter_block(block); + self.leave_block(); + } + } + + fn declare_function(&mut self, yul_func: &FunctionDefinition) -> FuncRef { + let sig = self.make_sig(yul_func); + self.mb.declare_function(sig) + } + + fn leave_block(&mut self) { + self.scope.pop().unwrap(); + self.funcs.pop().unwrap(); + } + + fn make_sig(&mut self, func_def: &FunctionDefinition) -> Signature { + let name = self.scope.make_prefixed_name(&func_def.name.name); + let args = vec![Type::I256; func_def.parameters.len()]; + let ret_ty = self.declare_ret_ty(func_def.returns.len()); + Signature::new(&name, Linkage::Private, &args, ret_ty) + } + + fn declare_ret_ty(&mut self, n_ret: usize) -> Type { + if let Some(ty) = self.declared_ret_tys.get(&n_ret) { + return *ty; + } + + let ret_ty = if n_ret == 0 { + Type::Unit + } else if n_ret == 1 { + Type::I256 + } else { + let type_name = format!("tuple{n_ret}"); + let fields = vec![Type::I256; n_ret]; + self.mb.declare_struct_type(&type_name, &fields, false) + }; + + self.declared_ret_tys.insert(n_ret, ret_ty); + ret_ty + } + + fn declare_global_var(&mut self, data: &Data) -> GlobalVariable { + let name = &data.name[1..data.name.len() - 1]; + let prefixed_name = self.scope.make_prefixed_name(name); + let (data, ty) = self.parse_data_value(data); + + let gv_data = GlobalVariableData::constant(prefixed_name, ty, Linkage::Private, data); + let gv = self.mb.make_global(gv_data); + self.object_items + .last_mut() + .unwrap() + .insert(name.to_string(), gv.into()); + gv + } + + fn parse_data_value(&self, data: &Data) -> (GvInitializer, Type) { + let data = &data.data; + if data.starts_with("hex") { + // The actual hex literal is surrounded by `"`, or `'`. + let u256 = U256::from_str_radix(&data[4..data.len() - 1], 16).unwrap(); + let cv = GvInitializer::make_imm(I256::make_positive(u256)); + (cv, Type::I256) + } else { + let value: Vec<_> = data + .bytes() + .map(|b| GvInitializer::Immediate(b.into())) + .collect(); + let len = value.len(); + let cv = GvInitializer::make_array(value); + let ty = self.mb.declare_array_type(Type::I8, len); + + (cv, ty) + } + } +} + +impl Default for Ctx { + fn default() -> Self { + Self::new() + } +} + +#[derive(Debug, Clone)] +pub enum Literal { + Number(I256), + String(String), +} + +impl Literal { + pub fn parse(yul_lit: &yul::Literal) -> Self { + let u256 = match yul_lit.literal.as_str() { + lit if lit.starts_with("0x") => U256::from_str_radix(&lit[2..], 16).unwrap(), + + lit if lit.chars().next().unwrap().is_numeric() => { + U256::from_str_radix(lit, 10).unwrap() + } + + "true" => U256::one(), + + "false" => U256::zero(), + + lit => { + return Self::String(strip_enclosing_quote(lit).to_string()); + } + }; + + Self::Number(I256::make_positive(u256)) + } + + pub fn as_imm(&self) -> I256 { + let s = match self { + Self::Number(num) => return *num, + Self::String(s) => s, + }; + + let s = strip_enclosing_quote(s); + let mut bytes: Vec = s.bytes().collect(); + let len = bytes.len(); + bytes.resize(32, 0); + assert!(len <= 32); + I256::from_be_bytes(&bytes) + } +} + +#[derive(Debug, Clone, Copy)] +enum ObjectItem { + GlobalVariable(GlobalVariable), + ContractCode(FuncRef), +} + +impl From for ObjectItem { + fn from(value: GlobalVariable) -> Self { + Self::GlobalVariable(value) + } +} + +impl From for ObjectItem { + fn from(value: FuncRef) -> Self { + Self::ContractCode(value) + } +} + +fn strip_enclosing_quote(s: &str) -> &str { + let len = s.len(); + &s[1..len - 1] +} diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..3b44b39 --- /dev/null +++ b/src/main.rs @@ -0,0 +1,49 @@ +use std::{fs, path::PathBuf}; + +use clap::Parser; +use sonatina_ir::ir_writer::ModuleWriter; +use yul_sonatina::compile; + +fn main() { + let args = Args::parse(); + let input = args.input_file; + let output = args.output.unwrap_or_else(|| { + let stem = input.file_stem().unwrap().to_str().unwrap(); + let mut output = std::env::current_dir().unwrap(); + output.push(format!("{stem}.sntn")); + output + }); + + let src = match fs::read_to_string(input) { + Ok(src) => src, + Err(err) => { + eprintln!("{err}"); + std::process::exit(err.raw_os_error().unwrap()); + } + }; + + let module = match compile(&src) { + Ok(module) => module, + Err(err) => { + eprintln!("{err}"); + std::process::exit(1); + } + }; + + let ir_text = ModuleWriter::new(&module).dump_string(); + if let Err(err) = fs::write(output, ir_text.as_bytes()) { + eprintln!("{err}"); + std::process::exit(err.raw_os_error().unwrap()); + } +} + +#[derive(Parser, Debug)] +#[command(version, about, long_about = None)] +struct Args { + /// The Yul source file to process + input_file: PathBuf, + + /// The output Sonatina file name (optional)t file to process + #[arg(short, long)] + output: Option, +} diff --git a/test_all.sh b/test_all.sh new file mode 100755 index 0000000..6a25ed1 --- /dev/null +++ b/test_all.sh @@ -0,0 +1,7 @@ +#!/bin/bash +set -e + +cargo +nightly fmt --all -- --check +cargo clippy --all-features --all-targets -- -D clippy::all +cargo doc --no-deps +cargo test --all-targets diff --git a/test_files/data.snap b/test_files/data.snap new file mode 100644 index 0000000..436414e --- /dev/null +++ b/test_files/data.snap @@ -0,0 +1,14 @@ +--- +source: tests/transpile.rs +input_file: test_files/data.yul +--- +target = evm-ethereum-cancun + +global private const i256 $Contract1::CONSTANT = 16675 + +func private %Contract1::__init__() -> unit { + block0: + v1.i256 = ptr_to_int $Contract1::CONSTANT i256; + evm_code_copy 0.i256 v1 32.i256; + return; +} diff --git a/test_files/data.yul b/test_files/data.yul new file mode 100644 index 0000000..f984352 --- /dev/null +++ b/test_files/data.yul @@ -0,0 +1,9 @@ +object "Contract1" { + code { + let d := dataoffset("CONSTANT") + let s := datasize("CONSTANT") + codecopy(0, d, s) + } + + data "CONSTANT" hex"4123" +} diff --git a/test_files/erc20.snap b/test_files/erc20.snap new file mode 100644 index 0000000..a203220 --- /dev/null +++ b/test_files/erc20.snap @@ -0,0 +1,353 @@ +--- +source: tests/transpile.rs +input_file: test_files/erc20.yul +--- +target = evm-ethereum-cancun + +func private %Token::runtime::__init__() -> unit { + block0: + v0.i256 = evm_call_value; + v1.i256 = is_zero v0; + call %Token::runtime::block0::require v1; + v2.i256 = call %Token::runtime::block0::selector; + br_table v2 block8 (1889567281.i256 block1) (404098525.i256 block2) (2835717307.i256 block3) (599290589.i256 block4) (157198259.i256 block5) (3714247998.i256 block6) (1086394137.i256 block7); + + block1: + v11.i256 = call %Token::runtime::block0::decodeAsAddress 0.i256; + v12.i256 = call %Token::runtime::block0::balanceOf v11; + call %Token::runtime::block0::returnUint v12; + jump block9; + + block2: + v13.i256 = call %Token::runtime::block0::totalSupply; + call %Token::runtime::block0::returnUint v13; + jump block9; + + block3: + v14.i256 = call %Token::runtime::block0::decodeAsAddress 0.i256; + v16.i256 = call %Token::runtime::block0::decodeAsUint 1.i256; + call %Token::runtime::block0::transfer v14 v16; + call %Token::runtime::block0::returnTrue; + jump block9; + + block4: + v17.i256 = call %Token::runtime::block0::decodeAsAddress 0.i256; + v18.i256 = call %Token::runtime::block0::decodeAsAddress 1.i256; + v20.i256 = call %Token::runtime::block0::decodeAsUint 2.i256; + call %Token::runtime::block0::transferFrom v17 v18 v20; + call %Token::runtime::block0::returnTrue; + jump block9; + + block5: + v21.i256 = call %Token::runtime::block0::decodeAsAddress 0.i256; + v22.i256 = call %Token::runtime::block0::decodeAsUint 1.i256; + call %Token::runtime::block0::approve v21 v22; + call %Token::runtime::block0::returnTrue; + jump block9; + + block6: + v23.i256 = call %Token::runtime::block0::decodeAsAddress 0.i256; + v24.i256 = call %Token::runtime::block0::decodeAsAddress 1.i256; + v25.i256 = call %Token::runtime::block0::allowance v23 v24; + call %Token::runtime::block0::returnUint v25; + jump block9; + + block7: + v26.i256 = call %Token::runtime::block0::decodeAsAddress 0.i256; + v27.i256 = call %Token::runtime::block0::decodeAsUint 1.i256; + call %Token::runtime::block0::mint v26 v27; + call %Token::runtime::block0::returnTrue; + jump block9; + + block8: + evm_revert 0.i256 0.i256; + + block9: + return; +} + +func private %Token::runtime::block0::mint(v0.i256, v1.i256) -> unit { + block0: + v2.i256 = call %Token::runtime::block0::calledByOwner; + call %Token::runtime::block0::require v2; + call %Token::runtime::block0::mintTokens v1; + call %Token::runtime::block0::addToBalance v0 v1; + call %Token::runtime::block0::emitTransfer 0.i256 v0 v1; + return; +} + +func private %Token::runtime::block0::transfer(v0.i256, v1.i256) -> unit { + block0: + v2.i256 = evm_caller; + call %Token::runtime::block0::executeTransfer v2 v0 v1; + return; +} + +func private %Token::runtime::block0::approve(v0.i256, v1.i256) -> unit { + block0: + call %Token::runtime::block0::revertIfZeroAddress v0; + v2.i256 = evm_caller; + call %Token::runtime::block0::setAllowance v2 v0 v1; + v3.i256 = evm_caller; + call %Token::runtime::block0::emitApproval v3 v0 v1; + return; +} + +func private %Token::runtime::block0::transferFrom(v0.i256, v1.i256, v2.i256) -> unit { + block0: + v3.i256 = evm_caller; + call %Token::runtime::block0::decreaseAllowanceBy v0 v3 v2; + call %Token::runtime::block0::executeTransfer v0 v1 v2; + return; +} + +func private %Token::runtime::block0::executeTransfer(v0.i256, v1.i256, v2.i256) -> unit { + block0: + call %Token::runtime::block0::revertIfZeroAddress v1; + call %Token::runtime::block0::deductFromBalance v0 v2; + call %Token::runtime::block0::addToBalance v1 v2; + call %Token::runtime::block0::emitTransfer v0 v1 v2; + return; +} + +func private %Token::runtime::block0::selector() -> i256 { + block0: + v1.i256 = evm_call_data_load 0.i256; + v3.i256 = evm_udiv v1 26959946667150639794667015087019630673637144422540572481103610249216.i256; + return v3; +} + +func private %Token::runtime::block0::decodeAsAddress(v0.i256) -> i256 { + block0: + v1.i256 = call %Token::runtime::block0::decodeAsUint v0; + v3.i256 = not 1461501637330902918203684832716283019655932542975.i256; + v4.i256 = and v1 v3; + v5.i256 = is_zero v4; + v6.i256 = is_zero v5; + br v6 block1 block2; + + block1: + evm_revert 0.i256 0.i256; + + block2: + return v1; +} + +func private %Token::runtime::block0::decodeAsUint(v0.i256) -> i256 { + block0: + v3.i256 = mul v0 32.i256; + v4.i256 = add 4.i256 v3; + v5.i256 = evm_call_data_size; + v6.i256 = add v4 32.i256; + v7.i256 = lt v5 v6; + br v7 block1 block2; + + block1: + evm_revert 0.i256 0.i256; + + block2: + v10.i256 = evm_call_data_load v9; + return v10; +} + +func private %Token::runtime::block0::returnUint(v0.i256) -> unit { + block0: + v2.*i256 = int_to_ptr 0.i256 *i256; + mstore v2 v0 i256; + evm_return 0.i256 32.i256; +} + +func private %Token::runtime::block0::returnTrue() -> unit { + block0: + call %Token::runtime::block0::returnUint 1.i256; + return; +} + +func private %Token::runtime::block0::emitTransfer(v0.i256, v1.i256, v2.i256) -> unit { + block0: + call %Token::runtime::block0::emitEvent 100389287136786176327247604509743168900146139575972864366142685224231313322991.i256 v0 v1 v2; + return; +} + +func private %Token::runtime::block0::emitApproval(v0.i256, v1.i256, v2.i256) -> unit { + block0: + call %Token::runtime::block0::emitEvent 63486140976153616755203102783360879283472101686154884697241723088393386309925.i256 v0 v1 v2; + return; +} + +func private %Token::runtime::block0::emitEvent(v0.i256, v1.i256, v2.i256, v3.i256) -> unit { + block0: + v5.*i256 = int_to_ptr 0.i256 *i256; + mstore v5 v3 i256; + evm_log3 0.i256 32.i256 v0 v1 v2; + return; +} + +func private %Token::runtime::block0::ownerPos() -> i256 { + block0: + return 0.i256; +} + +func private %Token::runtime::block0::totalSupplyPos() -> i256 { + block0: + return 1.i256; +} + +func private %Token::runtime::block0::accountToStorageOffset(v0.i256) -> i256 { + block0: + v2.i256 = add 4096.i256 v0; + return v2; +} + +func private %Token::runtime::block0::allowanceStorageOffset(v0.i256, v1.i256) -> i256 { + block0: + v2.i256 = call %Token::runtime::block0::accountToStorageOffset v0; + v4.*i256 = int_to_ptr 0.i256 *i256; + mstore v4 v2 i256; + v6.*i256 = int_to_ptr 32.i256 *i256; + mstore v6 v1 i256; + v8.i256 = evm_keccak256 0.i256 64.i256; + return v8; +} + +func private %Token::runtime::block0::owner() -> i256 { + block0: + v0.i256 = call %Token::runtime::block0::ownerPos; + v1.i256 = evm_sload v0; + return v1; +} + +func private %Token::runtime::block0::totalSupply() -> i256 { + block0: + v0.i256 = call %Token::runtime::block0::totalSupplyPos; + v1.i256 = evm_sload v0; + return v1; +} + +func private %Token::runtime::block0::mintTokens(v0.i256) -> unit { + block0: + v1.i256 = call %Token::runtime::block0::totalSupplyPos; + v2.i256 = call %Token::runtime::block0::totalSupply; + v3.i256 = call %Token::runtime::block0::safeAdd v2 v0; + evm_sstore v1 v3; + return; +} + +func private %Token::runtime::block0::balanceOf(v0.i256) -> i256 { + block0: + v1.i256 = call %Token::runtime::block0::accountToStorageOffset v0; + v2.i256 = evm_sload v1; + return v2; +} + +func private %Token::runtime::block0::addToBalance(v0.i256, v1.i256) -> unit { + block0: + v2.i256 = call %Token::runtime::block0::accountToStorageOffset v0; + v3.i256 = evm_sload v2; + v4.i256 = call %Token::runtime::block0::safeAdd v3 v1; + evm_sstore v2 v4; + return; +} + +func private %Token::runtime::block0::deductFromBalance(v0.i256, v1.i256) -> unit { + block0: + v2.i256 = call %Token::runtime::block0::accountToStorageOffset v0; + v3.i256 = evm_sload v2; + v4.i256 = call %Token::runtime::block0::lte v1 v3; + call %Token::runtime::block0::require v4; + v5.i256 = sub v3 v1; + evm_sstore v2 v5; + return; +} + +func private %Token::runtime::block0::allowance(v0.i256, v1.i256) -> i256 { + block0: + v2.i256 = call %Token::runtime::block0::allowanceStorageOffset v0 v1; + v3.i256 = evm_sload v2; + return v3; +} + +func private %Token::runtime::block0::setAllowance(v0.i256, v1.i256, v2.i256) -> unit { + block0: + v3.i256 = call %Token::runtime::block0::allowanceStorageOffset v0 v1; + evm_sstore v3 v2; + return; +} + +func private %Token::runtime::block0::decreaseAllowanceBy(v0.i256, v1.i256, v2.i256) -> unit { + block0: + v3.i256 = call %Token::runtime::block0::allowanceStorageOffset v0 v1; + v4.i256 = evm_sload v3; + v5.i256 = call %Token::runtime::block0::lte v2 v4; + call %Token::runtime::block0::require v5; + v6.i256 = sub v4 v2; + evm_sstore v3 v6; + return; +} + +func private %Token::runtime::block0::lte(v0.i256, v1.i256) -> i256 { + block0: + v2.i256 = gt v0 v1; + v3.i256 = is_zero v2; + return v3; +} + +func private %Token::runtime::block0::gte(v0.i256, v1.i256) -> i256 { + block0: + v2.i256 = lt v0 v1; + v3.i256 = is_zero v2; + return v3; +} + +func private %Token::runtime::block0::safeAdd(v0.i256, v1.i256) -> i256 { + block0: + v2.i256 = add v0 v1; + v3.i256 = lt v2 v0; + v4.i256 = lt v2 v1; + v5.i256 = or v3 v4; + br v5 block1 block2; + + block1: + evm_revert 0.i256 0.i256; + + block2: + return v2; +} + +func private %Token::runtime::block0::calledByOwner() -> i256 { + block0: + v0.i256 = call %Token::runtime::block0::owner; + v1.i256 = evm_caller; + v2.i256 = eq v0 v1; + return v2; +} + +func private %Token::runtime::block0::revertIfZeroAddress(v0.i256) -> unit { + block0: + call %Token::runtime::block0::require v0; + return; +} + +func private %Token::runtime::block0::require(v0.i256) -> unit { + block0: + v1.i256 = is_zero v0; + br v1 block1 block2; + + block1: + evm_revert 0.i256 0.i256; + + block2: + return; +} + +func private %Token::__init__() -> unit { + block0: + v1.i256 = evm_caller; + evm_sstore 0.i256 v1; + v2.*() -> unit = get_function_ptr %Token::runtime::__init__; + v3.i256 = ptr_to_int v2 i256; + v4.i256 = evm_contract_size %Token::runtime::__init__; + evm_code_copy 0.i256 v3 v4; + v5.i256 = evm_contract_size %Token::runtime::__init__; + evm_return 0.i256 v5; +} diff --git a/test_files/erc20.yul b/test_files/erc20.yul new file mode 100644 index 0000000..cbc04b5 --- /dev/null +++ b/test_files/erc20.yul @@ -0,0 +1,186 @@ +object "Token" { + code { + // Store the creator in slot zero. + sstore(0, caller()) + + // Deploy the contract + datacopy(0, dataoffset("runtime"), datasize("runtime")) + return(0, datasize("runtime")) + } + object "runtime" { + code { + // Protection against sending Ether + require(iszero(callvalue())) + + // Dispatcher + switch selector() + case 0x70a08231 /* "balanceOf(address)" */ { + returnUint(balanceOf(decodeAsAddress(0))) + } + case 0x18160ddd /* "totalSupply()" */ { + returnUint(totalSupply()) + } + case 0xa9059cbb /* "transfer(address,uint256)" */ { + transfer(decodeAsAddress(0), decodeAsUint(1)) + returnTrue() + } + case 0x23b872dd /* "transferFrom(address,address,uint256)" */ { + transferFrom(decodeAsAddress(0), decodeAsAddress(1), decodeAsUint(2)) + returnTrue() + } + case 0x095ea7b3 /* "approve(address,uint256)" */ { + approve(decodeAsAddress(0), decodeAsUint(1)) + returnTrue() + } + case 0xdd62ed3e /* "allowance(address,address)" */ { + returnUint(allowance(decodeAsAddress(0), decodeAsAddress(1))) + } + case 0x40c10f19 /* "mint(address,uint256)" */ { + mint(decodeAsAddress(0), decodeAsUint(1)) + returnTrue() + } + default { + revert(0, 0) + } + + function mint(account, amount) { + require(calledByOwner()) + + mintTokens(amount) + addToBalance(account, amount) + emitTransfer(0, account, amount) + } + function transfer(to, amount) { + executeTransfer(caller(), to, amount) + } + function approve(spender, amount) { + revertIfZeroAddress(spender) + setAllowance(caller(), spender, amount) + emitApproval(caller(), spender, amount) + } + function transferFrom(from, to, amount) { + decreaseAllowanceBy(from, caller(), amount) + executeTransfer(from, to, amount) + } + + function executeTransfer(from, to, amount) { + revertIfZeroAddress(to) + deductFromBalance(from, amount) + addToBalance(to, amount) + emitTransfer(from, to, amount) + } + + + /* ---------- calldata decoding functions ----------- */ + function selector() -> s { + s := div(calldataload(0), 0x100000000000000000000000000000000000000000000000000000000) + } + + function decodeAsAddress(offset) -> v { + v := decodeAsUint(offset) + if iszero(iszero(and(v, not(0xffffffffffffffffffffffffffffffffffffffff)))) { + revert(0, 0) + } + } + function decodeAsUint(offset) -> v { + let pos := add(4, mul(offset, 0x20)) + if lt(calldatasize(), add(pos, 0x20)) { + revert(0, 0) + } + v := calldataload(pos) + } + /* ---------- calldata encoding functions ---------- */ + function returnUint(v) { + mstore(0, v) + return(0, 0x20) + } + function returnTrue() { + returnUint(1) + } + + /* -------- events ---------- */ + function emitTransfer(from, to, amount) { + let signatureHash := 0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef + emitEvent(signatureHash, from, to, amount) + } + function emitApproval(from, spender, amount) { + let signatureHash := 0x8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b925 + emitEvent(signatureHash, from, spender, amount) + } + function emitEvent(signatureHash, indexed1, indexed2, nonIndexed) { + mstore(0, nonIndexed) + log3(0, 0x20, signatureHash, indexed1, indexed2) + } + + /* -------- storage layout ---------- */ + function ownerPos() -> p { p := 0 } + function totalSupplyPos() -> p { p := 1 } + function accountToStorageOffset(account) -> offset { + offset := add(0x1000, account) + } + function allowanceStorageOffset(account, spender) -> offset { + offset := accountToStorageOffset(account) + mstore(0, offset) + mstore(0x20, spender) + offset := keccak256(0, 0x40) + } + + /* -------- storage access ---------- */ + function owner() -> o { + o := sload(ownerPos()) + } + function totalSupply() -> supply { + supply := sload(totalSupplyPos()) + } + function mintTokens(amount) { + sstore(totalSupplyPos(), safeAdd(totalSupply(), amount)) + } + function balanceOf(account) -> bal { + bal := sload(accountToStorageOffset(account)) + } + function addToBalance(account, amount) { + let offset := accountToStorageOffset(account) + sstore(offset, safeAdd(sload(offset), amount)) + } + function deductFromBalance(account, amount) { + let offset := accountToStorageOffset(account) + let bal := sload(offset) + require(lte(amount, bal)) + sstore(offset, sub(bal, amount)) + } + function allowance(account, spender) -> amount { + amount := sload(allowanceStorageOffset(account, spender)) + } + function setAllowance(account, spender, amount) { + sstore(allowanceStorageOffset(account, spender), amount) + } + function decreaseAllowanceBy(account, spender, amount) { + let offset := allowanceStorageOffset(account, spender) + let currentAllowance := sload(offset) + require(lte(amount, currentAllowance)) + sstore(offset, sub(currentAllowance, amount)) + } + + /* ---------- utility functions ---------- */ + function lte(a, b) -> r { + r := iszero(gt(a, b)) + } + function gte(a, b) -> r { + r := iszero(lt(a, b)) + } + function safeAdd(a, b) -> r { + r := add(a, b) + if or(lt(r, a), lt(r, b)) { revert(0, 0) } + } + function calledByOwner() -> cbo { + cbo := eq(owner(), caller()) + } + function revertIfZeroAddress(addr) { + require(addr) + } + function require(condition) { + if iszero(condition) { revert(0, 0) } + } + } + } +} diff --git a/test_files/nested_object.snap b/test_files/nested_object.snap new file mode 100644 index 0000000..3c8dc59 --- /dev/null +++ b/test_files/nested_object.snap @@ -0,0 +1,73 @@ +--- +source: tests/transpile.rs +input_file: test_files/nested_object.yul +--- +target = evm-ethereum-cancun + +global private const i256 $Contract1::Table2 = 16675 + +func private %Contract1::Contract1_deployed::__init__() -> unit { + block0: + evm_return 0.i256 32.i256; +} + +func private %Contract1::Contract1_deployed::block0::allocate(v0.i256) -> i256 { + block0: + v2.*i256 = int_to_ptr 64.i256 *i256; + v3.i256 = mload v2 i256; + v4.i256 = is_zero v3; + br v4 block1 block2; + + block1: + jump block2; + + block2: + v6.i256 = phi (v3 block0) (96.i256 block1); + v8.i256 = add v6 v0; + v9.*i256 = int_to_ptr 64.i256 *i256; + mstore v9 v8 i256; + return v6; +} + +func private %Contract1::Contract2::__init__() -> unit { + block0: + return; +} + +func private %Contract1::__init__() -> unit { + block0: + v0.i256 = evm_contract_size %Contract1::Contract2::__init__; + v1.i256 = call %Contract1::block4::allocate v0; + v2.*() -> unit = get_function_ptr %Contract1::Contract2::__init__; + v3.i256 = ptr_to_int v2 i256; + evm_code_copy v1 v3 v0; + v4.i256 = add v1 v0; + v6.*i256 = int_to_ptr v4 *i256; + mstore v6 4660.i256 i256; + v9.i256 = add v0 32.i256; + v10.i256 = evm_create 0.i256 v1 v9; + v11.i256 = evm_contract_size %Contract1::Contract1_deployed::__init__; + v12.i256 = call %Contract1::block4::allocate v11; + v13.*() -> unit = get_function_ptr %Contract1::Contract1_deployed::__init__; + v14.i256 = ptr_to_int v13 i256; + evm_code_copy v12 v14 v11; + evm_return v12 v11; +} + +func private %Contract1::block4::allocate(v0.i256) -> i256 { + block0: + v2.*i256 = int_to_ptr 64.i256 *i256; + v3.i256 = mload v2 i256; + v4.i256 = is_zero v3; + br v4 block1 block2; + + block1: + jump block2; + + block2: + v6.i256 = phi (v3 block0) (96.i256 block1); + v8.i256 = add v6 v0; + v9.*i256 = int_to_ptr 64.i256 *i256; + mstore v9 v8 i256; + return v6; +} diff --git a/test_files/nested_object.yul b/test_files/nested_object.yul new file mode 100644 index 0000000..ff6d671 --- /dev/null +++ b/test_files/nested_object.yul @@ -0,0 +1,39 @@ +object "Contract1" { + code { + function allocate(size) -> ptr { + ptr := mload(0x40) + if iszero(ptr) { ptr := 0x60 } + mstore(0x40, add(ptr, size)) + } + + let size := datasize("Contract2") + let offset := allocate(size) + datacopy(offset, dataoffset("Contract2"), size) + mstore(add(offset, size), 0x1234) + pop(create(0, offset, add(size, 32))) + + size := datasize("Contract1_deployed") + offset := allocate(size) + datacopy(offset, dataoffset("Contract1_deployed"), size) + return(offset, size) + } + + data "Table2" hex"4123" + + object "Contract1_deployed" { + code { + function allocate(size) -> ptr { + ptr := mload(0x40) + if iszero(ptr) { ptr := 0x60 } + mstore(0x40, add(ptr, size)) + } + + return(0, 0x20) + } + } + + object "Contract2" { + code { + } + } +} diff --git a/test_files/power.snap b/test_files/power.snap new file mode 100644 index 0000000..72bb363 --- /dev/null +++ b/test_files/power.snap @@ -0,0 +1,59 @@ +--- +source: tests/transpile.rs +input_file: test_files/power.yul +--- +target = evm-ethereum-cancun + +func private %block0::power(v0.i256, v1.i256) -> i256 { + block0: + br_table v1 block3 (0.i256 block1) (1.i256 block2); + + block1: + jump block4; + + block2: + jump block4; + + block3: + v6.i256 = mul v0 v0; + v9.i256 = evm_udiv v1 2.i256; + v10.i256 = call %block0::power v6 v9; + v11.i256 = evm_umod v1 2.i256; + br_table v11 block6 (1.i256 block5); + + block4: + v15.i256 = phi (1.i256 block1) (v0 block2) (v16 block6); + return v15; + + block5: + v14.i256 = mul v0 v10; + jump block6; + + block6: + v16.i256 = phi (v10 block3) (v14 block5); + jump block4; +} + +func private %block0::power2(v0.i256, v1.i256) -> i256 { + block0: + jump block1; + + block1: + v11.i256 = phi (v0 block0) (v11 block3); + v10.i256 = phi (1.i256 block0) (v9 block3); + v5.i256 = phi (v1 block0) (v5 block3); + v4.i256 = phi (0.i256 block0) (v13 block3); + v6.i256 = lt v4 v5; + br v6 block2 block4; + + block2: + v9.i256 = mul v10 v11; + jump block3; + + block3: + v13.i256 = add v4 1.i256; + jump block1; + + block4: + return v10; +} diff --git a/test_files/power.yul b/test_files/power.yul new file mode 100644 index 0000000..6031b89 --- /dev/null +++ b/test_files/power.yul @@ -0,0 +1,24 @@ +{ + function power(base, exponent) -> result + { + switch exponent + case 0 { result := 1 } + case 1 { result := base } + default + { + result := power(mul(base, base), div(exponent, 2)) + switch mod(exponent, 2) + case 1 { result := mul(base, result) } + } + } + + function power2(base, exponent) -> result + { + result := 1 + for { let i := 0 } lt(i, exponent) { i := add(i, 1) } + { + result := mul(result, base) + } + } + +} diff --git a/tests/transpile.rs b/tests/transpile.rs new file mode 100644 index 0000000..02a175d --- /dev/null +++ b/tests/transpile.rs @@ -0,0 +1,59 @@ +use dir_test::{dir_test, Fixture}; +use sonatina_ir::ir_writer::ModuleWriter; +use yul_sonatina::compile; + +// copied from fe test-utils +/// A macro to assert that a value matches a snapshot. +/// If the snapshot does not exist, it will be created in the same directory as +/// the test file. +#[macro_export] +macro_rules! snap_test { + ($value:expr, $fixture_path: expr) => { + snap_test!($value, $fixture_path, None) + }; + + ($value:expr, $fixture_path: expr, $suffix: expr) => { + let mut settings = insta::Settings::new(); + let fixture_path = ::std::path::Path::new($fixture_path); + let fixture_dir = fixture_path.parent().unwrap(); + let fixture_name = fixture_path.file_stem().unwrap().to_str().unwrap(); + + settings.set_snapshot_path(fixture_dir); + settings.set_input_file($fixture_path); + settings.set_prepend_module_to_snapshot(false); + settings.set_omit_expression(true); + + let suffix: Option<&str> = $suffix; + let name = if let Some(suffix) = suffix { + format!("{fixture_name}.{suffix}") + } else { + fixture_name.into() + }; + settings.bind(|| { + insta::_macro_support::assert_snapshot( + (name, $value.as_str()).into(), + std::path::Path::new(env!("CARGO_MANIFEST_DIR")), + fixture_name, + module_path!(), + file!(), + line!(), + &$value, + ) + .unwrap() + }) + }; +} + +#[dir_test( + dir: "$CARGO_MANIFEST_DIR/test_files", + glob: "*.yul", +)] +fn test(fixture: Fixture<&str>) { + compile(fixture.content()).unwrap(); + + let module = compile(fixture.content()).unwrap(); + + let mut module_writer = ModuleWriter::new(&module); + let ir = module_writer.dump_string(); + snap_test!(ir, fixture.path()); +}